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:
co-authored by
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Leendert de Borst
parent
999b465467
commit
423330c864
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export default [
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: ".",
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
|
||||
Generated
+1550
-421
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,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]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ import { Redirect } from 'expo-router';
|
||||
*/
|
||||
export default function AppIndex() : React.ReactNode {
|
||||
return <Redirect href={'/items'} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 action→path 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%',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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}>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
+1216
-972
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
Generated
+4151
-4720
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 action→path 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,4 +1,4 @@
|
||||
import * as dateFormatter from '@/utils/dateFormatter';
|
||||
import * as dateFormatter from '@/utils/DateFormatter';
|
||||
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user