mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-20 15:41:40 -04:00
Add separate index page that calls init logic (#771)
This commit is contained in:
@@ -9,12 +9,14 @@ import { useDb } from '@/context/DbContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
import { CredentialIcon } from '@/components/CredentialIcon';
|
||||
import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
|
||||
export default function CredentialsScreen() {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDarkMode = colorScheme === 'dark';
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const { syncVault } = useVaultSync();
|
||||
|
||||
const dynamicStyles = {
|
||||
credentialItem: {
|
||||
@@ -68,8 +70,16 @@ export default function CredentialsScreen() {
|
||||
// Record start time
|
||||
const startTime = Date.now();
|
||||
|
||||
// Load data
|
||||
const credentials = await dbContext.sqliteClient!.getAllCredentials();
|
||||
// Sync vault and load credentials
|
||||
await syncVault({
|
||||
forceCheck: true,
|
||||
onSuccess: async () => {
|
||||
await loadCredentials();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error syncing vault:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate remaining time needed to reach 350ms minimum
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
@@ -79,15 +89,12 @@ export default function CredentialsScreen() {
|
||||
if (remainingDelay > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, remainingDelay));
|
||||
}
|
||||
|
||||
// Update the data
|
||||
setCredentialsList(credentials);
|
||||
setRefreshing(false);
|
||||
} catch (err) {
|
||||
console.error('Error refreshing credentials:', err);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
}, [syncVault, loadCredentials]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !isDatabaseAvailable) {
|
||||
|
||||
@@ -3,14 +3,13 @@ import { useFonts } from 'expo-font';
|
||||
import { Stack } from 'expo-router';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { AppState, AppStateStatus } from 'react-native';
|
||||
import { useEffect } from 'react';
|
||||
import { AppState, AppStateStatus, View, ActivityIndicator, Text } from 'react-native';
|
||||
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 Toast from 'react-native-toast-message';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { LoadingProvider } from '@/context/LoadingContext';
|
||||
@@ -19,9 +18,7 @@ import { AuthProvider } from '@/context/AuthContext';
|
||||
import { WebApiProvider } from '@/context/WebApiContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
|
||||
import { View, ActivityIndicator, Text } from 'react-native';
|
||||
import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
@@ -55,134 +52,13 @@ const toastConfig = {
|
||||
|
||||
function RootLayoutNav() {
|
||||
const colorScheme = useColorScheme();
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const isFirstMount = useRef(true);
|
||||
|
||||
// Check if user is authenticated and database is available
|
||||
const isFullyInitialized = authContext.isInitialized && dbContext.dbInitialized;
|
||||
const isAuthenticated = authContext.isLoggedIn;
|
||||
const isDatabaseAvailable = dbContext.dbAvailable;
|
||||
const requireLoginOrUnlock = isFullyInitialized && (!isAuthenticated || !isDatabaseAvailable);
|
||||
|
||||
/**
|
||||
* Check for vault updates and fetch if necessary
|
||||
*/
|
||||
const checkVaultUpdates = useCallback(async (isStartup: boolean = false) => {
|
||||
const isLoggedIn = await authContext.initializeAuth();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
console.log('Start check vault update: Not authenticated');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: add check to know if the encryption key is available in the keychain
|
||||
// If not, we should redirect to the unlock screen.
|
||||
// Or do this check in the "isLoggedIn" check above?
|
||||
const passwordHashBase64 = await AsyncStorage.getItem('passwordHash');
|
||||
if (!passwordHashBase64) {
|
||||
//console.log('Vault check error: Password hash not found');
|
||||
//await webApi.logout('Password hash not found');
|
||||
//rreturn;
|
||||
}
|
||||
|
||||
console.log('Start check vault updates');
|
||||
|
||||
try {
|
||||
// On startup, always check. Otherwise, check the time elapsed
|
||||
if (!isStartup) {
|
||||
const lastCheckStr = await AsyncStorage.getItem('lastVaultCheck');
|
||||
const lastCheck = lastCheckStr ? parseInt(lastCheckStr, 10) : 0;
|
||||
const now = Date.now();
|
||||
|
||||
// Only check if more than 1 hour has passed since last check
|
||||
if (now - lastCheck < 3600000) {
|
||||
console.log('Vault check skipped: Not enough time has passed since last check');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Checking vault updates');
|
||||
|
||||
// Update last check time
|
||||
await AsyncStorage.setItem('lastVaultCheck', Date.now().toString());
|
||||
|
||||
// Do status check to ensure this mobile app is supported and check vault revision
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
console.log('Vault check error:', statusError);
|
||||
await webApi.logout(statusError);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the vault revision is higher, fetch the latest vault
|
||||
const vaultMetadata = await dbContext.getVaultMetadata();
|
||||
const vaultRevisionNumber = vaultMetadata?.vaultRevisionNumber ?? 0;
|
||||
console.log('Vault revision:', statusResponse.vaultRevision);
|
||||
console.log('Current vault revision:', vaultRevisionNumber);
|
||||
|
||||
if (statusResponse.vaultRevision > vaultRevisionNumber) {
|
||||
const vaultResponseJson = await webApi.get<VaultResponse>('Vault');
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
|
||||
if (vaultError) {
|
||||
console.log('Vault check error:', vaultError);
|
||||
console.log('Vault response:', vaultResponseJson);
|
||||
await webApi.logout(vaultError);
|
||||
return;
|
||||
}
|
||||
// Initialize the SQLite context again with the newly retrieved decrypted blob
|
||||
// TODO: set encryption key in initializedatabase as separate method to make
|
||||
// it more clean.
|
||||
console.log('Re-initializing database with new vault');
|
||||
await dbContext.initializeDatabase(vaultResponseJson, null);
|
||||
}
|
||||
else {
|
||||
console.log('Vault check finished: Vault revision is the same, no action requiredr');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Vault check error:', err);
|
||||
}
|
||||
}, [isAuthenticated, dbContext, webApi]);
|
||||
|
||||
// Handle app state changes and initial mount
|
||||
useEffect(() => {
|
||||
// On first mount (cold boot), always check
|
||||
if (isFirstMount.current) {
|
||||
isFirstMount.current = false;
|
||||
checkVaultUpdates(true);
|
||||
}
|
||||
|
||||
const subscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
|
||||
if (nextAppState === 'active') {
|
||||
checkVaultUpdates(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, [checkVaultUpdates]);
|
||||
|
||||
// Show loading screen while initializing
|
||||
if (!isFullyInitialized) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
{requireLoginOrUnlock ? (
|
||||
<Stack.Screen name="login" />
|
||||
) : (
|
||||
<Stack.Screen name="(tabs)" />
|
||||
)}
|
||||
<Stack screenOptions={{ headerShown: false, animation: 'none' }}>
|
||||
<Stack.Screen name="index" />
|
||||
<Stack.Screen name="login" />
|
||||
<Stack.Screen name="(tabs)" />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
|
||||
48
mobile-app/app/index.tsx
Normal file
48
mobile-app/app/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { View, ActivityIndicator } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
|
||||
export default function InitialLoadingScreen() {
|
||||
const { isInitialized: isAuthInitialized, isLoggedIn } = useAuth();
|
||||
const { dbInitialized, dbAvailable } = useDb();
|
||||
const { syncVault } = useVaultSync();
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
const isFullyInitialized = isAuthInitialized && dbInitialized;
|
||||
const requireLoginOrUnlock = isFullyInitialized && (!isLoggedIn || !dbAvailable);
|
||||
|
||||
useEffect(() => {
|
||||
async function initialize() {
|
||||
if (hasInitialized.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasInitialized.current = true;
|
||||
|
||||
// Perform initial vault sync
|
||||
console.log('Initial vault sync');
|
||||
await syncVault({ forceCheck: true });
|
||||
|
||||
// Navigate to appropriate screen
|
||||
if (requireLoginOrUnlock) {
|
||||
console.log('Navigating to login');
|
||||
router.replace('/login');
|
||||
} else {
|
||||
console.log('Navigating to credentials');
|
||||
router.replace('/(tabs)/(credentials)');
|
||||
}
|
||||
}
|
||||
|
||||
initialize();
|
||||
}, [isFullyInitialized, requireLoginOrUnlock, syncVault]); // Keep all dependencies to satisfy ESLint
|
||||
|
||||
return (
|
||||
<ThemedView style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<ActivityIndicator size="large" color="#f97316" />
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
92
mobile-app/hooks/useVaultSync.ts
Normal file
92
mobile-app/hooks/useVaultSync.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useCallback } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
|
||||
|
||||
interface VaultSyncOptions {
|
||||
forceCheck?: boolean;
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: string) => void;
|
||||
}
|
||||
|
||||
export const useVaultSync = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
|
||||
const syncVault = useCallback(async (options: VaultSyncOptions = {}) => {
|
||||
const { forceCheck = false, onSuccess, onError } = options;
|
||||
console.log('syncVault called with forceCheck:', forceCheck);
|
||||
|
||||
try {
|
||||
const isLoggedIn = await authContext.initializeAuth();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
console.log('Vault sync: Not authenticated');
|
||||
return false;
|
||||
}
|
||||
|
||||
// If not forcing a check, verify the time elapsed since last check
|
||||
if (!forceCheck) {
|
||||
const lastCheckStr = await AsyncStorage.getItem('lastVaultCheck');
|
||||
const lastCheck = lastCheckStr ? parseInt(lastCheckStr, 10) : 0;
|
||||
const now = Date.now();
|
||||
|
||||
// Only check if more than 1 hour has passed since last check
|
||||
if (now - lastCheck < 3600000) {
|
||||
console.log('Vault sync skipped: Not enough time has passed since last check');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Checking vault updates');
|
||||
|
||||
// Update last check time
|
||||
await AsyncStorage.setItem('lastVaultCheck', Date.now().toString());
|
||||
|
||||
// Check app status and vault revision
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
console.log('Vault sync error:', statusError);
|
||||
await webApi.logout(statusError);
|
||||
onError?.(statusError);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare vault revisions
|
||||
const vaultMetadata = await dbContext.getVaultMetadata();
|
||||
const vaultRevisionNumber = vaultMetadata?.vaultRevisionNumber ?? 0;
|
||||
|
||||
if (statusResponse.vaultRevision > vaultRevisionNumber) {
|
||||
const vaultResponseJson = await webApi.get<VaultResponse>('Vault');
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson as VaultResponse);
|
||||
if (vaultError) {
|
||||
console.log('Vault sync error:', vaultError);
|
||||
await webApi.logout(vaultError);
|
||||
onError?.(vaultError);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('Re-initializing database with new vault');
|
||||
await dbContext.initializeDatabase(vaultResponseJson as VaultResponse, null);
|
||||
onSuccess?.();
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('Vault sync finished: No updates needed');
|
||||
onSuccess?.();
|
||||
return false;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error during vault sync';
|
||||
console.error('Vault sync error:', err);
|
||||
onError?.(errorMessage);
|
||||
return false;
|
||||
}
|
||||
}, [authContext, dbContext, webApi]);
|
||||
|
||||
return { syncVault };
|
||||
};
|
||||
Reference in New Issue
Block a user