mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-09 07:46:13 -04:00
Add vault-error.tsx page and scaffolding (#1926)
This commit is contained in:
committed by
Leendert de Borst
parent
172613fab3
commit
346dc03fb5
@@ -338,14 +338,14 @@ export default function ItemsScreen(): React.ReactNode {
|
||||
setIsLoadingItems(false);
|
||||
} catch (err) {
|
||||
console.error('Error loading items:', err);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: t('items.errorLoadingItems'),
|
||||
text2: t('common.errors.unknownError'),
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
const errorStack = err instanceof Error ? err.stack ?? '' : '';
|
||||
router.replace({
|
||||
pathname: '/vault-error',
|
||||
params: { errorMessage, errorStack, errorSource: 'items' },
|
||||
});
|
||||
setIsLoadingItems(false);
|
||||
}
|
||||
}, [dbContext.sqliteClient, setIsLoadingItems, t]);
|
||||
}, [dbContext.sqliteClient, setIsLoadingItems, router]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeFocus = navigation.addListener('focus', () => {
|
||||
|
||||
@@ -145,6 +145,7 @@ function RootLayoutNav() : React.ReactNode {
|
||||
<Stack.Screen name="reinitialize" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="unlock" options={{ headerShown: false }} />
|
||||
<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="+not-found" />
|
||||
</Stack>
|
||||
|
||||
@@ -50,107 +50,122 @@ export default function Initialize() : React.ReactNode {
|
||||
* Initialize the app.
|
||||
*/
|
||||
const initialize = async () : Promise<void> => {
|
||||
const { isLoggedIn, enabledAuthMethods } = await app.initializeAuth();
|
||||
try {
|
||||
const { isLoggedIn, enabledAuthMethods } = await app.initializeAuth();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we already have an unlocked vault, we can skip the biometric unlock
|
||||
* but still need to perform vault sync to check for updates.
|
||||
*/
|
||||
const isAlreadyUnlocked = await NativeVaultManager.isVaultUnlocked();
|
||||
|
||||
if (!isAlreadyUnlocked) {
|
||||
// Check if we have an encrypted database and if FaceID is enabled
|
||||
try {
|
||||
const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
|
||||
|
||||
if (hasEncryptedDatabase) {
|
||||
// Attempt automatic unlock using centralized helper
|
||||
updateStatus(t('app.status.unlockingVault'));
|
||||
const unlockResult = await VaultUnlockHelper.attemptAutomaticUnlock({ enabledAuthMethods, unlockVault: dbContext.unlockVault });
|
||||
|
||||
if (!unlockResult.success) {
|
||||
/*
|
||||
* Unlock failed or cancelled, redirect to unlock screen.
|
||||
* Only log non-cancellation errors to avoid noise.
|
||||
*/
|
||||
if (!unlockResult.error?.includes('cancelled')) {
|
||||
console.error('Automatic unlock failed:', unlockResult.error);
|
||||
}
|
||||
router.replace('/unlock');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the vault needs migration before syncing
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error during initial vault unlock:', err);
|
||||
router.replace('/unlock');
|
||||
if (!isLoggedIn) {
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
||||
/**
|
||||
* Vault already unlocked (e.g., from password unlock)
|
||||
* Check if migrations are needed.
|
||||
* If we already have an unlocked vault, we can skip the biometric unlock
|
||||
* but still need to perform vault sync to check for updates.
|
||||
*/
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
const isAlreadyUnlocked = await NativeVaultManager.isVaultUnlocked();
|
||||
|
||||
if (!isAlreadyUnlocked) {
|
||||
// Check if we have an encrypted database and if FaceID is enabled
|
||||
try {
|
||||
const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
|
||||
|
||||
if (hasEncryptedDatabase) {
|
||||
// Attempt automatic unlock using centralized helper
|
||||
updateStatus(t('app.status.unlockingVault'));
|
||||
const unlockResult = await VaultUnlockHelper.attemptAutomaticUnlock({ enabledAuthMethods, unlockVault: dbContext.unlockVault });
|
||||
|
||||
if (!unlockResult.success) {
|
||||
/*
|
||||
* Unlock failed or cancelled, redirect to unlock screen.
|
||||
* Only log non-cancellation errors to avoid noise.
|
||||
*/
|
||||
if (!unlockResult.error?.includes('cancelled')) {
|
||||
console.error('Automatic unlock failed:', unlockResult.error);
|
||||
}
|
||||
router.replace('/unlock');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the vault needs migration before syncing
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error during initial vault unlock:', err);
|
||||
router.replace('/unlock');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* Vault already unlocked (e.g., from password unlock)
|
||||
* Check if migrations are needed.
|
||||
*/
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Perform vault sync in background - don't block app access.
|
||||
* The ServerSyncIndicator will show sync progress/offline status.
|
||||
* This also handles uploading pending local changes (isDirty) from previous sessions.
|
||||
*/
|
||||
void (async (): Promise<void> => {
|
||||
try {
|
||||
await syncVault({
|
||||
/**
|
||||
* Handle successful vault sync.
|
||||
*/
|
||||
onSuccess: async () => {
|
||||
// Sync completed - ServerSyncIndicator will update
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* Handle offline state - just set offline mode and continue.
|
||||
* The ServerSyncIndicator will show offline status.
|
||||
*/
|
||||
onOffline: async () => {
|
||||
await dbContext.setIsOffline(true);
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* Handle error during vault sync.
|
||||
*/
|
||||
onError: async (error: string) => {
|
||||
console.error('Vault sync error during initialize:', error);
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* On upgrade required.
|
||||
*/
|
||||
onUpgradeRequired: () : void => {
|
||||
router.replace('/upgrade');
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await dbContext.refreshSyncState();
|
||||
}
|
||||
})();
|
||||
|
||||
// Navigate immediately - don't wait for sync
|
||||
navigation.navigateAfterUnlock();
|
||||
} catch (err) {
|
||||
/*
|
||||
* Catch any unhandled errors during vault initialization to prevent hard crashes.
|
||||
* Redirect to vault error screen where users can view and copy the error details
|
||||
* for remote debugging purposes.
|
||||
*/
|
||||
console.error('Fatal error during vault initialization:', err);
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
const errorStack = err instanceof Error ? err.stack ?? '' : '';
|
||||
router.replace({
|
||||
pathname: '/vault-error',
|
||||
params: { errorMessage, errorStack, errorSource: 'initialize' },
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Perform vault sync in background - don't block app access.
|
||||
* The ServerSyncIndicator will show sync progress/offline status.
|
||||
* This also handles uploading pending local changes (isDirty) from previous sessions.
|
||||
*/
|
||||
void (async (): Promise<void> => {
|
||||
try {
|
||||
await syncVault({
|
||||
/**
|
||||
* Handle successful vault sync.
|
||||
*/
|
||||
onSuccess: async () => {
|
||||
// Sync completed - ServerSyncIndicator will update
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* Handle offline state - just set offline mode and continue.
|
||||
* The ServerSyncIndicator will show offline status.
|
||||
*/
|
||||
onOffline: async () => {
|
||||
await dbContext.setIsOffline(true);
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* Handle error during vault sync.
|
||||
*/
|
||||
onError: async (error: string) => {
|
||||
console.error('Vault sync error during initialize:', error);
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* On upgrade required.
|
||||
*/
|
||||
onUpgradeRequired: () : void => {
|
||||
router.replace('/upgrade');
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await dbContext.refreshSyncState();
|
||||
}
|
||||
})();
|
||||
|
||||
// Navigate immediately - don't wait for sync
|
||||
navigation.navigateAfterUnlock();
|
||||
};
|
||||
|
||||
initialize();
|
||||
|
||||
@@ -48,115 +48,130 @@ export default function ReinitializeScreen() : React.ReactNode {
|
||||
* Initialize the app.
|
||||
*/
|
||||
const initialize = async () : Promise<void> => {
|
||||
const { isLoggedIn, enabledAuthMethods } = await app.initializeAuth();
|
||||
try {
|
||||
const { isLoggedIn, enabledAuthMethods } = await app.initializeAuth();
|
||||
|
||||
// If user is not logged in, navigate to login immediately
|
||||
if (!isLoggedIn) {
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
// If user is not logged in, navigate to login immediately
|
||||
if (!isLoggedIn) {
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we already have an unlocked vault, we can skip the biometric unlock
|
||||
* but still need to perform vault sync to check for updates.
|
||||
*/
|
||||
const isAlreadyUnlocked = await NativeVaultManager.isVaultUnlocked();
|
||||
/**
|
||||
* If we already have an unlocked vault, we can skip the biometric unlock
|
||||
* but still need to perform vault sync to check for updates.
|
||||
*/
|
||||
const isAlreadyUnlocked = await NativeVaultManager.isVaultUnlocked();
|
||||
|
||||
if (!isAlreadyUnlocked) {
|
||||
// Check if we have an encrypted database
|
||||
try {
|
||||
const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
|
||||
if (!isAlreadyUnlocked) {
|
||||
// Check if we have an encrypted database
|
||||
try {
|
||||
const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
|
||||
|
||||
if (hasEncryptedDatabase) {
|
||||
// Attempt automatic unlock using centralized helper
|
||||
updateStatus(t('app.status.unlockingVault'));
|
||||
// Small delay to ensure loading screen is fully rendered before showing native unlock dialog
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
const unlockResult = await VaultUnlockHelper.attemptAutomaticUnlock({ enabledAuthMethods, unlockVault: dbContext.unlockVault });
|
||||
if (hasEncryptedDatabase) {
|
||||
// Attempt automatic unlock using centralized helper
|
||||
updateStatus(t('app.status.unlockingVault'));
|
||||
// Small delay to ensure loading screen is fully rendered before showing native unlock dialog
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
const unlockResult = await VaultUnlockHelper.attemptAutomaticUnlock({ enabledAuthMethods, unlockVault: dbContext.unlockVault });
|
||||
|
||||
if (!unlockResult.success) {
|
||||
// Unlock failed, redirect to unlock screen
|
||||
console.error('Automatic unlock failed:', unlockResult.error);
|
||||
if (!unlockResult.success) {
|
||||
// Unlock failed, redirect to unlock screen
|
||||
console.error('Automatic unlock failed:', unlockResult.error);
|
||||
router.replace('/unlock');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add small delay for UX
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
updateStatus(t('app.status.decryptingVault'));
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
// Check if the vault needs migration before syncing
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// No encrypted database, redirect to unlock screen
|
||||
router.replace('/unlock');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add small delay for UX
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
updateStatus(t('app.status.decryptingVault'));
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
// Check if the vault needs migration before syncing
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// No encrypted database, redirect to unlock screen
|
||||
} catch (err) {
|
||||
console.error('Error during initial vault unlock:', err);
|
||||
router.replace('/unlock');
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error during initial vault unlock:', err);
|
||||
router.replace('/unlock');
|
||||
return;
|
||||
} else {
|
||||
/**
|
||||
* Vault already unlocked (e.g., from password unlock)
|
||||
* Check if migrations are needed.
|
||||
*/
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* Vault already unlocked (e.g., from password unlock)
|
||||
* Check if migrations are needed.
|
||||
|
||||
/*
|
||||
* Perform vault sync in background - don't block app access.
|
||||
* The ServerSyncIndicator will show sync progress/offline status.
|
||||
* This also handles uploading pending local changes (isDirty) from previous sessions.
|
||||
*/
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
}
|
||||
void (async (): Promise<void> => {
|
||||
try {
|
||||
await syncVault({
|
||||
/**
|
||||
* Handle successful vault sync.
|
||||
*/
|
||||
onSuccess: async () => {
|
||||
// Sync completed - ServerSyncIndicator will update
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* Handle error during vault sync.
|
||||
* Authentication errors are already handled in useVaultSync.
|
||||
*/
|
||||
onError: async (error: string) => {
|
||||
console.error('Vault sync error during reinitialize:', error);
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* Handle offline state - just set offline mode and continue.
|
||||
* The ServerSyncIndicator will show offline status.
|
||||
*/
|
||||
onOffline: async () => {
|
||||
await dbContext.setIsOffline(true);
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* On upgrade required.
|
||||
*/
|
||||
onUpgradeRequired: () : void => {
|
||||
router.replace('/upgrade');
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await dbContext.refreshSyncState();
|
||||
}
|
||||
})();
|
||||
|
||||
// Navigate immediately
|
||||
navigation.navigateAfterUnlock();
|
||||
} catch (err) {
|
||||
/*
|
||||
* Catch any unhandled errors during vault re-initialization to prevent hard crashes.
|
||||
* Redirect to vault error screen where users can view and copy the error details
|
||||
* for remote debugging purposes.
|
||||
*/
|
||||
console.error('Fatal error during vault re-initialization:', err);
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
const errorStack = err instanceof Error ? err.stack ?? '' : '';
|
||||
router.replace({
|
||||
pathname: '/vault-error',
|
||||
params: { errorMessage, errorStack, errorSource: 'reinitialize' },
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Perform vault sync in background - don't block app access.
|
||||
* The ServerSyncIndicator will show sync progress/offline status.
|
||||
* This also handles uploading pending local changes (isDirty) from previous sessions.
|
||||
*/
|
||||
void (async (): Promise<void> => {
|
||||
try {
|
||||
await syncVault({
|
||||
/**
|
||||
* Handle successful vault sync.
|
||||
*/
|
||||
onSuccess: async () => {
|
||||
// Sync completed - ServerSyncIndicator will update
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* Handle error during vault sync.
|
||||
* Authentication errors are already handled in useVaultSync.
|
||||
*/
|
||||
onError: async (error: string) => {
|
||||
console.error('Vault sync error during reinitialize:', error);
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* Handle offline state - just set offline mode and continue.
|
||||
* The ServerSyncIndicator will show offline status.
|
||||
*/
|
||||
onOffline: async () => {
|
||||
await dbContext.setIsOffline(true);
|
||||
await dbContext.refreshSyncState();
|
||||
},
|
||||
/**
|
||||
* On upgrade required.
|
||||
*/
|
||||
onUpgradeRequired: () : void => {
|
||||
router.replace('/upgrade');
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await dbContext.refreshSyncState();
|
||||
}
|
||||
})();
|
||||
|
||||
// Navigate immediately
|
||||
navigation.navigateAfterUnlock();
|
||||
};
|
||||
|
||||
initialize();
|
||||
|
||||
286
apps/mobile-app/app/vault-error.tsx
Normal file
286
apps/mobile-app/app/vault-error.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { StyleSheet, View, ScrollView, Dimensions, Text, Platform } from 'react-native';
|
||||
|
||||
import { copyToClipboard } from '@/utils/ClipboardUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useLogout } from '@/hooks/useLogout';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
import Logo from '@/assets/images/logo.svg';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
|
||||
/**
|
||||
* Vault error screen displayed when the app encounters an unrecoverable error
|
||||
* during vault initialization or bootstrap.
|
||||
*/
|
||||
export default function VaultErrorScreen() : React.ReactNode {
|
||||
const { errorMessage, errorStack, errorSource } = useLocalSearchParams<{
|
||||
errorMessage: string;
|
||||
errorStack: string;
|
||||
errorSource: string;
|
||||
}>();
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const { logoutUserInitiated } = useLogout();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
/**
|
||||
* Copy the full error details to clipboard for sharing with support.
|
||||
*/
|
||||
const handleCopyError = useCallback(async (): Promise<void> => {
|
||||
const errorReport = [
|
||||
`Source: ${errorSource ?? 'unknown'}`,
|
||||
`Platform: ${Platform.OS} ${Platform.Version}`,
|
||||
`Error: ${errorMessage ?? 'Unknown error'}`,
|
||||
'',
|
||||
'Stack trace:',
|
||||
errorStack ?? 'No stack trace available',
|
||||
].join('\n');
|
||||
|
||||
await copyToClipboard(errorReport);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}, [errorMessage, errorStack, errorSource]);
|
||||
|
||||
/**
|
||||
* Retry by navigating back to the initialize screen.
|
||||
*/
|
||||
const handleRetry = useCallback((): void => {
|
||||
router.replace('/initialize');
|
||||
}, []);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
appName: {
|
||||
color: colors.text,
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
button: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
justifyContent: 'center',
|
||||
marginBottom: 12,
|
||||
minHeight: 50,
|
||||
paddingVertical: 8,
|
||||
width: '100%',
|
||||
},
|
||||
buttonText: {
|
||||
color: colors.primarySurfaceText,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 4,
|
||||
textAlign: 'center',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 10,
|
||||
padding: 20,
|
||||
width: '100%',
|
||||
},
|
||||
copyButton: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderColor: colors.accentBorder,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginTop: 8,
|
||||
minHeight: 40,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
copyButtonText: {
|
||||
color: colors.primary,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginLeft: 6,
|
||||
},
|
||||
detailsContainer: {
|
||||
backgroundColor: colors.background,
|
||||
borderColor: colors.accentBorder,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
marginTop: 12,
|
||||
maxHeight: 250,
|
||||
padding: 12,
|
||||
},
|
||||
detailsText: {
|
||||
color: colors.textMuted,
|
||||
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
||||
fontSize: 11,
|
||||
},
|
||||
errorIcon: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
errorMessageText: {
|
||||
color: colors.errorText,
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
},
|
||||
gradientContainer: {
|
||||
height: Dimensions.get('window').height * 0.4,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
},
|
||||
headerSection: {
|
||||
paddingBottom: 24,
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 24,
|
||||
},
|
||||
logoContainer: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
logoutButton: {
|
||||
alignSelf: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
logoutButtonText: {
|
||||
color: colors.red,
|
||||
fontSize: 16,
|
||||
},
|
||||
mainContent: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingBottom: 40,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
showDetailsButton: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginTop: 12,
|
||||
},
|
||||
showDetailsText: {
|
||||
color: colors.primary,
|
||||
fontSize: 14,
|
||||
marginLeft: 4,
|
||||
},
|
||||
subtitle: {
|
||||
color: colors.text,
|
||||
fontSize: 14,
|
||||
marginBottom: 16,
|
||||
opacity: 0.7,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container} testID="vault-error-screen">
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.loginHeader, colors.background]}
|
||||
style={styles.gradientContainer}
|
||||
/>
|
||||
<View style={styles.mainContent}>
|
||||
<View style={styles.headerSection}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Logo width={80} height={80} />
|
||||
<Text style={styles.appName}>{t('app.vaultError.title')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.errorIcon}>
|
||||
<MaterialIcons name="error-outline" size={48} color={colors.red} />
|
||||
</View>
|
||||
|
||||
<ThemedText style={styles.subtitle}>
|
||||
{t('app.vaultError.description')}
|
||||
</ThemedText>
|
||||
|
||||
<ThemedText style={styles.errorMessageText}>
|
||||
{errorMessage ?? t('common.errors.unknownError')}
|
||||
</ThemedText>
|
||||
|
||||
{/* Show/hide details toggle */}
|
||||
<RobustPressable
|
||||
style={styles.showDetailsButton}
|
||||
onPress={() => setShowDetails(!showDetails)}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={showDetails ? 'expand-less' : 'expand-more'}
|
||||
size={20}
|
||||
color={colors.primary}
|
||||
/>
|
||||
<ThemedText style={styles.showDetailsText}>
|
||||
{showDetails ? t('app.vaultError.hideDetails') : t('app.vaultError.showDetails')}
|
||||
</ThemedText>
|
||||
</RobustPressable>
|
||||
|
||||
{/* Stack trace details */}
|
||||
{showDetails && (
|
||||
<View>
|
||||
<ScrollView style={styles.detailsContainer} nestedScrollEnabled>
|
||||
<Text style={styles.detailsText} selectable>
|
||||
{`Source: ${errorSource ?? 'unknown'}\nPlatform: ${Platform.OS} ${Platform.Version}\n\n${errorStack ?? 'No stack trace available'}`}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
<RobustPressable
|
||||
style={styles.copyButton}
|
||||
onPress={handleCopyError}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={copied ? 'check' : 'content-copy'}
|
||||
size={16}
|
||||
color={colors.primary}
|
||||
/>
|
||||
<ThemedText style={styles.copyButtonText}>
|
||||
{copied ? t('common.copied') : t('app.vaultError.copyErrorDetails')}
|
||||
</ThemedText>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<View style={{ marginTop: 20 }}>
|
||||
<RobustPressable
|
||||
style={styles.button}
|
||||
onPress={handleRetry}
|
||||
testID="retry-button"
|
||||
>
|
||||
<ThemedText style={styles.buttonText}>
|
||||
{t('common.retry')}
|
||||
</ThemedText>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
|
||||
<RobustPressable
|
||||
style={styles.logoutButton}
|
||||
onPress={logoutUserInitiated}
|
||||
testID="logout-button"
|
||||
>
|
||||
<ThemedText style={styles.logoutButtonText}>
|
||||
{t('auth.logout')}
|
||||
</ThemedText>
|
||||
</RobustPressable>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
"generate": "Generate",
|
||||
"attachments": "Attachments",
|
||||
"or": "or",
|
||||
"retry": "Retry",
|
||||
"deleteItemConfirmTitle": "Delete Item",
|
||||
"deleteItemConfirmDescription": "Are you sure you want to delete this item?",
|
||||
"errors": {
|
||||
@@ -618,6 +619,13 @@
|
||||
"vaultAutoLockedMessage": "Vault auto-locked after timeout.",
|
||||
"attemptingToUnlockMessage": "Attempting to unlock."
|
||||
},
|
||||
"vaultError": {
|
||||
"title": "Vault Error",
|
||||
"description": "An error occurred while loading your vault. You can retry or copy the error details to share with support.",
|
||||
"showDetails": "Show error details",
|
||||
"hideDetails": "Hide error details",
|
||||
"copyErrorDetails": "Copy error details"
|
||||
},
|
||||
"loginSettings": {
|
||||
"title": "API Connection",
|
||||
"aliasvaultNet": "Aliasvault.net",
|
||||
|
||||
@@ -250,8 +250,12 @@ class SqliteClient implements IDatabaseClient {
|
||||
const results = await NativeVaultManager.executeQuery(query, convertedParams);
|
||||
return results as T[];
|
||||
} catch (error) {
|
||||
console.error('Error executing query:', error);
|
||||
throw error;
|
||||
const originalMessage = error instanceof Error ? error.message : String(error);
|
||||
const queryPreview = query.trim().substring(0, 200);
|
||||
const enrichedError = new Error(originalMessage);
|
||||
enrichedError.stack = `SQL: ${queryPreview}\n\n${error instanceof Error && error.stack ? error.stack : ''}`;
|
||||
console.error('Error executing query:', enrichedError.message);
|
||||
throw enrichedError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,8 +282,12 @@ class SqliteClient implements IDatabaseClient {
|
||||
const result = await NativeVaultManager.executeUpdate(query, convertedParams);
|
||||
return result as number;
|
||||
} catch (error) {
|
||||
console.error('Error executing update:', error);
|
||||
throw error;
|
||||
const originalMessage = error instanceof Error ? error.message : String(error);
|
||||
const queryPreview = query.trim().substring(0, 200);
|
||||
const enrichedError = new Error(originalMessage);
|
||||
enrichedError.stack = `SQL: ${queryPreview}\n\n${error instanceof Error && error.stack ? error.stack : ''}`;
|
||||
console.error('Error executing update:', enrichedError.message);
|
||||
throw enrichedError;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user