diff --git a/apps/mobile-app/app/initialize.tsx b/apps/mobile-app/app/initialize.tsx index d2cbb43eb..2627608c9 100644 --- a/apps/mobile-app/app/initialize.tsx +++ b/apps/mobile-app/app/initialize.tsx @@ -24,6 +24,7 @@ export default function Initialize() : React.ReactNode { const skipButtonTimeoutRef = useRef(null); const lastStatusRef = useRef(''); const canShowSkipButtonRef = useRef(false); // Only allow skip button after vault unlock + const abortControllerRef = useRef(null); const { t } = useTranslation(); const app = useApp(); const { syncVault } = useVaultSync(); @@ -68,6 +69,13 @@ export default function Initialize() : React.ReactNode { * Handle offline scenario - show alert with options to open local vault or retry sync. */ const handleOfflineFlow = useCallback((): void => { + // Don't show the alert if we're already in offline mode + if (app.isOffline) { + console.debug('Already in offline mode, skipping offline flow alert'); + router.replace('/(tabs)/credentials'); + return; + } + Alert.alert( t('app.alerts.syncIssue'), t('app.alerts.syncIssueMessage'), @@ -136,6 +144,12 @@ export default function Initialize() : React.ReactNode { updateStatus(t('app.status.retryingConnection')); setShowSkipButton(false); + // Abort any pending sync operation + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + // Clear any existing timeout if (skipButtonTimeoutRef.current) { clearTimeout(skipButtonTimeoutRef.current); @@ -241,9 +255,13 @@ export default function Initialize() : React.ReactNode { canShowSkipButtonRef.current = true; } + // Create abort controller for sync operations + abortControllerRef.current = new AbortController(); + // Now perform vault sync (network operations - these are skippable) await syncVault({ initialSync: true, + abortSignal: abortControllerRef.current.signal, /** * Handle the status update. */ @@ -301,6 +319,13 @@ export default function Initialize() : React.ReactNode { * Handle skip button press by calling the offline handler. */ const handleSkipPress = (): void => { + // Abort any pending sync operation + if (abortControllerRef.current) { + console.debug('Aborting pending sync operation'); + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + // Clear any existing timeout if (skipButtonTimeoutRef.current) { clearTimeout(skipButtonTimeoutRef.current); diff --git a/apps/mobile-app/hooks/useVaultSync.ts b/apps/mobile-app/hooks/useVaultSync.ts index 6f3eb078d..aeffe2fc4 100644 --- a/apps/mobile-app/hooks/useVaultSync.ts +++ b/apps/mobile-app/hooks/useVaultSync.ts @@ -44,6 +44,7 @@ type VaultSyncOptions = { onStatus?: (message: string) => void; onOffline?: () => void; onUpgradeRequired?: () => void; + abortSignal?: AbortSignal; } /** @@ -58,12 +59,18 @@ export const useVaultSync = () : { const dbContext = useDb(); const syncVault = useCallback(async (options: VaultSyncOptions = {}) => { - const { initialSync = false, onSuccess, onError, onStatus, onOffline, onUpgradeRequired } = options; + const { initialSync = false, onSuccess, onError, onStatus, onOffline, onUpgradeRequired, abortSignal } = options; // For the initial sync, we add an artifical delay to various steps which makes it feel more fluid. const enableDelay = initialSync; try { + // Check if operation was aborted + if (abortSignal?.aborted) { + console.debug('VaultSync: Operation aborted before starting'); + return false; + } + const { isLoggedIn } = await app.initializeAuth(); if (!isLoggedIn) { @@ -71,6 +78,12 @@ export const useVaultSync = () : { return false; } + // Check if operation was aborted + if (abortSignal?.aborted) { + console.debug('VaultSync: Operation aborted after auth check'); + return false; + } + // Update status onStatus?.(t('vault.checkingVaultUpdates')); @@ -79,6 +92,12 @@ export const useVaultSync = () : { await new Promise(resolve => setTimeout(resolve, 300)); } + // Check if operation was aborted + if (abortSignal?.aborted) { + console.debug('VaultSync: Operation aborted after status update'); + return false; + } + // Step 1: Check if a new vault version is available // This calls Auth/status endpoint and compares vault revisions let hasNewVault = false; @@ -86,11 +105,24 @@ export const useVaultSync = () : { try { const versionCheckResult = await NativeVaultManager.isNewVaultVersionAvailable(); + + // Check if operation was aborted after version check + if (abortSignal?.aborted) { + console.debug('VaultSync: Operation aborted after version check'); + return false; + } + hasNewVault = versionCheckResult.isNewVersionAvailable; newRevision = versionCheckResult.newRevision; // Step 2: If a new version is available, download it if (hasNewVault && newRevision != null) { + // Check if operation was aborted before download + if (abortSignal?.aborted) { + console.debug('VaultSync: Operation aborted before download'); + return false; + } + onStatus?.(t('vault.syncingUpdatedVault')); // Run downloadVault with a min delay for UX purposes