diff --git a/mobile-app/app/index.tsx b/mobile-app/app/index.tsx
index 82c43ba10..80fdfeae9 100644
--- a/mobile-app/app/index.tsx
+++ b/mobile-app/app/index.tsx
@@ -6,14 +6,20 @@ import { useVaultSync } from '@/hooks/useVaultSync';
import { ThemedView } from '@/components/ThemedView';
import LoadingIndicator from '@/components/LoadingIndicator';
import { install } from 'react-native-quick-crypto';
+import { NativeModules } from 'react-native';
export default function InitialLoadingScreen() {
const { isInitialized: isAuthInitialized, isLoggedIn } = useAuth();
const { dbInitialized, dbAvailable } = useDb();
const { syncVault } = useVaultSync();
- const hasInitialized = useRef(false);
+ const authContext = useAuth();
const [status, setStatus] = useState('');
+ const hasInitialized = useRef(false);
+ // TODO: isauthinitialized and dbinitialize do not have to be checekd anymore separately.
+ // Refactor this and check usages of isAuthInitialized and dbInitialized in other places.
+ // Doing the check in this file once should be enough. Especially after adding the server
+ // status check later which should fire periodically..
const isFullyInitialized = isAuthInitialized && dbInitialized;
const requireLoginOrUnlock = isFullyInitialized && (!isLoggedIn || !dbAvailable);
@@ -22,13 +28,22 @@ export default function InitialLoadingScreen() {
install();
useEffect(() => {
+ if (hasInitialized.current) {
+ return;
+ }
+
+ hasInitialized.current = true;
+
async function initialize() {
- if (hasInitialized.current) {
+ const isLoggedIn = await authContext.initializeAuth();
+
+ // If user is not logged in, navigate to login immediately
+ if (!isLoggedIn) {
+ console.log('User not logged in, navigating to login');
+ router.replace('/login');
return;
}
- hasInitialized.current = true;
-
// Perform initial vault sync
console.log('Initial vault sync');
await syncVault({
@@ -38,22 +53,56 @@ export default function InitialLoadingScreen() {
}
});
- // Navigate to appropriate screen
- if (requireLoginOrUnlock) {
- console.log('Navigating to login');
- router.replace('/login');
- } else {
- console.log('Navigating to credentials');
- router.replace('/(tabs)/(credentials)');
+ // Try to unlock with FaceID
+ try {
+ // This will check if the encrypted database file exists locally.
+ // TODO: this isvaultinitialized should be renamed to "isVaultExists" or something similar
+ // as we're just checking if the file exists.
+ const isInitialized = await NativeModules.CredentialManager.isVaultInitialized();
+ if (isInitialized) {
+ // Attempt to unlock the vault with FaceID.
+ setStatus('Unlocking vault|');
+ const isUnlocked = await NativeModules.CredentialManager.unlockVault();
+ if (isUnlocked) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ setStatus('Decrypting vault');
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ console.log('FaceID unlock successful, navigating to credentials');
+ router.replace('/(tabs)/(credentials)');
+ return;
+ }
+ else {
+ console.log('FaceID unlock failed, navigating to unlock screen');
+ router.replace('/unlock');
+ }
+ }
+ else {
+ // Vault is not initialized which means the database does not exist or decryption key is missing
+ // from device's keychain. Navigate to the unlock screen.
+ console.log('Vault is not initialized (db file does not exist), navigating to unlock screen');
+ router.replace('/unlock');
+ return;
+ }
+ } catch (error) {
+ console.log('FaceID unlock failed:', error);
+ // If FaceID fails (too many attempts, manual cancel, etc.)
+ // navigate to unlock screen
+ router.replace('/unlock');
+ return;
}
+
+ // If we get here, something went wrong with the FaceID unlock
+ // Navigate to unlock screen as a fallback
+ console.log('FaceID unlock failed, navigating to unlock screen');
+ router.replace('/unlock');
}
initialize();
- }, [isFullyInitialized, requireLoginOrUnlock, syncVault]); // Keep all dependencies to satisfy ESLint
+ }, [isFullyInitialized, requireLoginOrUnlock, syncVault]);
return (
- {isLoggedIn && status ? : null}
+ {(isLoggedIn && status) ? : null}
);
}
\ No newline at end of file
diff --git a/mobile-app/app/unlock.tsx b/mobile-app/app/unlock.tsx
new file mode 100644
index 000000000..f8707a38e
--- /dev/null
+++ b/mobile-app/app/unlock.tsx
@@ -0,0 +1,155 @@
+import { useState } from 'react';
+import { StyleSheet, View, TextInput, TouchableOpacity, Alert } from 'react-native';
+import { router } from 'expo-router';
+import { useAuth } from '@/context/AuthContext';
+import { useDb } from '@/context/DbContext';
+import { ThemedView } from '@/components/ThemedView';
+import { ThemedText } from '@/components/ThemedText';
+import LoadingIndicator from '@/components/LoadingIndicator';
+import { NativeModules } from 'react-native';
+import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
+
+export default function UnlockScreen() {
+ const { isLoggedIn } = useAuth();
+ const { initializeDatabase } = useDb();
+ const [password, setPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleUnlock = async () => {
+ if (!password) {
+ Alert.alert('Error', 'Please enter your password');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ // Initialize the database with the provided password
+ // We need to get the vault response from the API first
+ const response = await fetch('https://api.aliasvault.net/v1/vault', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch vault');
+ }
+
+ const vaultResponse: VaultResponse = await response.json();
+
+ // Initialize the database with the vault response and password
+ await initializeDatabase(vaultResponse, password);
+
+ // If successful, navigate to credentials
+ router.replace('/(tabs)/(credentials)');
+ } catch (error) {
+ Alert.alert('Error', 'Incorrect password. Please try again.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleLogout = async () => {
+ // Clear any stored tokens or session data
+ // This will be handled by the auth context
+ router.replace('/login');
+ };
+
+ return (
+
+
+ Unlock Vault
+ Enter your password to unlock your vault
+
+
+
+
+
+ {isLoading ? 'Unlocking...' : 'Unlock'}
+
+
+
+
+ Logout
+
+
+
+ {isLoading && }
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ content: {
+ width: '100%',
+ maxWidth: 400,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ textAlign: 'center',
+ },
+ subtitle: {
+ fontSize: 16,
+ marginBottom: 24,
+ textAlign: 'center',
+ opacity: 0.7,
+ },
+ input: {
+ width: '100%',
+ height: 50,
+ borderWidth: 1,
+ borderColor: '#ccc',
+ borderRadius: 8,
+ paddingHorizontal: 16,
+ marginBottom: 16,
+ fontSize: 16,
+ },
+ button: {
+ width: '100%',
+ height: 50,
+ backgroundColor: '#007AFF',
+ borderRadius: 8,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ buttonText: {
+ color: 'white',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ logoutButton: {
+ width: '100%',
+ height: 50,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ logoutButtonText: {
+ color: '#007AFF',
+ fontSize: 16,
+ },
+});
\ No newline at end of file
diff --git a/mobile-app/components/LoadingIndicator.tsx b/mobile-app/components/LoadingIndicator.tsx
index 03b39f653..ae5fe17e9 100644
--- a/mobile-app/components/LoadingIndicator.tsx
+++ b/mobile-app/components/LoadingIndicator.tsx
@@ -48,6 +48,9 @@ export default function LoadingIndicator({ status }: LoadingIndicatorProps) {
])
);
+ // Reset dots when status changes
+ setDots('');
+
const dotsInterval = setInterval(() => {
setDots(prevDots => {
if (prevDots.length >= 3) return '';
@@ -61,7 +64,12 @@ export default function LoadingIndicator({ status }: LoadingIndicatorProps) {
animation.stop();
clearInterval(dotsInterval);
};
- }, []);
+ }, [status]);
+
+ // If the status ends with a pipe character (|), don't show any dots
+ // This provides an explicit way to disable the loading dots animation
+ const statusTrimmed = status.endsWith('|') ? status.slice(0, -1) : status;
+ const shouldShowDots = !status.endsWith('|');
const styles = StyleSheet.create({
container: {
@@ -150,7 +158,10 @@ export default function LoadingIndicator({ status }: LoadingIndicatorProps) {
]}
/>
- {status}{dots}
+
+ {statusTrimmed}
+ {shouldShowDots && dots}
+
);
}
\ No newline at end of file
diff --git a/mobile-app/context/DbContext.tsx b/mobile-app/context/DbContext.tsx
index 9af63efab..f99eac843 100644
--- a/mobile-app/context/DbContext.tsx
+++ b/mobile-app/context/DbContext.tsx
@@ -66,13 +66,16 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
// Get metadata from SQLite client
const metadata = await sqliteClient.getVaultMetadata();
if (metadata) {
+ console.log('Vault metadata found, setting dbInitialized and dbAvailable to true');
setDbInitialized(true);
setDbAvailable(true);
} else {
+ console.log('Vault metadata not found, setting dbInitialized and dbAvailable to false');
setDbInitialized(true);
setDbAvailable(false);
}
} else {
+ console.log('Vault not initialized, setting dbInitialized and dbAvailable to false');
setDbInitialized(true);
setDbAvailable(false);
}
diff --git a/mobile-app/hooks/useVaultSync.ts b/mobile-app/hooks/useVaultSync.ts
index 67687fc30..236cbdacf 100644
--- a/mobile-app/hooks/useVaultSync.ts
+++ b/mobile-app/hooks/useVaultSync.ts
@@ -59,7 +59,7 @@ export const useVaultSync = () => {
onStatus?.('Checking vault updates');
const statusResponse = await withMinimumDelay(
() => webApi.getStatus(),
- 700,
+ 300,
initialSync
);
const statusError = webApi.validateStatusResponse(statusResponse);
@@ -99,9 +99,11 @@ export const useVaultSync = () => {
}
console.log('Vault sync finished: No updates needed');
- onStatus?.('Decrypting vault');
- await new Promise(resolve => setTimeout(resolve, 300));
- onSuccess?.(false);
+ await withMinimumDelay(
+ () => Promise.resolve(onSuccess?.(false)),
+ 300,
+ initialSync
+ );
return false;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error during vault sync';
diff --git a/mobile-app/ios/CredentialManager/CredentialManager.m b/mobile-app/ios/CredentialManager/CredentialManager.m
index 5a2ed6433..5b59012f3 100644
--- a/mobile-app/ios/CredentialManager/CredentialManager.m
+++ b/mobile-app/ios/CredentialManager/CredentialManager.m
@@ -6,8 +6,11 @@ RCT_EXTERN_METHOD(requiresMainQueueSetup)
RCT_EXTERN_METHOD(addCredential:(NSString *)username password:(NSString *)password service:(NSString *)service)
RCT_EXTERN_METHOD(getCredentials)
RCT_EXTERN_METHOD(clearVault)
+// TODO: isvaultinitialized should be renamed to "isVaultExists" or something similar as we're just checking if the file exists.
+// The initializevault actually initializes the vault by decrypting it and loading the DB into memory.
RCT_EXTERN_METHOD(isVaultInitialized:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(getVaultMetadata:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+RCT_EXTERN_METHOD(unlockVault:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
// New methods for SQLite database operations
RCT_EXTERN_METHOD(storeDatabase:(NSString *)base64EncryptedDb
diff --git a/mobile-app/ios/CredentialManager/CredentialManager.swift b/mobile-app/ios/CredentialManager/CredentialManager.swift
index f7fb152c2..bc50c76d2 100644
--- a/mobile-app/ios/CredentialManager/CredentialManager.swift
+++ b/mobile-app/ios/CredentialManager/CredentialManager.swift
@@ -148,6 +148,30 @@ class CredentialManager: NSObject {
}
}
+ @objc
+ func unlockVault(_ resolve: @escaping RCTPromiseResolveBlock,
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
+ do {
+ // TODO: rename this to unlockvault? so meaning is: initialized means it exists, and unlock means
+ // we're decrypting the encrypted database (that exists) and try to load it into memory. If unlocking
+ // fails, user is redirected to the unlock screen in the react native app.
+ try credentialStore.initializeDatabase()
+ resolve(true)
+ } catch {
+ // Check if the error is related to Face ID or decryption
+ if let nsError = error as NSError? {
+ if nsError.domain == "SharedCredentialStore" {
+ // These are our known error codes for initialization failures
+ if nsError.code == 1 || nsError.code == 2 || nsError.code == 10 {
+ resolve(false)
+ return
+ }
+ }
+ }
+ reject("INIT_ERROR", "Failed to unlock vault: \(error.localizedDescription)", error)
+ }
+ }
+
@objc
func requiresMainQueueSetup() -> Bool {
return false
diff --git a/mobile-app/ios/CredentialManager/SharedCredentialStore.swift b/mobile-app/ios/CredentialManager/SharedCredentialStore.swift
index 7a303ca78..4cad24629 100644
--- a/mobile-app/ios/CredentialManager/SharedCredentialStore.swift
+++ b/mobile-app/ios/CredentialManager/SharedCredentialStore.swift
@@ -93,7 +93,7 @@ class SharedCredentialStore {
}
// Get the encrypted database as a base64 encoded string
- func getEncryptedDatabase() -> String? {
+ private func getEncryptedDatabase() -> String? {
do {
// Return the base64 encoded string
return try String(contentsOf: getEncryptedDbPath(), encoding: .utf8)
@@ -103,7 +103,7 @@ class SharedCredentialStore {
}
// Get the vault metadata from UserDefaults
- func getVaultMetadata() -> String? {
+ public func getVaultMetadata() -> String? {
return UserDefaults.standard.string(forKey: "vault_metadata")
}