From c2ebbe2ad343eed1198ee2dada99d892f7c20f81 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 11 Dec 2025 23:28:47 +0100 Subject: [PATCH] Refactor all vault mutate calls to use async method (#1404) --- .../entrypoints/popup/hooks/useVaultMutate.ts | 76 +----- .../entrypoints/popup/pages/auth/Upgrade.tsx | 28 +-- .../pages/credentials/CredentialAddEdit.tsx | 51 ++-- .../popup/pages/items/ItemsList.tsx | 32 +-- .../popup/pages/items/RecentlyDeleted.tsx | 91 ++------ .../popup/pages/passkeys/PasskeyCreate.tsx | 217 ++++++++---------- 6 files changed, 161 insertions(+), 334 deletions(-) diff --git a/apps/browser-extension/src/entrypoints/popup/hooks/useVaultMutate.ts b/apps/browser-extension/src/entrypoints/popup/hooks/useVaultMutate.ts index 730cf5758..576cfc798 100644 --- a/apps/browser-extension/src/entrypoints/popup/hooks/useVaultMutate.ts +++ b/apps/browser-extension/src/entrypoints/popup/hooks/useVaultMutate.ts @@ -1,5 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { useCallback, useRef } from 'react'; import { sendMessage } from 'webext-bridge/popup'; import { useDb } from '@/entrypoints/popup/context/DbContext'; @@ -8,18 +7,13 @@ import { EncryptionUtility } from '@/utils/EncryptionUtility'; import { useVaultSync } from './useVaultSync'; -type VaultMutationOptions = { - onSuccess?: () => void; - onError?: (error: Error) => void; -} - /** * Hook to execute a vault mutation. * * Flow: * 1. Execute the mutation on local database * 2. Save encrypted vault locally and mark as dirty (increments mutation sequence) - * 3. Trigger sync which handles: upload, merge if needed, offline mode + * 3. Trigger sync in background which handles: upload, merge if needed, offline mode * * The mutation sequence is used for race detection: * - Each mutation increments the sequence @@ -27,14 +21,8 @@ type VaultMutationOptions = { * - This ensures we never lose local changes during concurrent operations */ export function useVaultMutate(): { - executeVaultMutation: (operation: () => Promise, options?: VaultMutationOptions) => Promise; executeVaultMutationAsync: (operation: () => Promise) => Promise; - isLoading: boolean; - syncStatus: string; } { - const { t } = useTranslation(); - const [isLoading, setIsLoading] = useState(false); - const [syncStatus, setSyncStatus] = useState(''); const dbContext = useDb(); const { syncVault } = useVaultSync(); @@ -131,67 +119,7 @@ export function useVaultMutate(): { void triggerSync(); }, [saveLocally, triggerSync]); - /** - * Execute a vault mutation: save locally, then sync with server. - * - * The sync handles all scenarios: - * - Online + same revision + isDirty → upload - * - Online + server newer + isDirty → merge + upload - * - Offline → changes are safe locally, will sync when back online - */ - const executeVaultMutation = useCallback(async ( - operation: () => Promise, - options: VaultMutationOptions = {} - ) => { - try { - setIsLoading(true); - setSyncStatus(t('common.savingChangesToVault')); - - // 1. Execute mutation and save locally (always succeeds if no exceptions) - await saveLocally(operation); - - // 2. Sync with server (handles upload, merge, offline - everything) - await syncVault({ - /** - * Handle status updates during sync. - * @param message - Status message to display - */ - onStatus: (message) => setSyncStatus(message), - /** - * Handle successful sync completion. - */ - onSuccess: async () => { - // Refresh state from storage - await dbContext.refreshSyncState(); - options.onSuccess?.(); - }, - /** - * Handle offline mode - local save succeeded. - */ - onOffline: () => { - // Local save succeeded, user can continue working offline - setSyncStatus(t('common.offlineModeSaved')); - options.onSuccess?.(); - }, - /** - * Handle sync errors. - * @param error - Error message from sync - */ - onError: (error) => options.onError?.(new Error(error)) - }); - } catch (error) { - console.error('Error during vault mutation:', error); - options.onError?.(error instanceof Error ? error : new Error(t('common.errors.unknownError'))); - } finally { - setIsLoading(false); - setSyncStatus(''); - } - }, [dbContext, saveLocally, syncVault, t]); - return { - executeVaultMutation, executeVaultMutationAsync, - isLoading, - syncStatus, }; } diff --git a/apps/browser-extension/src/entrypoints/popup/pages/auth/Upgrade.tsx b/apps/browser-extension/src/entrypoints/popup/pages/auth/Upgrade.tsx index 3487891d7..655435fc5 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/auth/Upgrade.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/auth/Upgrade.tsx @@ -36,7 +36,7 @@ const Upgrade: React.FC = () => { const [showVersionInfo, setShowVersionInfo] = useState(false); const { setIsInitialLoading } = useLoading(); const webApi = useWebApi(); - const { executeVaultMutation, isLoading: isVaultMutationLoading, syncStatus } = useVaultMutate(); + const { executeVaultMutationAsync } = useVaultMutate(); const { syncVault } = useVaultSync(); const navigate = useNavigate(); @@ -127,7 +127,7 @@ const Upgrade: React.FC = () => { } // Use the useVaultMutate hook to handle the upgrade and vault upload - await executeVaultMutation(async () => { + await executeVaultMutationAsync(async () => { // Begin transaction sqliteClient.beginTransaction(); @@ -146,21 +146,9 @@ const Upgrade: React.FC = () => { // Commit transaction sqliteClient.commitTransaction(); - }, { - /** - * Handle successful upgrade completion. - */ - onSuccess: () => { - void handleUpgradeSuccess(); - }, - /** - * Handle upgrade error. - */ - onError: (error: Error) => { - console.error('Upgrade failed:', error); - setError(error.message); - } }); + + await handleUpgradeSuccess(); } catch (error) { console.error('Upgrade failed:', error); setError(error instanceof Error ? error.message : t('common.errors.unknownError')); @@ -217,11 +205,11 @@ const Upgrade: React.FC = () => { return (
{/* Full loading screen overlay */} - {(isLoading || isVaultMutationLoading) && ( + {isLoading && (
- {syncStatus || t('upgrade.upgrading')} + {t('upgrade.upgrading')}
)} @@ -313,13 +301,13 @@ const Upgrade: React.FC = () => { id="upgrade-button" onClick={handleUpgrade} > - {isLoading || isVaultMutationLoading ? (syncStatus || t('upgrade.upgrading')) : t('upgrade.upgrade')} + {isLoading ? t('upgrade.upgrading') : t('upgrade.upgrade')} diff --git a/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialAddEdit.tsx b/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialAddEdit.tsx index 54efb3160..4c504ad72 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialAddEdit.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialAddEdit.tsx @@ -91,7 +91,7 @@ const CredentialAddEdit: React.FC = () => { Notes: Yup.string().nullable().optional() }), [t]); - const { executeVaultMutation, isLoading, syncStatus } = useVaultMutate(); + const { executeVaultMutationAsync } = useVaultMutate(); const [mode, setMode] = useState('random'); const { setHeaderButtons } = useHeaderButtons(); const { setIsInitialLoading } = useLoading(); @@ -381,18 +381,13 @@ const CredentialAddEdit: React.FC = () => { return; } - executeVaultMutation(async () => { + await executeVaultMutationAsync(async () => { dbContext.sqliteClient!.deleteCredentialById(id); - }, { - /** - * Navigate to the credentials list page on success. - */ - onSuccess: () => { - void clearPersistedValues(); - navigate('/credentials'); - } }); - }, [id, executeVaultMutation, dbContext.sqliteClient, navigate, clearPersistedValues]); + + void clearPersistedValues(); + navigate('/credentials'); + }, [id, executeVaultMutationAsync, dbContext.sqliteClient, navigate, clearPersistedValues]); /** * Initialize the identity and password generators with settings from user's vault. @@ -595,7 +590,7 @@ const CredentialAddEdit: React.FC = () => { } } - executeVaultMutation(async () => { + await executeVaultMutationAsync(async () => { setLocalLoading(false); if (isEditMode) { @@ -609,23 +604,18 @@ const CredentialAddEdit: React.FC = () => { const credentialId = await dbContext.sqliteClient!.createCredential(data, attachments, totpCodes); data.Id = credentialId.toString(); } - }, { - /** - * Navigate to the credential details page on success. - */ - onSuccess: () => { - void clearPersistedValues(); - // If in add mode, navigate to the credential details page. - if (!isEditMode) { - // Navigate to the credential details page. - navigate(`/credentials/${data.Id}`, { replace: true }); - } else { - // If in edit mode, pop the current page from the history stack to end up on details page as well. - navigate(-1); - } - }, }); - }, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues, originalAttachmentIds, attachments, originalTotpCodeIds, totpCodes, passkeyMarkedForDeletion]); + + void clearPersistedValues(); + // If in add mode, navigate to the credential details page. + if (!isEditMode) { + // Navigate to the credential details page. + navigate(`/credentials/${data.Id}`, { replace: true }); + } else { + // If in edit mode, pop the current page from the history stack to end up on details page as well. + navigate(-1); + } + }, [isEditMode, dbContext.sqliteClient, executeVaultMutationAsync, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues, originalAttachmentIds, attachments, originalTotpCodeIds, totpCodes, passkeyMarkedForDeletion]); // Set header buttons on mount and clear on unmount useEffect((): (() => void) => { @@ -664,12 +654,9 @@ const CredentialAddEdit: React.FC = () => { return (