diff --git a/apps/browser-extension/src/entrypoints/popup/components/EmailPreview.tsx b/apps/browser-extension/src/entrypoints/popup/components/EmailPreview.tsx index 9eaadf637..75934940b 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/EmailPreview.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/EmailPreview.tsx @@ -156,14 +156,30 @@ export const EmailPreview: React.FC = ({ email }) => { } return prevEmails; }); + + // Clear any previous error on successful load + setError(null); } } catch { // Try to parse as error response instead const apiErrorResponse = response as ApiErrorResponse; + + // Suppress errors while vault has unsynced changes (e.g., after item creation) + // The server may not know about newly created items/aliases yet + if (dbContext.shouldSuppressEmailErrors()) { + // Don't set error, keep loading state - will retry on next interval + return; + } + setError(t('emails.apiErrors.' + apiErrorResponse?.code)); return; } } catch { + // Suppress errors while vault has unsynced changes + if (dbContext.shouldSuppressEmailErrors()) { + return; + } + setError(t('common.errors.unknownError')); return; } diff --git a/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx b/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx index 816fe947d..062d90474 100644 --- a/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx +++ b/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx @@ -44,6 +44,12 @@ type DbContextType = { * Set the syncing state. */ setIsSyncing: (syncing: boolean) => void; + /** + * Check if email errors should be suppressed. + * Errors are suppressed when vault has local changes not yet synced, + * as the server may not know about newly created items/aliases yet. + */ + shouldSuppressEmailErrors: () => boolean; /** * Load a decrypted vault into memory (SQLite client). */ @@ -107,6 +113,15 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } */ const [serverRevision, setServerRevision] = useState(0); + /** + * Check if email errors should be suppressed. + * Errors are suppressed when vault has local changes not yet synced, + * as the server may not know about newly created items/aliases yet. + */ + const shouldSuppressEmailErrors = useCallback(() => { + return isDirty || isSyncing; + }, [isDirty, isSyncing]); + /** * Set the offline mode state and persist it to local storage. * Updates both ref (sync) and state (triggers re-render). @@ -277,6 +292,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } serverRevision, setIsOffline, setIsSyncing, + shouldSuppressEmailErrors, loadDatabase, loadStoredDatabase, storeEncryptionKey, @@ -285,7 +301,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } getVaultMetadata, refreshSyncState, hasPendingMigrations, - }), [sqliteClient, dbInitialized, dbAvailable, isOffline, getIsOffline, isDirty, isSyncing, serverRevision, setIsOffline, loadDatabase, loadStoredDatabase, storeEncryptionKey, storeEncryptionKeyDerivationParams, clearDatabase, getVaultMetadata, refreshSyncState, hasPendingMigrations]); + }), [sqliteClient, dbInitialized, dbAvailable, isOffline, getIsOffline, isDirty, isSyncing, serverRevision, setIsOffline, shouldSuppressEmailErrors, loadDatabase, loadStoredDatabase, storeEncryptionKey, storeEncryptionKeyDerivationParams, clearDatabase, getVaultMetadata, refreshSyncState, hasPendingMigrations]); return ( diff --git a/apps/mobile-app/components/items/details/EmailPreview.tsx b/apps/mobile-app/components/items/details/EmailPreview.tsx index 25881ef5e..4d188d298 100644 --- a/apps/mobile-app/components/items/details/EmailPreview.tsx +++ b/apps/mobile-app/components/items/details/EmailPreview.tsx @@ -204,10 +204,23 @@ export const EmailPreview: React.FC = ({ email }) : React.Rea } catch { // Try to parse as error response instead const apiErrorResponse = response as ApiErrorResponse; + + // Suppress errors while vault has unsynced changes (e.g., after item creation) + // The server may not know about newly created items/aliases yet + if (dbContext.shouldSuppressEmailErrors()) { + // Don't set error, keep loading state - will retry on next interval + return; + } + setError(t(`apiErrors.${apiErrorResponse?.code}`)); return; } } catch { + // Suppress errors while vault has unsynced changes + if (dbContext.shouldSuppressEmailErrors()) { + return; + } + setError(t('items.emailLoadError')); } } diff --git a/apps/mobile-app/context/DbContext.tsx b/apps/mobile-app/context/DbContext.tsx index 08259df40..8e81a958e 100644 --- a/apps/mobile-app/context/DbContext.tsx +++ b/apps/mobile-app/context/DbContext.tsx @@ -15,6 +15,12 @@ type DbContextType = { isOffline: boolean; setIsSyncing: (syncing: boolean) => void; setIsOffline: (offline: boolean) => Promise; + /** + * Check if email errors should be suppressed. + * Errors are suppressed when vault has local changes not yet synced, + * as the server may not know about newly created items/aliases yet. + */ + shouldSuppressEmailErrors: () => boolean; refreshSyncState: () => Promise; storeEncryptionKey: (derivedKey: string) => Promise; storeEncryptionKeyDerivationParams: (keyDerivationParams: EncryptionKeyDerivationParams) => Promise; @@ -63,6 +69,15 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } */ const [isOffline, setIsOfflineState] = useState(false); + /** + * Check if email errors should be suppressed. + * Errors are suppressed when vault has local changes not yet synced, + * as the server may not know about newly created items/aliases yet. + */ + const shouldSuppressEmailErrors = useCallback(() => { + return isDirty || isSyncing; + }, [isDirty, isSyncing]); + /** * Unlock the vault in the native module which will decrypt the database using the stored encryption key * and load it into memory. @@ -246,6 +261,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } isOffline, setIsSyncing, setIsOffline, + shouldSuppressEmailErrors, refreshSyncState, hasPendingMigrations, clearDatabase, @@ -256,7 +272,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } storeEncryptionKeyDerivationParams, checkStoredVault, setDatabaseAvailable, - }), [sqliteClient, dbInitialized, dbAvailable, isDirty, isSyncing, isOffline, setIsSyncing, setIsOffline, refreshSyncState, hasPendingMigrations, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams, checkStoredVault, setDatabaseAvailable]); + }), [sqliteClient, dbInitialized, dbAvailable, isDirty, isSyncing, isOffline, setIsSyncing, setIsOffline, shouldSuppressEmailErrors, refreshSyncState, hasPendingMigrations, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams, checkStoredVault, setDatabaseAvailable]); return ( diff --git a/apps/server/AliasVault.Api/Controllers/VaultController.cs b/apps/server/AliasVault.Api/Controllers/VaultController.cs index 1941ba0cc..9899b1eb3 100644 --- a/apps/server/AliasVault.Api/Controllers/VaultController.cs +++ b/apps/server/AliasVault.Api/Controllers/VaultController.cs @@ -141,6 +141,9 @@ public class VaultController(ILogger logger, IAliasServerDbCont return Unauthorized(); } + // Simulate a delay to test the email error suppression logic. + await Task.Delay(10000); + // Compare the logged-in username with the username in the provided vault model. // If they do not match reject the request. This is important because it's // possible that a user has logged in with a different username than the one