Upgrade mobile app Expo and browser extension WXT frameworks (#2067)

* Bump the npm_and_yarn group across 2 directories with 2 updates

Bumps the npm_and_yarn group with 1 update in the /apps/browser-extension directory: [ws](https://github.com/websockets/ws).
Bumps the npm_and_yarn group with 1 update in the /apps/mobile-app directory: [postcss](https://github.com/postcss/postcss).


Updates `ws` from 8.18.2 to 8.21.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.2...8.21.0)

Updates `ws` from 8.18.2 to 8.21.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.2...8.21.0)

Updates `postcss` from 8.4.49 to 8.5.15
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.49...8.5.15)

Updates `postcss` from 8.4.49 to 8.5.15
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.49...8.5.15)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.15
  dependency-type: indirect
- dependency-name: ws
  dependency-version: 8.21.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update package-lock.json (#2067)

* Update NodeJS to latest 24.x LTS

* Revert Expo back to 53.x (#2067)

* Upgrade expo and react-native (#2067)

* Update mobile app to support Expo 56 (#2067)

* Update wxt (#2067)

* Change util filenames to be consistent PascalCase (#2067)

* Restore deep-linking behavior to work with new Expo Router (#2067)

* Update eslint.config.js (#2067)

* Update package-lock.json (#2067)

* Refactor linting (#2067)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Leendert de Borst <ldeborst@xivisoft.com>
This commit is contained in:
dependabot[bot]
2026-05-26 10:47:39 +02:00
committed by GitHub
co-authored by dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Leendert de Borst
parent 999b465467
commit 423330c864
59 changed files with 7495 additions and 6691 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ runs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'npm'
cache-dependency-path: apps/mobile-app/package-lock.json
@@ -15,7 +15,7 @@ runs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'npm'
cache-dependency-path: apps/browser-extension/package-lock.json
@@ -12,7 +12,7 @@ runs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
+1 -1
View File
@@ -20,7 +20,7 @@ runs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'npm'
cache-dependency-path: apps/mobile-app/package-lock.json
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'npm'
cache-dependency-path: apps/mobile-app/package-lock.json
@@ -35,7 +35,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -105,7 +105,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'npm'
cache-dependency-path: apps/browser-extension/package-lock.json
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'npm'
cache-dependency-path: apps/mobile-app/package-lock.json
+1 -1
View File
@@ -80,7 +80,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
+2 -2
View File
@@ -51,7 +51,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'npm'
cache-dependency-path: apps/mobile-app/package-lock.json
@@ -104,7 +104,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '24'
cache: 'npm'
cache-dependency-path: apps/mobile-app/package-lock.json
+1 -1
View File
@@ -25,7 +25,7 @@ export default [
ecmaVersion: "latest",
sourceType: "module",
project: "./tsconfig.json",
tsconfigRootDir: ".",
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
+1550 -421
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -39,13 +39,13 @@
"buffer": "^6.0.3",
"dompurify": "^3.4.0",
"globals": "^16.0.0",
"i18next": "^25.3.1",
"i18next": "^26.2.0",
"otpauth": "^9.3.6",
"qrcode": "^1.5.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.57.0",
"react-i18next": "^15.6.0",
"react-i18next": "^17.0.8",
"react-router-dom": "^7.12.0",
"secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0",
"sql.js": "^1.12.0",
@@ -77,10 +77,10 @@
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.10",
"postcss": "^8.5.15",
"tailwindcss": "^3.4.17",
"typescript": "^5.6.3",
"vite-plugin-static-copy": "^2.3.2",
"wxt": "^0.20.11"
"vite-plugin-static-copy": "^3.4.0",
"wxt": "^0.20.26"
}
}
+5 -3
View File
@@ -98,6 +98,8 @@ android {
// Instrumented test configuration
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
signingConfigs {
debug {
@@ -245,15 +247,15 @@ dependencies {
if (isGifEnabled) {
// For animated gif support
implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}")
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
}
if (isWebpEnabled) {
// For webp support
implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}")
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
if (isWebpAnimatedEnabled) {
// Animated webp support
implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}")
implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
}
}
@@ -8,14 +8,11 @@ import androidx.lifecycle.ProcessLifecycleOwner
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.common.ReleaseLevel
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
import expo.modules.ExpoReactHostFactory
import net.aliasvault.app.nativevaultmanager.NativeVaultManagerPackage
import net.aliasvault.app.vaultstore.VaultStore
@@ -24,39 +21,26 @@ import net.aliasvault.app.vaultstore.VaultStore
*/
class MainApplication : Application(), ReactApplication {
/**
* The react native host.
*/
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
this,
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages
override val reactHost: ReactHost by lazy {
ExpoReactHostFactory.getDefaultReactHost(
context = applicationContext,
packageList = PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(NativeVaultManagerPackage())
return packages
}
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
},
)
override val reactHost: ReactHost
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
// add(MyReactNativePackage())
add(NativeVaultManagerPackage())
},
)
}
override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
@Suppress("SwallowedException")
DefaultNewArchitectureEntryPoint.releaseLevel = try {
ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
} catch (e: IllegalArgumentException) {
ReleaseLevel.STABLE
}
loadReactNative(this)
ApplicationLifecycleDispatcher.onApplicationCreate(this)
// Setup process lifecycle observer for auto-lock functionality
+4 -4
View File
@@ -2,11 +2,11 @@
buildscript {
ext {
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '36.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '30')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35')
kotlinVersion = findProperty('android.kotlinVersion') ?: '2.0.21'
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '36')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '36')
kotlinVersion = findProperty('android.kotlinVersion') ?: '2.1.20'
detektVersion = '1.23.5'
ndkVersion = "28.0.12433566"
+25 -24
View File
@@ -1,38 +1,39 @@
pluginManagement {
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().toString())
def reactNativeGradlePlugin = new File(
providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
}.standardOutput.asText.get().trim()
).getParentFile().absolutePath
includeBuild(reactNativeGradlePlugin)
def expoPluginsPath = new File(
providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
}.standardOutput.asText.get().trim(),
"../android/expo-gradle-plugin"
).absolutePath
includeBuild(expoPluginsPath)
}
plugins {
id("com.facebook.react.settings")
id("expo-autolinking-settings")
}
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
ex.autolinkLibrariesFromCommand()
} else {
def command = [
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
'react-native-config',
'--json',
'--platform',
'android'
].toList()
ex.autolinkLibrariesFromCommand(command)
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
}
}
expoAutolinking.useExpoModules()
rootProject.name = 'AliasVault'
dependencyResolutionManagement {
versionCatalogs {
reactAndroidLibs {
from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml")))
}
}
}
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
useExpoModules()
expoAutolinking.useExpoVersionCatalog()
include ':app'
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile())
includeBuild(expoAutolinking.reactNativeGradlePlugin)
+17 -20
View File
@@ -7,7 +7,6 @@
"icon": "./assets/images/icon.png",
"scheme": "aliasvault",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"platforms": [
"ios",
"android",
@@ -15,30 +14,14 @@
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "net.aliasvault.app",
"splash": {
"image": "./assets/images/adaptive-icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff"
}
"bundleIdentifier": "net.aliasvault.app"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#000000"
},
"package": "net.aliasvault.app",
"splash": {
"image": "./assets/images/adaptive-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"dark": {
"image": "./assets/images/adaptive-icon.png",
"resizeMode": "contain",
"backgroundColor": "#202020"
}
}
"package": "net.aliasvault.app"
},
"web": {
"bundler": "metro",
@@ -56,7 +39,21 @@
}
}
],
"expo-localization"
"expo-localization",
[
"expo-splash-screen",
{
"image": "./assets/images/adaptive-icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"dark": {
"image": "./assets/images/adaptive-icon.png",
"resizeMode": "contain",
"backgroundColor": "#202020"
}
}
]
],
"experiments": {
"typedRoutes": true
@@ -103,6 +103,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
}, [dbContext, id, webApi, t]);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- loadEmail is async; setState only fires after data is fetched
loadEmail();
}, [id, loadEmail]);
+4 -3
View File
@@ -31,7 +31,7 @@ export default function EmailsScreen() : React.ReactNode {
const webApi = useWebApi();
const colors = useColors();
const navigation = useNavigation();
const scrollY = useRef(new Animated.Value(0)).current;
const [scrollY] = useState(() => new Animated.Value(0));
const scrollViewRef = useRef<ScrollView>(null);
const [error, setError] = useState<string | null>(null);
const [emails, setEmails] = useState<MailboxEmail[]>([]);
@@ -113,7 +113,7 @@ export default function EmailsScreen() : React.ReactNode {
* Load more emails (next page).
*/
const loadMoreEmails = useCallback(async () : Promise<void> => {
if (isLoadingMore || !dbContext?.sqliteClient || dbContext.isOffline) {
if (isLoadingMore || !dbContext.sqliteClient || dbContext.isOffline) {
return;
}
@@ -151,7 +151,7 @@ export default function EmailsScreen() : React.ReactNode {
} finally {
setIsLoadingMore(false);
}
}, [isLoadingMore, dbContext?.sqliteClient, dbContext.isOffline, webApi, currentPage, PAGE_SIZE, t]);
}, [isLoadingMore, dbContext.sqliteClient, dbContext.isOffline, webApi, currentPage, PAGE_SIZE, t]);
useEffect(() => {
const unsubscribeFocus = navigation.addListener('focus', () => {
@@ -189,6 +189,7 @@ export default function EmailsScreen() : React.ReactNode {
* Load emails on mount.
*/
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- loadEmails is async; setState only fires after data is fetched
loadEmails();
}, [loadEmails]);
+26 -19
View File
@@ -1,9 +1,9 @@
import { Buffer } from 'buffer';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { usePreventRemove, NavigationAction } from '@react-navigation/native';
import * as Haptics from 'expo-haptics';
import { Stack, useLocalSearchParams, useNavigation, useRouter } from 'expo-router';
import { usePreventRemove, type NavigationAction } from 'expo-router/react-navigation';
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, View, Keyboard, Platform, ScrollView, KeyboardAvoidingView, TouchableOpacity } from 'react-native';
@@ -747,6 +747,27 @@ export default function AddEditItemScreen(): React.ReactNode {
setHasUnsavedChanges(true);
}, []);
/**
* Show the 2FA section and auto-open add modal if no codes exist.
*/
const handle2FAAdd = useCallback((): void => {
setShow2FA(true);
// Auto-open the add modal if there are no TOTP codes yet
if (totpCodes.filter(tc => !tc.IsDeleted).length === 0) {
// Defer to next tick to ensure the section is rendered first
setTimeout(() => {
totpShowAddFormRef.current?.();
}, 100);
}
}, [totpCodes]);
/**
* Show the attachments section.
*/
const handleAttachmentsAdd = useCallback((): void => {
setShowAttachments(true);
}, []);
/**
* Optional sections for AddFieldMenu.
*/
@@ -754,35 +775,21 @@ export default function AddEditItemScreen(): React.ReactNode {
const sections: OptionalSection[] = [];
// 2FA - only for types with login fields
if (hasLoginFields) {
// eslint-disable-next-line react-hooks/refs -- handle2FAAdd accesses totpShowAddFormRef only inside a setTimeout callback (event handler context), not during render
sections.push({
key: '2fa',
isVisible: show2FA,
/**
* Show the 2FA section and auto-open add modal if no codes exist
*/
onAdd: () => {
setShow2FA(true);
// Auto-open the add modal if there are no TOTP codes yet
if (totpCodes.filter(tc => !tc.IsDeleted).length === 0) {
// Defer to next tick to ensure the section is rendered first
setTimeout(() => {
totpShowAddFormRef.current?.();
}, 100);
}
}
onAdd: handle2FAAdd,
});
}
// Attachments - always available
sections.push({
key: 'attachments',
isVisible: showAttachments,
/**
* Show the attachments section
*/
onAdd: () => setShowAttachments(true)
onAdd: handleAttachmentsAdd,
});
return sections;
}, [hasLoginFields, show2FA, showAttachments, totpCodes]);
}, [hasLoginFields, show2FA, showAttachments, handle2FAAdd, handleAttachmentsAdd]);
/**
* Submit the form.
+1 -1
View File
@@ -1,4 +1,4 @@
import { useFocusEffect } from '@react-navigation/native';
import { useFocusEffect } from 'expo-router';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -1,6 +1,5 @@
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useNavigation } from '@react-navigation/native';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router';
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, Platform, View, Text, TextInput, TouchableOpacity, RefreshControl, FlatList } from 'react-native';
@@ -12,9 +11,9 @@ import type { CredentialSortOrder } from '@/utils/db/repositories/SettingsReposi
import type { Item, ItemType } from '@/utils/dist/core/models/vault';
import { getFieldValue, FieldKey, ItemTypes } from '@/utils/dist/core/models/vault';
import emitter from '@/utils/EventEmitter';
import { canHaveSubfolders, getRecursiveItemCount } from '@/utils/folderUtils';
import { canHaveSubfolders, getRecursiveItemCount } from '@/utils/FolderUtils';
import { HapticsUtility } from '@/utils/HapticsUtility';
import { applyTypeFilter, isItemTypeFilter, parseItemFilterType, type ItemFilterType } from '@/utils/itemFilters';
import { applyTypeFilter, isItemTypeFilter, parseItemFilterType, type ItemFilterType } from '@/utils/ItemFilters';
import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError';
import { useColors } from '@/hooks/useColorScheme';
@@ -317,6 +316,7 @@ export default function FolderViewScreen(): React.ReactNode {
return;
}
// eslint-disable-next-line react-hooks/set-state-in-effect -- syncing loading state with async data fetch on auth/db readiness
setIsLoadingItems(true);
loadItems();
}, [isAuthenticated, isDatabaseAvailable, loadItems, setIsLoadingItems]);
+5 -4
View File
@@ -1,6 +1,5 @@
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useNavigation } from '@react-navigation/native';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router';
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, Text, Platform, Animated, TextInput, TouchableOpacity, View, RefreshControl } from 'react-native';
@@ -13,7 +12,7 @@ import type { Item, ItemType } from '@/utils/dist/core/models/vault';
import { getFieldValue, FieldKey, ItemTypes } from '@/utils/dist/core/models/vault';
import emitter from '@/utils/EventEmitter';
import { HapticsUtility } from '@/utils/HapticsUtility';
import { applyTypeFilter, isItemTypeFilter, type ItemFilterType } from '@/utils/itemFilters';
import { applyTypeFilter, isItemTypeFilter, type ItemFilterType } from '@/utils/ItemFilters';
import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError';
import { useColors } from '@/hooks/useColorScheme';
@@ -66,7 +65,7 @@ export default function ItemsScreen(): React.ReactNode {
const { t } = useTranslation();
const colors = useColors();
const navigate = useNavigationDebounce();
const scrollY = useRef(new Animated.Value(0)).current;
const [scrollY] = useState(() => new Animated.Value(0));
const navigation = useNavigation();
const insets = useSafeAreaInsets();
const flatListRef = useRef<Animated.FlatList<Item | null>>(null);
@@ -113,6 +112,7 @@ export default function ItemsScreen(): React.ReactNode {
useEffect(() => {
if (itemUrl) {
const decodedUrl = decodeURIComponent(itemUrl);
// eslint-disable-next-line react-hooks/set-state-in-effect -- syncing search query with external deep-link parameter
setSearchQuery(decodedUrl);
}
}, [itemUrl]);
@@ -430,6 +430,7 @@ export default function ItemsScreen(): React.ReactNode {
}
setIsLoadingItems(true);
// eslint-disable-next-line react-hooks/set-state-in-effect -- loadItems is async; setState only fires after data is fetched
loadItems();
}, [isAuthenticated, isDatabaseAvailable, loadItems, setIsLoadingItems]);
@@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons';
import { useFocusEffect, useRouter } from 'expo-router';
import { useState, useCallback, useRef, useEffect } from 'react';
import { useState, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, View, TouchableOpacity } from 'react-native';
@@ -28,18 +28,12 @@ export default function IdentityGeneratorLanguageScreen(): React.ReactNode {
const { executeVaultMutation } = useVaultMutate();
const [language, setLanguage] = useState<string>('en');
const [languageOptions, setLanguageOptions] = useState<ILanguageOption[]>([]);
const [languageOptions] = useState<ILanguageOption[]>(() => getAvailableLanguages());
// Store pending changes and initial values
const pendingChanges = useRef<{ language?: string }>({});
const initialValue = useRef<string>('en');
// Load available languages on mount
useEffect(() => {
const languages = getAvailableLanguages();
setLanguageOptions(languages);
}, []);
useFocusEffect(
useCallback(() => {
/**
@@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons';
import { useFocusEffect, useRouter } from 'expo-router';
import { useState, useCallback, useRef, useEffect } from 'react';
import { useState, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, View, TouchableOpacity } from 'react-native';
@@ -29,8 +29,8 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
const [language, setLanguage] = useState<string>('en');
const [gender, setGender] = useState<string>('random');
const [ageRange, setAgeRange] = useState<string>('random');
const [languageOptions, setLanguageOptions] = useState<ILanguageOption[]>([]);
const [ageRangeOptions, setAgeRangeOptions] = useState<IAgeRangeOption[]>([]);
const [languageOptions] = useState<ILanguageOption[]>(() => getAvailableLanguages());
const [ageRangeOptions] = useState<IAgeRangeOption[]>(() => getAvailableAgeRanges());
// Store pending changes and initial values (language is managed in subview)
const pendingChanges = useRef<{ gender?: string; ageRange?: string }>({});
@@ -42,14 +42,6 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
{ label: t('settings.identityGeneratorSettings.genderOptions.female'), value: 'female' }
];
// Load available languages and age ranges on mount
useEffect(() => {
const languages = getAvailableLanguages();
const ranges = getAvailableAgeRanges();
setLanguageOptions(languages);
setAgeRangeOptions(ranges);
}, []);
useFocusEffect(
useCallback(() => {
/**
@@ -37,7 +37,7 @@ export default function SettingsScreen() : React.ReactNode {
const { logoutUserInitiated } = useLogout();
const { loadApiUrl, getDisplayUrl } = useApiUrl();
const navigate = useNavigationDebounce();
const scrollY = useRef(new Animated.Value(0)).current;
const [scrollY] = useState(() => new Animated.Value(0));
const scrollViewRef = useRef<ScrollView>(null);
const [autoLockDisplay, setAutoLockDisplay] = useState<string>('');
const [clipboardClearDisplay, setClipboardClearDisplay] = useState<string>('');
@@ -7,7 +7,7 @@ import { StyleSheet, View, TouchableOpacity, Switch, Platform } from 'react-nati
import type { PasswordSettings } from '@/utils/dist/core/models/vault';
import { CreatePasswordGenerator } from '@/utils/dist/core/password-generator';
import { sliderToLength, lengthToSlider, SLIDER_MIN, SLIDER_MAX } from '@/utils/passwordLengthSlider';
import { sliderToLength, lengthToSlider, SLIDER_MIN, SLIDER_MAX } from '@/utils/PasswordLengthSlider';
import { useColors } from '@/hooks/useColorScheme';
import { useVaultMutate } from '@/hooks/useVaultMutate';
@@ -144,6 +144,7 @@ export default function ActiveSessionsScreen() : React.ReactNode {
};
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- loadSessions is async; setState only fires after data is fetched
loadSessions();
}, [loadSessions]);
@@ -117,6 +117,7 @@ export default function AuthLogsScreen() : React.ReactNode {
};
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- loadLogs is async; setState only fires after data is fetched
loadLogs();
}, [loadLogs]);
@@ -202,11 +202,12 @@ export default function DeleteAccountScreen(): React.ReactNode {
};
/**
* Renders a warning item with a bullet point
* Renders a warning item with a bullet point.
* Called inline (not as a JSX component) to avoid creating a new component on every render.
* @param text - The warning text to display
* @returns A React component
* @returns The warning item JSX
*/
const WarningItem = ({ text }: { text: string }): React.ReactNode => (
const renderWarningItem = (text: string): React.ReactNode => (
<View style={styles.warningItem}>
<ThemedText style={styles.warningIcon}></ThemedText>
<ThemedText style={styles.warningItemText}>{text}</ThemedText>
@@ -236,9 +237,9 @@ export default function DeleteAccountScreen(): React.ReactNode {
<ThemedText style={styles.warningText}>
{t('settings.securitySettings.deleteAccount.warningText')}
</ThemedText>
<WarningItem text={t('settings.securitySettings.deleteAccount.warningVaults')} />
<WarningItem text={t('settings.securitySettings.deleteAccount.warningAliases')} />
<WarningItem text={t('settings.securitySettings.deleteAccount.warningRecovery')} />
{renderWarningItem(t('settings.securitySettings.deleteAccount.warningVaults'))}
{renderWarningItem(t('settings.securitySettings.deleteAccount.warningAliases'))}
{renderWarningItem(t('settings.securitySettings.deleteAccount.warningRecovery'))}
<View style={styles.inputContainer}>
<ThemedText style={styles.label}>{t('settings.securitySettings.deleteAccount.enterUsername')}</ThemedText>
<ThemedTextInput
@@ -259,7 +260,7 @@ export default function DeleteAccountScreen(): React.ReactNode {
<ThemedText style={styles.warningText}>
{t('settings.securitySettings.deleteAccount.finalWarning')}
</ThemedText>
<WarningItem text={t('settings.securitySettings.deleteAccount.irreversibleWarning')} />
{renderWarningItem(t('settings.securitySettings.deleteAccount.irreversibleWarning'))}
<View style={styles.inputContainer}>
<ThemedText style={styles.label}>{t('settings.securitySettings.deleteAccount.password')}</ThemedText>
<ThemedTextInput
+37 -85
View File
@@ -1,19 +1,18 @@
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Href, Stack, useRouter } from 'expo-router';
import { DarkTheme, DefaultTheme, Stack, ThemeProvider, usePathname, useRouter } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect, useRef, useState } from 'react';
import { Linking, StyleSheet, Platform } from 'react-native';
import { useEffect, useRef } from 'react';
import { Linking, Platform } from 'react-native';
import 'react-native-reanimated';
import 'react-native-get-random-values';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { install } from 'react-native-quick-crypto';
import { resolveDeepLink } from '@/utils/DeepLinkResolver';
import { useColors, useColorScheme } from '@/hooks/useColorScheme';
import SpaceMono from '@/assets/fonts/SpaceMono-Regular.ttf';
import LoadingIndicator from '@/components/LoadingIndicator';
import { ThemedView } from '@/components/themed/ThemedView';
import { AliasVaultToast } from '@/components/Toast';
import { AppProvider } from '@/context/AppContext';
import { AuthProvider } from '@/context/AuthContext';
@@ -26,6 +25,11 @@ import { initI18n } from '@/i18n';
SplashScreen.preventAutoHideAsync();
/*
* Install react-native-quick-crypto synchronously at module load.
*/
install();
/**
* Root layout navigation.
*/
@@ -33,91 +37,44 @@ function RootLayoutNav() : React.ReactNode {
const colorScheme = useColorScheme();
const colors = useColors();
const router = useRouter();
const pathname = usePathname();
const navigation = useNavigation();
const [bootComplete, setBootComplete] = useState(false);
const hasBooted = useRef(false);
const pendingActionPath = useRef<string | null>(null);
const splashHidden = useRef(false);
const { bootHandled } = navigation;
useEffect(() => {
if (hasBooted.current) {
return;
}
hasBooted.current = true;
/**
* Initialize the app by redirecting to the initialize page.
* Initialize i18n and inspect the cold-start deep link in parallel.
* If a deep link is detected, set the return URL so after succesful
* initialization, the app will navigate to the deep link target.
*/
const initializeApp = async () : Promise<void> => {
if (hasBooted.current) {
return;
(async (): Promise<void> => {
const [, initialUrl] = await Promise.all([initI18n(), Linking.getInitialURL()]);
const resolved = await resolveDeepLink(initialUrl);
if (resolved) {
navigation.setReturnUrl(resolved);
}
router.replace('/initialize');
// Install the react-native-quick-crypto library which is used by the EncryptionUtility
install();
// Initialize i18n and wait for it to be ready
await initI18n();
hasBooted.current = true;
// Check if we have a pending deep link and pass it to initialize
const initialUrl = await Linking.getInitialURL();
if (initialUrl) {
const path = initialUrl
.replace('net.aliasvault.app://', '')
.replace('aliasvault://', '')
.replace('exp+aliasvault://', '');
/*
* Action URLs (open/...) are owned by the /open/[...path] route (ActionHandler).
* We route there explicitly in the redirect effect below instead of going via
* the NavigationContext. This prevents double navigation after vault unlock.
*/
if (path.startsWith('open/')) {
pendingActionPath.current = path;
} else {
navigation.setReturnUrl({ path });
}
}
setBootComplete(true);
};
initializeApp();
navigation.markBootHandled();
})();
}, [navigation, router]);
useEffect(() => {
/**
* Redirect to a explicit target page if we have one (in case of non-happy path).
*/
const redirect = async () : Promise<void> => {
if (!bootComplete) {
return;
}
if (pendingActionPath.current) {
router.replace(`/${pendingActionPath.current}` as Href);
return;
}
router.replace('/initialize');
};
redirect();
}, [bootComplete, router]);
const styles = StyleSheet.create({
container: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
});
if (!bootComplete) {
return (
<ThemedView style={styles.container}>
{/* Loading state while booting */}
<LoadingIndicator />
</ThemedView>
);
}
if (splashHidden.current || !bootHandled || !pathname || pathname === '/') {
return;
}
splashHidden.current = true;
SplashScreen.hideAsync();
}, [bootHandled, pathname]);
const customDefaultTheme = {
...DefaultTheme,
@@ -162,6 +119,7 @@ function RootLayoutNav() : React.ReactNode {
<Stack.Screen name="upgrade" options={{ headerShown: false }} />
<Stack.Screen name="vault-error" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="open/[...path]" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<AliasVaultToast />
@@ -177,12 +135,6 @@ export default function RootLayout() : React.ReactNode {
SpaceMono: SpaceMono,
});
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
if (!loaded) {
return null;
}
+1 -1
View File
@@ -7,4 +7,4 @@ import { Redirect } from 'expo-router';
*/
export default function AppIndex() : React.ReactNode {
return <Redirect href={'/items'} />;
}
}
+1
View File
@@ -98,6 +98,7 @@ export default function SettingsScreen() : React.ReactNode {
}, [DEFAULT_OPTIONS]);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- loadStoredSettings is async; setState only fires after data is fetched
loadStoredSettings();
}, [loadStoredSettings]);
+23 -23
View File
@@ -1,11 +1,11 @@
import { Buffer } from 'buffer';
import { MaterialIcons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native';
import { LinearGradient } from 'expo-linear-gradient';
import { router } from 'expo-router';
import { router, useFocusEffect } from 'expo-router';
import React, { useState, useEffect, useRef } from 'react';
import { StyleSheet, View, Text, SafeAreaView, TextInput, ActivityIndicator, Animated, ScrollView, KeyboardAvoidingView, Platform, Dimensions } from 'react-native';
import { StyleSheet, View, Text, TextInput, ActivityIndicator, Animated, ScrollView, KeyboardAvoidingView, Platform, Dimensions } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useApiUrl } from '@/utils/ApiUrlUtility';
import { AppUnlockUtility } from '@/utils/AppUnlockUtility';
@@ -41,18 +41,32 @@ export default function LoginScreen() : React.ReactNode {
const { showAlert, showDialog } = useDialog();
// Animation values for entrance effects
const logoScale = useRef(new Animated.Value(0.3)).current;
const logoOpacity = useRef(new Animated.Value(0)).current;
const titleTranslateY = useRef(new Animated.Value(20)).current;
const titleOpacity = useRef(new Animated.Value(0)).current;
const formOpacity = useRef(new Animated.Value(0)).current;
const formTranslateY = useRef(new Animated.Value(30)).current;
const [logoScale] = useState(() => new Animated.Value(0.3));
const [logoOpacity] = useState(() => new Animated.Value(0));
const [titleTranslateY] = useState(() => new Animated.Value(20));
const [titleOpacity] = useState(() => new Animated.Value(0));
const [formOpacity] = useState(() => new Animated.Value(0));
const [formTranslateY] = useState(() => new Animated.Value(30));
const { loadApiUrl, getDisplayUrl } = useApiUrl();
// Track if username prefill has been attempted (only do it once on mount)
const usernamePrefillAttemptedRef = useRef(false);
const [credentials, setCredentials] = useState({
username: '',
password: '',
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
const [twoFactorCode, setTwoFactorCode] = useState('');
const [initiateLoginResponse, setInitiateLoginResponse] = useState<LoginResponse | null>(null);
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
const [passwordHashBase64, setPasswordHashBase64] = useState<string | null>(null);
const [loginStatus, setLoginStatus] = useState<string | null>(null);
const [showPassword, setShowPassword] = useState(false);
useEffect(() => {
/* Staggered entrance animations - Logo: scale up with spring + fade in */
Animated.parallel([
@@ -133,20 +147,6 @@ export default function LoginScreen() : React.ReactNode {
loadApiUrl();
});
const [credentials, setCredentials] = useState({
username: '',
password: '',
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
const [twoFactorCode, setTwoFactorCode] = useState('');
const [initiateLoginResponse, setInitiateLoginResponse] = useState<LoginResponse | null>(null);
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
const [passwordHashBase64, setPasswordHashBase64] = useState<string | null>(null);
const [loginStatus, setLoginStatus] = useState<string | null>(null);
const [showPassword, setShowPassword] = useState(false);
const authContext = useApp();
const dbContext = useDb();
const webApi = useWebApi();
+89 -125
View File
@@ -1,11 +1,28 @@
import { Href, useRouter, useLocalSearchParams, useGlobalSearchParams } from 'expo-router';
import { useEffect, useRef } from 'react';
import { StyleSheet, View } from 'react-native';
import {
ResolvedTarget,
openActionRequiresVaultUnlock,
resolveOpenAction,
} from '@/utils/DeepLinkResolver';
import LoadingIndicator from '@/components/LoadingIndicator';
import { ThemedView } from '@/components/themed/ThemedView';
import { useNavigation } from '@/context/NavigationContext';
import NativeVaultManager from '@/specs/NativeVaultManager';
// Declare __DEV__ global for TypeScript (provided by React Native runtime)
declare const __DEV__: boolean;
/**
* Build an Href from a ResolvedTarget, appending any params as a query string.
*/
function buildHref(target: ResolvedTarget): Href {
if (!target.params || Object.keys(target.params).length === 0) {
return target.path as Href;
}
const qs = new URLSearchParams(target.params).toString();
return `${target.path}?${qs}` as Href;
}
/**
* Action-based deep link handler for special actions triggered from outside the app.
@@ -16,17 +33,16 @@ declare const __DEV__: boolean;
* - mobile-unlock/[requestId] - Mobile device unlock via QR code
* - __debug__/set-offline/[true|false] - (DEV only) Toggle offline mode for E2E testing
* - __debug__/set-api-url/[encoded-url] - (DEV only) Set API URL for E2E testing
* - __debug__/set-server-revision/[number] - (DEV only) Set local server revision for RPO testing
*
* This route exists to handle deep links that Expo Router processes before our
* Linking.addEventListener can intercept them. It provides proper navigation
* flow for each action type.
* The actionpath mapping lives in `utils/DeepLinkResolver` and is shared
* with the root layout's cold-boot interceptor; this component only handles
* the warm-start dispatch (vault-unlocked check, debug side effects).
*/
export default function ActionHandler() : null {
export default function ActionHandler() : React.ReactNode {
const router = useRouter();
const params = useGlobalSearchParams();
const localParams = useLocalSearchParams();
const { setReturnUrl } = useNavigation();
const { setReturnUrl, bootHandled } = useNavigation();
const hasNavigated = useRef<boolean>(false);
useEffect(() => {
@@ -34,139 +50,87 @@ export default function ActionHandler() : null {
return;
}
// Get the path segments (first segment is the action)
/*
* Cold-boot: the root _layout already intercepts the initial URL, resolves the
* deep-link target itself, and redirects through /initialize. So when not booted
* yet, we stop executing here and trust the root layout to handle the deep link.
*/
if (!bootHandled) {
hasNavigated.current = true;
return;
}
const pathSegments = (params.path || localParams.path) as string[] | string | undefined;
const pathArray = Array.isArray(pathSegments) ? pathSegments : pathSegments ? [pathSegments] : [];
if (pathArray.length === 0) {
// No action specified, go to items
router.replace('/(tabs)/items');
hasNavigated.current = true;
return;
}
const [action, ...actionParams] = pathArray;
// Collect query/route params other than `path` itself for the resolver.
const queryParams: Record<string, string> = {};
for (const [key, value] of Object.entries(params)) {
if (key !== 'path' && typeof value === 'string') {
queryParams[key] = value;
}
}
// Handle different action types
switch (action) {
case 'mobile-unlock': {
// Mobile unlock action: $/mobile-unlock/[requestId]
const requestId = actionParams[0];
if (!requestId) {
console.error('[ActionHandler] mobile-unlock requires requestId');
router.replace('/(tabs)/settings');
hasNavigated.current = true;
return;
}
/*
* Claim the effect synchronously so a re-run can't double-dispatch while
* we're awaiting resolveOpenAction (which awaits debug side effects).
*/
hasNavigated.current = true;
(async (): Promise<void> => {
const target = await resolveOpenAction(pathArray, queryParams);
if (!target) {
console.warn('[ActionHandler] Unknown or invalid action:', pathArray.join('/'));
router.replace('/(tabs)/items');
return;
}
if (openActionRequiresVaultUnlock(pathArray)) {
/*
* Check if vault is unlocked. If the app was opened via deep link while auto-locked,
* the vault will be locked. In this case, redirect to /reinitialize first to unlock
* the vault, then forward to the mobile-unlock URL.
* Vault must be unlocked before the target screen makes sense. If the
* app was opened (e.g.) via deep link while auto-locked, detour through
* /reinitialize first and forward to the target via setReturnUrl.
*/
NativeVaultManager.isVaultUnlocked().then((isUnlocked: boolean) => {
if (hasNavigated.current) {
// Already navigated, skip
try {
const isUnlocked = await NativeVaultManager.isVaultUnlocked();
if (!isUnlocked) {
setReturnUrl(target);
router.replace('/reinitialize');
return;
}
if (!isUnlocked) {
// Set return URL to forward to mobile-unlock after reinitialize completes
setReturnUrl({
path: `/(tabs)/settings/mobile-unlock/${requestId}`,
params: params.pk ? { pk: params.pk as string } : undefined,
});
router.replace('/reinitialize');
} else {
// Vault is unlocked, navigate directly to mobile-unlock
router.replace(`/(tabs)/settings/mobile-unlock/${requestId}` as Href);
}
hasNavigated.current = true;
}).catch(() => {
// Error checking vault status, try navigating anyway
router.replace(`/(tabs)/settings/mobile-unlock/${requestId}` as Href);
hasNavigated.current = true;
});
break;
} catch {
// Error checking vault status, fall through and try navigating anyway.
}
}
/**
* ----------------------------------------------------------------------------
* Debug actions for E2E testing (only works in dev mode)
* ----------------------------------------------------------------------------
*/
case '__debug__': {
if (!__DEV__) {
console.warn('[ActionHandler] Debug actions only available in development');
router.replace('/(tabs)/items');
hasNavigated.current = true;
return;
}
router.replace(buildHref(target));
})();
}, [params, localParams, router, hasNavigated, setReturnUrl, bootHandled]);
const [debugAction, ...debugParams] = actionParams;
switch (debugAction) {
case 'set-offline': {
// Set offline mode: aliasvault://open/__debug__/set-offline/true
const isOffline = debugParams[0] === 'true';
console.debug('[ActionHandler] Setting offline mode:', isOffline);
NativeVaultManager.setOfflineMode(isOffline)
.then(() => {
console.debug('[ActionHandler] Offline mode set to:', isOffline);
})
.catch((error: Error) => {
console.error('[ActionHandler] Failed to set offline mode:', error);
});
router.replace('/(tabs)/items');
hasNavigated.current = true;
break;
}
case 'set-api-url': {
/*
* Set API URL: aliasvault://open/__debug__/set-api-url/http%3A%2F%2Flocalhost%3A5092
* Note: If slashes in URL aren't encoded, they become separate path segments
* So we join all remaining params with '/' to reconstruct the URL
*/
if (debugParams.length === 0) {
console.error('[ActionHandler] set-api-url requires URL parameter');
router.replace('/(tabs)/items');
hasNavigated.current = true;
return;
}
// Join params back together (handles both encoded and unencoded slashes)
const joinedUrl = debugParams.join('/');
const url = decodeURIComponent(joinedUrl);
console.debug('[ActionHandler] Setting API URL:', url);
NativeVaultManager.setApiUrl(url)
.then(() => {
console.debug('[ActionHandler] API URL set to:', url);
})
.catch((error: Error) => {
console.error('[ActionHandler] Failed to set API URL:', error);
});
router.replace('/(tabs)/items');
hasNavigated.current = true;
break;
}
default:
console.warn('[ActionHandler] Unknown debug action:', debugAction);
router.replace('/(tabs)/items');
hasNavigated.current = true;
break;
}
break;
}
default:
// Unknown action, log and go to items
console.warn('[ActionHandler] Unknown action:', action);
router.replace('/(tabs)/items');
hasNavigated.current = true;
break;
}
}, [params, localParams, router, hasNavigated, setReturnUrl]);
return null;
/*
* Render loading view while the navigation action is being executed.
*/
return (
<ThemedView style={styles.container}>
<View>
<LoadingIndicator />
</View>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
flex: 1,
justifyContent: 'flex-start',
paddingHorizontal: 20,
paddingTop: '40%',
},
});
+1 -6
View File
@@ -36,20 +36,15 @@ export default function UpgradeScreen() : React.ReactNode {
const [isLoading, setIsLoading] = useState(false);
const [currentVersion, setCurrentVersion] = useState<VaultVersion | null>(null);
const [latestVersion, setLatestVersion] = useState<VaultVersion | null>(null);
const [upgradeStatus, setUpgradeStatus] = useState('');
const colors = useColors();
const { t } = useTranslation();
const [upgradeStatus, setUpgradeStatus] = useState(() => t('upgrade.status.preparingUpgrade'));
const { executeVaultMutation, isLoading: isVaultMutationLoading, syncStatus } = useVaultMutate();
const { syncVault } = useVaultSync();
const { showAlert, showConfirm } = useDialog();
const [isSyncingOnLoad, setIsSyncingOnLoad] = useState(true);
// Initialize upgrade status with translation
useEffect(() => {
setUpgradeStatus(t('upgrade.status.preparingUpgrade'));
}, [t]);
/**
* Load version information from the database.
*/
@@ -4,7 +4,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { getFolderIdPath, getFolderPath } from '@/utils/folderUtils';
import { getFolderIdPath, getFolderPath } from '@/utils/FolderUtils';
import { useColors } from '@/hooks/useColorScheme';
import { useDb } from '@/context/DbContext';
@@ -7,7 +7,7 @@ import { View, TextInput, TextInputProps, StyleSheet, Platform, TouchableOpacity
import type { PasswordSettings } from '@/utils/dist/core/models/vault';
import { CreatePasswordGenerator } from '@/utils/dist/core/password-generator';
import { HapticsUtility } from '@/utils/HapticsUtility';
import { sliderToLength, lengthToSlider, SLIDER_MIN, SLIDER_MAX } from '@/utils/passwordLengthSlider';
import { sliderToLength, lengthToSlider, SLIDER_MIN, SLIDER_MAX } from '@/utils/PasswordLengthSlider';
import { useColors } from '@/hooks/useColorScheme';
@@ -1,6 +1,5 @@
import { MaterialIcons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native';
import { router } from 'expo-router';
import { router, useFocusEffect } from 'expo-router';
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { View, StyleSheet, TouchableOpacity, Linking, AppState } from 'react-native';
@@ -1,4 +1,5 @@
import { SafeAreaView, StyleProp, ViewStyle } from 'react-native';
import { StyleProp, ViewStyle } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useColors } from '@/hooks/useColorScheme';
@@ -1,7 +1,5 @@
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
/**
* Blur tab bar background component.
@@ -19,12 +17,3 @@ export default function BlurTabBarBackground(): React.ReactNode {
/>
);
}
/**
* Use bottom tab overflow.
*/
export function useBottomTabOverflow(): number {
const tabHeight = useBottomTabBarHeight();
const { bottom } = useSafeAreaInsets();
return tabHeight - bottom;
}
@@ -1,9 +1,2 @@
// This is a shim for web and Android where the tab bar is generally opaque.
export default undefined;
/**
* Use bottom tab overflow.
*/
export function useBottomTabOverflow(): number {
return 0;
}
+12 -9
View File
@@ -1,7 +1,6 @@
import { Buffer } from 'buffer';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NavigationContainerRef, ParamListBase } from '@react-navigation/native';
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
import EncryptionUtility from '@/utils/EncryptionUtility';
import type { AuthMethod } from '@/utils/AppUnlockUtility';
@@ -12,7 +11,6 @@ import NativeVaultManager from '@/specs/NativeVaultManager';
import i18n from '@/i18n';
import { LocalPreferencesService } from '@/services/LocalPreferencesService';
export const navigationRef = React.createRef<NavigationContainerRef<ParamListBase>>();
export type { AuthMethod } from '@/utils/AppUnlockUtility';
type AuthContextType = {
@@ -79,18 +77,23 @@ export const AuthProvider: React.FC<{
* @returns object containing whether the user is logged in and enabled auth methods
*/
const initializeAuth = useCallback(async (): Promise<{ isLoggedIn: boolean; enabledAuthMethods: AuthMethod[] }> => {
// Sync legacy config to native layer (can be removed in future version 0.25.0+)
// IMPORTANT: We must await this to ensure migration completes before checking auth status
await syncLegacyConfigToNative();
let accessToken = await NativeVaultManager.getAccessToken();
let username = await NativeVaultManager.getUsername();
const accessToken = await NativeVaultManager.getAccessToken();
const username = await NativeVaultManager.getUsername();
if (!accessToken || !username) {
/**
* TODO: Remove syncLegacyConfigToNative entirely in version 0.25.0+ once enough
* time has passed that all active users have migrated off AsyncStorage.
*/
await syncLegacyConfigToNative();
// Re-read in case the migration moved data into native
accessToken = await NativeVaultManager.getAccessToken();
username = await NativeVaultManager.getUsername();
}
// Update local React state
let isAuthenticated = false;
let methods: AuthMethod[] = ['password'];
// Check if user is logged in (has both access token and username)
if (accessToken && username) {
setUsername(username);
setIsLoggedIn(true);
+21 -1
View File
@@ -21,6 +21,19 @@ type NavigationContextType = {
* Handles return URLs and default navigation to credentials tab.
*/
navigateAfterUnlock: () => void;
/**
* Whether the root layout has finished its cold-boot interception. Routes
* that handle deep links (e.g. /open/[...path]) must wait for this to be
* true before acting, otherwise they race the boot detour to /initialize.
*/
bootHandled: boolean;
/**
* Mark cold-boot interception as complete. Called by the root layout once
* the initial URL has been inspected and any redirect dispatched.
*/
markBootHandled: () => void;
}
const NavigationContext = createContext<NavigationContextType | undefined>(undefined);
@@ -33,6 +46,7 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
const pathname = usePathname();
const params = useGlobalSearchParams();
const [returnUrl, setReturnUrl] = useState<{ path: string; params?: Record<string, string> } | null>(null);
const [bootHandled, setBootHandled] = useState(false);
const appState = useRef(AppState.currentState);
const lastRouteRef = useRef<{ path: string, params?: object }>({ path: pathname, params });
@@ -202,11 +216,17 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
};
}, [isVaultUnlocked, pathname, router]);
const markBootHandled = useCallback((): void => {
setBootHandled(true);
}, []);
const contextValue = useMemo(() => ({
returnUrl,
setReturnUrl,
navigateAfterUnlock,
}), [returnUrl, navigateAfterUnlock]);
bootHandled,
markBootHandled,
}), [returnUrl, navigateAfterUnlock, bootHandled, markBootHandled]);
return (
<NavigationContext.Provider value={contextValue}>
+3 -2
View File
@@ -1,3 +1,4 @@
import type { TFunction } from 'i18next';
import { useTranslation as useReactI18nextTranslation } from 'react-i18next';
/**
@@ -5,11 +6,11 @@ import { useTranslation as useReactI18nextTranslation } from 'react-i18next';
* @returns Translation utilities
*/
export const useTranslation = (): {
t: (key: string, options?: object) => string;
t: TFunction;
currentLanguage: string;
} => {
const { t, i18n } = useReactI18nextTranslation();
return {
t,
currentLanguage: i18n.language,
@@ -272,7 +272,7 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */ = {
CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
@@ -282,84 +282,13 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
CE59C7602E4F47FD0024A246 /* AliasVaultUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = AliasVaultUITests;
sourceTree = "<group>";
};
CE77825E2EA1822400A75E6F /* VaultUtils */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = VaultUtils;
sourceTree = "<group>";
};
CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = VaultStoreKit;
sourceTree = "<group>";
};
CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = VaultStoreKitTests;
sourceTree = "<group>";
};
CEE4816B2DBE8AC800F4A367 /* VaultUI */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = VaultUI;
sourceTree = "<group>";
};
CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = VaultModels;
sourceTree = "<group>";
};
CEE909812DA548C7008D568F /* Autofill */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = Autofill;
sourceTree = "<group>";
};
CE59C7602E4F47FD0024A246 /* AliasVaultUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AliasVaultUITests; sourceTree = "<group>"; };
CE77825E2EA1822400A75E6F /* VaultUtils */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUtils; sourceTree = "<group>"; };
CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKit; sourceTree = "<group>"; };
CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKitTests; sourceTree = "<group>"; };
CEE4816B2DBE8AC800F4A367 /* VaultUI */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUI; sourceTree = "<group>"; };
CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultModels; sourceTree = "<group>"; };
CEE909812DA548C7008D568F /* Autofill */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Autofill; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -519,7 +448,7 @@
path = Generated;
sourceTree = "<group>";
};
6114AD0A31612EA499210371 /* */ = {
6114AD0A31612EA499210371 = {
isa = PBXGroup;
children = (
8F8258CE46969A87661E0302 /* RustCore.swift */,
@@ -555,7 +484,7 @@
832341AE1AAA6A7D00B99B32 /* Libraries */,
D65327D7A22EEC0BE12398D9 /* Pods */,
83CBBA001A601CBA00E9B192 /* Products */,
6114AD0A31612EA499210371 /* */,
6114AD0A31612EA499210371,
CE5212B62F0C061800F4C835 /* Recovered References */,
3AD00C41DD88E219A26C2E41 /* RustCoreFramework */,
);
@@ -1114,13 +1043,10 @@
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -1128,13 +1054,10 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -1165,6 +1088,7 @@
};
7C20B1F4BE2D9D5DB4B311E8 /* Build Rust Core */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -1234,11 +1158,16 @@
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)/.xcode.env",
"$(SRCROOT)/.xcode.env.local",
"$(SRCROOT)/AliasVault/AliasVault.entitlements",
"$(SRCROOT)/Pods/Target Support Files/Pods-AliasVault/expo-configure-project.sh",
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
"$(SRCROOT)/Pods/Target Support Files/Pods-AliasVault/ExpoModulesProvider.swift",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -1326,13 +1255,19 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-AliasVault/Pods-AliasVault-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoModulesJSI/ExpoModulesJSI.framework/ExpoModulesJSI",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ExpoModulesJSI.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -1536,7 +1471,7 @@
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = RustCoreFramework/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1712,13 +1647,19 @@
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = (
OTHER_CFLAGS = (
"$(inherited)",
" ",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_CPLUSPLUSFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
};
name = Debug;
@@ -1769,12 +1710,18 @@
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_LDFLAGS = (
OTHER_CFLAGS = (
"$(inherited)",
" ",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_CPLUSPLUSFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;
};
@@ -1796,7 +1743,7 @@
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = RustCoreFramework/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -2,14 +2,15 @@ import UIKit
import React
import React_RCTAppDelegate
import ReactAppDependencyProvider
internal import Expo
import VaultStoreKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var reactNativeDelegate: ReactNativeDelegate?
var reactNativeFactory: RCTReactNativeFactory?
private var reactNativeDelegate: ReactNativeDelegate?
private var reactNativeFactory: ExpoReactNativeFactory?
func application(
_ application: UIApplication,
@@ -28,7 +29,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
#endif
let delegate = ReactNativeDelegate()
let factory = RCTReactNativeFactory(delegate: delegate)
let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
reactNativeDelegate = delegate
@@ -137,7 +138,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
private class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
override func sourceURL(for bridge: RCTBridge) -> URL? {
self.bundleURL()
}
+2 -1
View File
@@ -7,7 +7,7 @@ podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties
ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
platform :ios, podfile_properties['ios.deploymentTarget'] || '17.0'
install! 'cocoapods',
:deterministic_uuids => false
@@ -19,6 +19,7 @@ target 'AliasVault' do
pod 'SwiftLint', :configurations => ['Debug']
pod 'Macaw', '~> 0.9.7'
pod 'SignalArgon2', '~> 1.3.2'
pod 'ExpoModulesJSI', :path => '../node_modules/expo-modules-jsi/apple'
pod 'ExpoModulesCore', :path => '../node_modules/expo-modules-core'
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,5 +1,6 @@
{
"expo.jsEngine": "hermes",
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
"newArchEnabled": "true"
"newArchEnabled": "true",
"ios.deploymentTarget": "17.0"
}
@@ -18,5 +18,7 @@
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>RCTNewArchEnabled</key>
<true/>
</dict>
</plist>
+4151 -4720
View File
File diff suppressed because it is too large Load Diff
+41 -39
View File
@@ -24,80 +24,82 @@
}
},
"dependencies": {
"@expo/vector-icons": "^14.1.0",
"@expo/vector-icons": "^15.0.2",
"@hookform/resolvers": "^5.0.1",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-native-community/slider": "4.5.6",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/slider": "5.2.0",
"@types/jsrsasign": "^10.5.15",
"expo": "^53.0.22",
"expo-blur": "~14.1.5",
"expo-clipboard": "~7.1.5",
"expo-constants": "~17.1.7",
"expo-document-picker": "~13.1.6",
"expo-file-system": "~18.1.11",
"expo-font": "~13.3.2",
"expo-haptics": "~14.1.4",
"expo-linear-gradient": "~14.1.5",
"expo-linking": "~7.1.7",
"expo-local-authentication": "~16.0.5",
"expo-localization": "~16.1.6",
"expo-router": "~5.1.6",
"expo-sharing": "~13.1.5",
"expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5",
"expo-web-browser": "~14.2.0",
"expo": "^56.0.4",
"expo-blur": "~56.0.3",
"expo-clipboard": "~56.0.3",
"expo-constants": "~56.0.15",
"expo-document-picker": "~56.0.4",
"expo-file-system": "~56.0.7",
"expo-font": "~56.0.5",
"expo-haptics": "~56.0.3",
"expo-intent-launcher": "~56.0.4",
"expo-linear-gradient": "~56.0.4",
"expo-linking": "~56.0.11",
"expo-local-authentication": "~56.0.4",
"expo-localization": "~56.0.6",
"expo-router": "~56.2.6",
"expo-sharing": "~56.0.13",
"expo-splash-screen": "~56.0.10",
"expo-status-bar": "~56.0.4",
"expo-symbols": "~56.0.5",
"expo-web-browser": "~56.0.5",
"fbemitter": "^3.0.0",
"i18next": "^25.3.2",
"i18next": "^26.0.0",
"lodash": "^4.18.1",
"react": "19.0.0",
"react": "19.2.3",
"react-hook-form": "^7.56.1",
"react-i18next": "^15.6.0",
"react-native": "~0.79.6",
"react-i18next": "^16.6.6",
"react-native": "0.85.3",
"react-native-aes-gcm-crypto": "^0.2.2",
"react-native-context-menu-view": "^1.19.0",
"react-native-draggable-flatlist": "^4.0.3",
"react-native-edge-to-edge": "1.6.0",
"react-native-gesture-handler": "~2.24.0",
"react-native-gesture-handler": "~2.31.1",
"react-native-get-random-values": "^1.11.0",
"react-native-image-zoom-viewer": "^3.0.1",
"react-native-qrcode-svg": "^6.3.21",
"react-native-quick-crypto": "^0.7.13",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.6.1",
"react-native-screens": "~4.15.4",
"react-native-svg": "^15.15.1",
"react-native-reanimated": "4.3.1",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "4.25.2",
"react-native-svg": "15.15.4",
"react-native-svg-transformer": "^1.5.0",
"react-native-toast-message": "^2.2.1",
"react-native-webview": "13.13.5",
"react-native-webview": "13.16.1",
"react-native-worklets": "0.8.3",
"sanitize-html": "^2.17.0",
"yup": "^1.6.1",
"expo-intent-launcher": "~12.1.5"
"yup": "^1.6.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/core": "^7.29.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.35.0",
"@react-native-community/cli": "^20.1.2",
"@stylistic/eslint-plugin": "^5.3.1",
"@types/fbemitter": "^2.0.35",
"@types/lodash": "^4.17.16",
"@types/react": "~19.0.10",
"@types/react": "~19.2.14",
"@types/react-test-renderer": "^18.3.0",
"@types/sanitize-html": "^2.16.0",
"@types/sql.js": "^1.4.9",
"@types/yup": "^0.29.14",
"eslint": "^9.35.0",
"eslint-config-expo": "~9.2.0",
"eslint-config-expo": "~56.0.4",
"eslint-plugin-jsdoc": "^55.2.0",
"eslint-plugin-react-native": "^5.0.0",
"globals": "^16.3.0",
"typescript": "~5.8.3"
"typescript": "~6.0.3"
},
"engines": {
"node": ">=20"
},
"overrides": {
"react-dom": "19.2.3"
},
"private": true
}
+145
View File
@@ -0,0 +1,145 @@
import NativeVaultManager from '@/specs/NativeVaultManager';
declare const __DEV__: boolean;
export type ResolvedTarget = {
path: string;
params?: Record<string, string>;
};
/**
* Resolve an `open/<action>/...` URL into a final navigation target.
*
* Returns null when the action is unrecognised or is missing required params.
* For `__debug__/...` actions the navigation target is always /(tabs)/items
* and the associated native side effect is awaited internally before the
* function resolves E2E callers can deep-link into a debug action and
* trust that the resulting state (e.g. offline mode) is applied by the time
* navigation runs.
*
* Used by both the cold-boot interceptor (root layout) and warm-start
* ActionHandler so the actionpath mapping lives in one place.
*/
export async function resolveOpenAction(
actionSegments: string[],
queryParams: Record<string, string>,
): Promise<ResolvedTarget | null> {
const [action, ...rest] = actionSegments;
switch (action) {
case 'mobile-unlock': {
const requestId = rest[0];
if (!requestId) {
return null;
}
const params: Record<string, string> = {};
if (queryParams.pk) {
params.pk = queryParams.pk;
}
return {
path: `/(tabs)/settings/mobile-unlock/${requestId}`,
params: Object.keys(params).length > 0 ? params : undefined,
};
}
case '__debug__':
if (!__DEV__) {
return null;
}
try {
await executeDebugSideEffect(rest);
} catch (err) {
console.error('[DeepLinkResolver] debug side effect failed:', err);
}
return { path: '/(tabs)/items' };
default:
return null;
}
}
/**
* Whether an `open/<action>` requires the vault to be unlocked before the
* resolved target makes sense. Warm-start ActionHandler uses this to decide
* between navigating directly and detouring through /reinitialize.
*/
export function openActionRequiresVaultUnlock(actionSegments: string[]): boolean {
return actionSegments[0] === 'mobile-unlock';
}
/**
* Parse a deep-link URL and resolve into a post-unlock navigation target.
*
* - `aliasvault://open/<action>/...` `resolveOpenAction`
* - any other path (e.g. `aliasvault://items/abc`) that route directly
*
* Used by the root layout's cold-boot interceptor. The caller is responsible
* for routing through /initialize before navigating to the returned target.
* Returns null for invalid, empty, or unrecognised URLs.
*/
export async function resolveDeepLink(url: string | null): Promise<ResolvedTarget | null> {
if (!url) {
return null;
}
let rawPath = '';
const queryParams: Record<string, string> = {};
try {
const parsed = new URL(url);
rawPath = `${parsed.host}${parsed.pathname}`.replace(/^\/+/, '');
parsed.searchParams.forEach((value, key) => {
queryParams[key] = value;
});
} catch {
return null;
}
if (!rawPath) {
return null;
}
const segments = rawPath.split('/').filter(Boolean);
if (segments[0] === 'open') {
return resolveOpenAction(segments.slice(1), queryParams);
}
return {
path: rawPath,
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
};
}
/**
* Execute the side effect for a `__debug__/<debugAction>` URL.
*
* `debugSegments` is the path segments AFTER `__debug__/`, e.g. for
* `__debug__/set-offline/true` this receives `['set-offline', 'true']`.
*/
async function executeDebugSideEffect(debugSegments: string[]): Promise<void> {
if (!__DEV__) {
return;
}
const [debugAction, ...debugParams] = debugSegments;
switch (debugAction) {
case 'set-offline': {
const isOffline = debugParams[0] === 'true';
console.debug('[DeepLinkResolver] Setting offline mode:', isOffline);
await NativeVaultManager.setOfflineMode(isOffline);
return;
}
case 'set-api-url': {
if (debugParams.length === 0) {
console.error('[DeepLinkResolver] set-api-url requires URL parameter');
return;
}
// Join all remaining segments so unencoded slashes in the URL still work.
const url = decodeURIComponent(debugParams.join('/'));
console.debug('[DeepLinkResolver] Setting API URL:', url);
await NativeVaultManager.setApiUrl(url);
return;
}
default:
console.warn('[DeepLinkResolver] Unknown debug action:', debugAction);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import * as dateFormatter from '@/utils/dateFormatter';
import * as dateFormatter from '@/utils/DateFormatter';
import NativeVaultManager from '@/specs/NativeVaultManager';