mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-19 13:57:18 -04:00
Refactor initial sync to happen outside of navigation context (#771)
This commit is contained in:
@@ -23,17 +23,11 @@ export default function AutofillCredentialCreatedScreen() : React.ReactNode {
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
// Handle app state changes to auto-dismiss when app comes back from background
|
||||
// Handle app state changes to auto-dismiss when app goes to background
|
||||
useEffect(() => {
|
||||
let wasInBackground = false;
|
||||
|
||||
const subscription = AppState.addEventListener('change', (nextAppState) => {
|
||||
if (nextAppState === 'background') {
|
||||
wasInBackground = true;
|
||||
} else if (nextAppState === 'active' && wasInBackground) {
|
||||
// App is returning from background, dismiss the screen
|
||||
router.back();
|
||||
wasInBackground = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { Linking, StyleSheet } from 'react-native';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
import { useFonts } from 'expo-font';
|
||||
import { Stack } from 'expo-router';
|
||||
import { Href, Stack, useRouter } from 'expo-router';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import 'react-native-reanimated';
|
||||
/*
|
||||
* Required for certain modules such as secure-remote-password which relies on crypto.getRandomValues
|
||||
* and this is not available in react-native without this polyfill
|
||||
*/
|
||||
import 'react-native-get-random-values';
|
||||
import { install } from 'react-native-quick-crypto';
|
||||
|
||||
import { useColors, useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { DbProvider } from '@/context/DbContext';
|
||||
import { AuthProvider } from '@/context/AuthContext';
|
||||
import { DbProvider, useDb } from '@/context/DbContext';
|
||||
import { AuthProvider, useAuth } from '@/context/AuthContext';
|
||||
import { WebApiProvider } from '@/context/WebApiContext';
|
||||
import { AliasVaultToast } from '@/components/Toast';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
import SpaceMono from '@/assets/fonts/SpaceMono-Regular.ttf';
|
||||
import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
/**
|
||||
@@ -28,8 +28,131 @@ SplashScreen.preventAutoHideAsync();
|
||||
function RootLayoutNav() : React.ReactNode {
|
||||
const colorScheme = useColorScheme();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const [status, setStatus] = useState('');
|
||||
const { initializeAuth } = useAuth();
|
||||
const { syncVault } = useVaultSync();
|
||||
const dbContext = useDb();
|
||||
|
||||
const [bootComplete, setBootComplete] = useState(false);
|
||||
const [redirectTarget, setRedirectTarget] = useState<string | null>(null);
|
||||
const hasBooted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasBooted.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Install the react-native-quick-crypto library which is used by the EncryptionUtility
|
||||
install();
|
||||
|
||||
hasBooted.current = true;
|
||||
|
||||
/**
|
||||
* Initialize the app.
|
||||
*/
|
||||
const initialize = async () : Promise<void> => {
|
||||
const { isLoggedIn, enabledAuthMethods } = await initializeAuth();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
setRedirectTarget('/login');
|
||||
setBootComplete(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform initial vault sync
|
||||
await syncVault({
|
||||
initialSync: true,
|
||||
/**
|
||||
* Handle the status update.
|
||||
*/
|
||||
onStatus: (message) => {
|
||||
setStatus(message);
|
||||
}
|
||||
});
|
||||
|
||||
const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
|
||||
if (hasEncryptedDatabase) {
|
||||
const isFaceIDEnabled = enabledAuthMethods.includes('faceid');
|
||||
if (!isFaceIDEnabled) {
|
||||
setRedirectTarget('/unlock');
|
||||
setBootComplete(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('Unlocking vault');
|
||||
const isUnlocked = await dbContext.unlockVault();
|
||||
if (isUnlocked) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setStatus('Decrypting vault');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// The vault is successfully unlocked, so we let the native code handle the default routing.
|
||||
} else {
|
||||
setRedirectTarget('/unlock');
|
||||
}
|
||||
} else {
|
||||
setRedirectTarget('/unlock');
|
||||
}
|
||||
|
||||
setBootComplete(true);
|
||||
};
|
||||
|
||||
initialize();
|
||||
}, [dbContext, syncVault, initializeAuth]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Simulate stack navigation.
|
||||
*/
|
||||
function simulateStackNavigation(from: string, to: string) : void {
|
||||
router.replace(from as Href);
|
||||
setTimeout(() => {
|
||||
router.push(to as Href);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the target if we have one, otherwise check for a deep link and simulate stack navigation.
|
||||
*/
|
||||
const redirect = async () : Promise<void> => {
|
||||
if (bootComplete && redirectTarget) {
|
||||
// If we have an explicit redirect target, we navigate to it. This overrides potential deep link handling.
|
||||
router.replace(redirectTarget as Href);
|
||||
} else if (bootComplete) {
|
||||
const initialUrl = await Linking.getInitialURL();
|
||||
if (initialUrl) {
|
||||
/**
|
||||
* Check for certain deep link routes, and if found, ensure we simulate the stack navigation
|
||||
* as otherwise the "back" button for navigation will not work as expected.
|
||||
*/
|
||||
const path = initialUrl.replace('net.aliasvault.app://', '');
|
||||
const isDetailRoute = path.includes('credentials/');
|
||||
if (isDetailRoute) {
|
||||
simulateStackNavigation('/(tabs)/credentials', path);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
redirect();
|
||||
}, [bootComplete, redirectTarget, router]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
if (!bootComplete) {
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
{status ? <LoadingIndicator status={status} /> : null}
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
// Create custom themes that extend the default ones.
|
||||
const customDefaultTheme = {
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
@@ -50,22 +173,24 @@ function RootLayoutNav() : React.ReactNode {
|
||||
|
||||
return (
|
||||
<ThemeProvider value={colorScheme === 'dark' ? customDarkTheme : customDefaultTheme}>
|
||||
<Stack screenOptions={{
|
||||
headerShown: true,
|
||||
animation: 'none',
|
||||
headerTransparent: true,
|
||||
headerStyle: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
},
|
||||
headerTintColor: colors.primary,
|
||||
headerTitleStyle: {
|
||||
color: colors.text,
|
||||
},
|
||||
}}>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: true,
|
||||
animation: 'none',
|
||||
headerTransparent: true,
|
||||
headerStyle: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
},
|
||||
headerTintColor: colors.primary,
|
||||
headerTitleStyle: {
|
||||
color: colors.text,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="login" options={{ title: 'Login', headerShown: false }} />
|
||||
<Stack.Screen name="login-settings" options={{ title: 'Login Settings' }} />
|
||||
<Stack.Screen name="sync" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="reinitialize" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="unlock" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" options={{ title: 'Not Found' }} />
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { Redirect } from 'expo-router';
|
||||
import { install } from 'react-native-quick-crypto';
|
||||
|
||||
/**
|
||||
* App index which is the entry point of the app and redirects to the sync screen, which will
|
||||
* redirect to the login screen if the user is not logged in or to the main tabs screen if the user is logged in.
|
||||
*/
|
||||
export default function AppIndex() : React.ReactNode {
|
||||
// Install the react-native-quick-crypto library which is used by the EncryptionUtility
|
||||
install();
|
||||
|
||||
return <Redirect href={'/sync'} />
|
||||
return <Redirect href={'/credentials'} />
|
||||
}
|
||||
@@ -1,25 +1,27 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { router } from 'expo-router';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import NativeVaultManager from '../specs/NativeVaultManager';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
/**
|
||||
* Sync screen which will redirect to the login screen if the user is not logged in
|
||||
* or to the credentials screen if the user is logged in.
|
||||
* Reinitialize screen which is triggered when the app was still open but the database in memory
|
||||
* was cleared because of a time-out. When this happens, we need to re-initialize and unlock the vault.
|
||||
*/
|
||||
export default function SyncScreen() : React.ReactNode {
|
||||
export default function ReinitializeScreen() : React.ReactNode {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const { syncVault } = useVaultSync();
|
||||
const [status, setStatus] = useState('');
|
||||
const hasInitialized = useRef(false);
|
||||
const colors = useColors();
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInitialized.current) {
|
||||
@@ -103,17 +105,28 @@ export default function SyncScreen() : React.ReactNode {
|
||||
initialize();
|
||||
}, [syncVault, authContext, dbContext]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
message: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
messageContainer: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 10,
|
||||
padding: 20,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
{status ? <LoadingIndicator status={status} /> : null}
|
||||
<View style={styles.messageContainer}>
|
||||
<ThemedText style={styles.message}>Vault locked due to time-out, unlocking vault again...</ThemedText>
|
||||
{status ? <LoadingIndicator status={status} /> : null}
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
@@ -98,7 +98,6 @@ export default function LoadingIndicator({ status }: LoadingIndicatorProps): Rea
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
|
||||
@@ -257,14 +257,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
// Check if vault is unlocked.
|
||||
const isUnlocked = await isVaultUnlocked();
|
||||
if (!isUnlocked) {
|
||||
// Database connection failed, navigate to unlock flow
|
||||
router.replace('/sync');
|
||||
} else {
|
||||
// Vault is still unlocked, staying on current screen
|
||||
// Database connection failed, navigate to reinitialize flow
|
||||
router.replace('/reinitialize');
|
||||
}
|
||||
} catch {
|
||||
// Database query failed, navigate to unlock flow
|
||||
router.replace('/sync');
|
||||
// Database query failed, navigate to reinitialize flow
|
||||
router.replace('/reinitialize');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user