From 62b4ec86431bb8e1063c9b83bff89577affab7e1 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 31 Jan 2026 14:29:38 +0100 Subject: [PATCH 1/2] Add explicit client version disable mechanism to API (#1548) --- .../Controllers/AuthController.cs | 9 +- .../AliasVault.Api/Helpers/VersionHelper.cs | 32 +++++++ .../Shared/AliasVault.Shared.Core/AppInfo.cs | 20 +++- .../Vault/VersionTests.cs | 92 +++++++++++++++++++ 4 files changed, 148 insertions(+), 5 deletions(-) diff --git a/apps/server/AliasVault.Api/Controllers/AuthController.cs b/apps/server/AliasVault.Api/Controllers/AuthController.cs index 4a660acdb..48d2880ba 100644 --- a/apps/server/AliasVault.Api/Controllers/AuthController.cs +++ b/apps/server/AliasVault.Api/Controllers/AuthController.cs @@ -110,10 +110,11 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM if (AppInfo.MinimumClientVersions.TryGetValue(platform, out var minimumVersion)) { - if (VersionHelper.IsVersionEqualOrNewer(clientVersion, minimumVersion)) - { - clientSupported = true; - } + // Check if version meets minimum requirement AND is not in blocked list + var meetsMinimum = VersionHelper.IsVersionEqualOrNewer(clientVersion, minimumVersion); + var isBlocked = VersionHelper.IsVersionBlocked(platform, clientVersion, AppInfo.UnsupportedClientVersions); + + clientSupported = meetsMinimum && !isBlocked; } else { diff --git a/apps/server/AliasVault.Api/Helpers/VersionHelper.cs b/apps/server/AliasVault.Api/Helpers/VersionHelper.cs index 5f2f6d4b6..34bcd8c93 100644 --- a/apps/server/AliasVault.Api/Helpers/VersionHelper.cs +++ b/apps/server/AliasVault.Api/Helpers/VersionHelper.cs @@ -59,4 +59,36 @@ public static class VersionHelper // Compare the versions return v1 >= v2; } + + /// + /// Checks if a version is blocked for a specific platform. + /// Checks both platform-specific blocks and global blocks (using "*" key). + /// + /// The platform to check (e.g., "chrome", "ios"). + /// The version to check. + /// Dictionary of platform to blocked versions. Use "*" for global blocks. + /// True if the version is blocked for this platform, false otherwise. + public static bool IsVersionBlocked(string platform, string version, IReadOnlyDictionary> blockedVersions) + { + if (string.IsNullOrEmpty(version) || blockedVersions == null || blockedVersions.Count == 0) + { + return false; + } + + // Check global blocks (applies to all platforms) + if (blockedVersions.TryGetValue("*", out var globalBlocked) && globalBlocked.Contains(version)) + { + return true; + } + + // Check platform-specific blocks + if (!string.IsNullOrEmpty(platform) && + blockedVersions.TryGetValue(platform, out var platformBlocked) && + platformBlocked.Contains(version)) + { + return true; + } + + return false; + } } diff --git a/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs b/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs index c78e9cb95..d79fc7b41 100644 --- a/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs +++ b/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs @@ -42,7 +42,7 @@ public static class AppInfo /// for all clients as we are using a monorepo to build all clients from the same source code. But it's /// possible to override the minimum client version for a specific client if needed. /// - public const string MinimumClientVersion = "0.26.1"; + public const string MinimumClientVersion = "0.12.0"; /// /// Gets a dictionary of minimum supported client versions that the WebApi supports. @@ -68,6 +68,24 @@ public static class AppInfo { "android", MinimumClientVersion }, }.AsReadOnly(); + /// + /// Gets a dictionary of specific client versions that are explicitly unsupported (blocked) per platform. + /// This is useful for blocking specific versions with (critical) bugs while still allowing + /// older versions that predate the bug to continue working. + /// For example: if 0.26.0 has a critical bug fixed in 0.26.1, we can block only 0.26.0 + /// without affecting users on 0.25.x who may still have compatible vaults. + /// Use "*" as the platform key to block a version across all platforms. + /// + public static IReadOnlyDictionary> UnsupportedClientVersions { get; } = new Dictionary> + { + // Block version across all platforms: "*" applies to all clients. + { "*", ["0.26.0"] }, // Version with vault migration bug, fixed in 0.26.1 + + // Platform-specific blocks (examples): + // { "chrome", ["0.25.0"] }, + // { "ios", ["0.24.0", "0.24.1"] }, + }; + /// /// Gets the build number, typically used in CI/CD pipelines. /// Can be overridden at build time. diff --git a/apps/server/Tests/AliasVault.UnitTests/Vault/VersionTests.cs b/apps/server/Tests/AliasVault.UnitTests/Vault/VersionTests.cs index 0b3685858..fb4fef0cc 100644 --- a/apps/server/Tests/AliasVault.UnitTests/Vault/VersionTests.cs +++ b/apps/server/Tests/AliasVault.UnitTests/Vault/VersionTests.cs @@ -68,4 +68,96 @@ public class VersionTests var version2 = "1.1.0"; Assert.That(VersionHelper.IsVersionEqualOrNewer(version1, version2), Is.True); } + + /// + /// Test that a version in the global blocked list is correctly identified as blocked. + /// + [Test] + public void VersionBlockedReturnsTrueForGloballyBlockedVersion() + { + var blockedVersions = new Dictionary> + { + { "*", ["0.26.0", "0.27.0"] }, + }; + + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("ios", "0.27.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("android", "0.26.0", blockedVersions), Is.True); + } + + /// + /// Test that a version in the platform-specific blocked list is correctly identified as blocked. + /// + [Test] + public void VersionBlockedReturnsTrueForPlatformSpecificBlockedVersion() + { + var blockedVersions = new Dictionary> + { + { "chrome", ["0.25.0"] }, + { "ios", ["0.24.0"] }, + }; + + // Platform-specific blocks should work + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.25.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("ios", "0.24.0", blockedVersions), Is.True); + + // Other platforms should not be blocked + Assert.That(VersionHelper.IsVersionBlocked("firefox", "0.25.0", blockedVersions), Is.False); + Assert.That(VersionHelper.IsVersionBlocked("android", "0.24.0", blockedVersions), Is.False); + } + + /// + /// Test that global and platform-specific blocks work together. + /// + [Test] + public void VersionBlockedCombinesGlobalAndPlatformSpecific() + { + var blockedVersions = new Dictionary> + { + { "*", ["0.26.0"] }, + { "chrome", ["0.25.0"] }, + }; + + // Global block applies to all platforms for exact version + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("ios", "0.26.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("ios", "0.26.1", blockedVersions), Is.False); + + // Platform-specific block only applies to that platform + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.25.0", blockedVersions), Is.True); + Assert.That(VersionHelper.IsVersionBlocked("firefox", "0.25.0", blockedVersions), Is.False); + } + + /// + /// Test that a version not in the blocked list is correctly identified as not blocked. + /// + [Test] + public void VersionBlockedReturnsFalseForNonBlockedVersion() + { + var blockedVersions = new Dictionary> + { + { "*", ["0.26.0"] }, + }; + + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.25.3", blockedVersions), Is.False); + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.1", blockedVersions), Is.False); + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.27.0", blockedVersions), Is.False); + } + + /// + /// Test that empty or null inputs are handled correctly. + /// + [Test] + public void VersionBlockedHandlesEmptyInputs() + { + var blockedVersions = new Dictionary> + { + { "*", ["0.26.0"] }, + }; + + var emptyBlockedVersions = new Dictionary>(); + + Assert.That(VersionHelper.IsVersionBlocked("chrome", string.Empty, blockedVersions), Is.False); + Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.0", emptyBlockedVersions), Is.False); + } } From ed6a8e5a153b2ea26621f86841d7247cbf19dcb3 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 31 Jan 2026 15:56:22 +0100 Subject: [PATCH 2/2] Tweak mobile app and browser extension clientSupported check to show proper error (#1548) --- .../entrypoints/popup/hooks/useVaultSync.ts | 6 ++ .../entrypoints/popup/pages/auth/Login.tsx | 20 +++++-- .../aliasvault/app/vaultstore/VaultSync.kt | 2 + apps/mobile-app/app/login.tsx | 47 ++++++++------- apps/mobile-app/context/AuthContext.tsx | 11 ++-- apps/mobile-app/context/DialogContext.tsx | 57 +++++++++++++++++-- apps/mobile-app/events/DialogEventEmitter.ts | 30 ++++++++++ apps/mobile-app/hooks/useVaultSync.ts | 17 ++++-- .../ios/VaultStoreKit/VaultStore+Sync.swift | 3 +- 9 files changed, 155 insertions(+), 38 deletions(-) create mode 100644 apps/mobile-app/events/DialogEventEmitter.ts diff --git a/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts b/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts index c9a904495..5f5c4074b 100644 --- a/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts +++ b/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts @@ -109,6 +109,12 @@ export const useVaultSync = (): { syncVault: (options?: VaultSyncOptions) => Pro const statusError = webApi.validateStatusResponse(statusResponse); if (statusError) { + // Version compatibility errors require logout + if (statusError === 'clientVersionNotSupported' || statusError === 'serverVersionNotSupported') { + await app.logout(t('common.errors.' + statusError)); + return false; + } + // Other errors just show the error onError?.(t('common.errors.' + statusError)); return false; } diff --git a/apps/browser-extension/src/entrypoints/popup/pages/auth/Login.tsx b/apps/browser-extension/src/entrypoints/popup/pages/auth/Login.tsx index 7fc3f7616..936aae1f6 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/auth/Login.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/auth/Login.tsx @@ -26,6 +26,9 @@ import type { MobileLoginResult } from '@/utils/types/messaging/MobileLoginResul import { storage } from '#imports'; +/** Track if username prefill has been attempted (only do it once on mount) */ +let usernamePrefillAttempted = false; + /** * Login page */ @@ -168,6 +171,9 @@ const Login: React.FC = () => { return; } + // Reset prefill flag so next logout will prefill again + usernamePrefillAttempted = false; + // Navigate to reinitialize page which will take care of the proper redirect. navigate('/reinitialize', { replace: true }); @@ -188,10 +194,16 @@ const Login: React.FC = () => { } setClientUrl(clientUrl); - // Check for saved username (from forced logout) and prefill - const savedUsername = await storage.getItem('local:username') as string | null; - if (savedUsername) { - setCredentials(prev => ({ ...prev, username: savedUsername })); + /* + * Check for saved username (from forced logout) and prefill once on mount + * If user clears it, don't repopulate + */ + if (!usernamePrefillAttempted) { + usernamePrefillAttempted = true; + const savedUsername = await storage.getItem('local:username') as string | null; + if (savedUsername) { + setCredentials(prev => ({ ...prev, username: savedUsername })); + } } setIsInitialLoading(false); diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt index 8fd1fbd76..b3092fe26 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt @@ -623,6 +623,8 @@ class VaultSync( is VaultSyncError.SessionExpired, is VaultSyncError.AuthenticationFailed, is VaultSyncError.PasswordChanged, + is VaultSyncError.ClientVersionNotSupported, + is VaultSyncError.ServerVersionNotSupported, -> { VaultSyncResult( success = false, diff --git a/apps/mobile-app/app/login.tsx b/apps/mobile-app/app/login.tsx index e1f740f7c..dfc8e547c 100644 --- a/apps/mobile-app/app/login.tsx +++ b/apps/mobile-app/app/login.tsx @@ -4,7 +4,7 @@ import { MaterialIcons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import { LinearGradient } from 'expo-linear-gradient'; import { router } from 'expo-router'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { StyleSheet, View, Text, SafeAreaView, TextInput, ActivityIndicator, Animated, ScrollView, KeyboardAvoidingView, Platform, Dimensions } from 'react-native'; import { useApiUrl } from '@/utils/ApiUrlUtility'; @@ -41,6 +41,9 @@ export default function LoginScreen() : React.ReactNode { const [fadeAnim] = useState(new Animated.Value(0)); const { loadApiUrl, getDisplayUrl } = useApiUrl(); + // Track if username prefill has been attempted (only do it once on mount) + const usernamePrefillAttemptedRef = useRef(false); + useEffect(() => { Animated.timing(fadeAnim, { toValue: 1, @@ -52,19 +55,18 @@ export default function LoginScreen() : React.ReactNode { /** * Check for saved username (from forced logout) and prefill the username field. * This enables users to easily re-login after a forced logout. - * Only prefill if the username field is empty (user hasn't started typing). + * Only prefill once on mount - if user clears it, don't repopulate. */ const loadSavedUsername = async () : Promise => { + if (usernamePrefillAttemptedRef.current) { + return; + } + usernamePrefillAttemptedRef.current = true; + try { const savedUsername = await NativeVaultManager.getUsername(); if (savedUsername) { - setCredentials(prev => { - // Only prefill if username is empty - don't overwrite user input - if (prev.username === '') { - return { ...prev, username: savedUsername }; - } - return prev; - }); + setCredentials(prev => ({ ...prev, username: savedUsername })); } } catch { // Ignore errors - username prefill is optional @@ -222,14 +224,17 @@ export default function LoginScreen() : React.ReactNode { } } - let checkSuccess = true; + let upgradeRequired = false; /* * Sync vault from server (downloads, stores, and validates compatibility) * This will handle the forced logout recovery check in case our local vault is dirty * or is ahead of server in case of RPO event. + * + * Critical errors (auth, version) are handled internally via app.logout(message) + * which shows a native alert. We check the return value to know if sync succeeded. */ - await syncVault({ + const syncSuccess = await syncVault({ /** * Update login status during sync. */ @@ -237,21 +242,18 @@ export default function LoginScreen() : React.ReactNode { setLoginStatus(status); }, /** - * Handle the status update. + * Handle non-critical errors (shown via custom dialog). */ onError: (message) => { - checkSuccess = false; - - // Show modal with error message + // Show modal with error message for non-critical errors showAlert(t('common.error'), message); - // Error will trigger logout through the sync process setIsLoading(false); }, /** * On upgrade required. */ onUpgradeRequired: async () : Promise => { - checkSuccess = false; + upgradeRequired = true; // Still login to ensure the user is logged in. await authContext.login(); @@ -262,8 +264,12 @@ export default function LoginScreen() : React.ReactNode { }, }); - if (!checkSuccess) { - // If the syncvault checks have failed, we can't continue with the login process. + if (!syncSuccess || upgradeRequired) { + /* + * Sync failed or upgrade required - don't continue with login + * Critical errors already showed alert via app.logout() + */ + setIsLoading(false); return; } @@ -275,6 +281,9 @@ export default function LoginScreen() : React.ReactNode { await authContext.login(); + // Reset prefill flag so next logout will prefill again + usernamePrefillAttemptedRef.current = false; + authContext.setOfflineMode(false); setTwoFactorRequired(false); setTwoFactorCode(''); diff --git a/apps/mobile-app/context/AuthContext.tsx b/apps/mobile-app/context/AuthContext.tsx index 121616902..5669f3342 100644 --- a/apps/mobile-app/context/AuthContext.tsx +++ b/apps/mobile-app/context/AuthContext.tsx @@ -4,10 +4,11 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { NavigationContainerRef, ParamListBase } from '@react-navigation/native'; import * as LocalAuthentication from 'expo-local-authentication'; import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react'; -import { Alert, Platform } from 'react-native'; +import { Platform } from 'react-native'; import EncryptionUtility from '@/utils/EncryptionUtility'; import { useDb } from '@/context/DbContext'; +import { dialogEventEmitter } from '@/events/DialogEventEmitter'; import NativeVaultManager from '@/specs/NativeVaultManager'; import i18n from '@/i18n'; @@ -221,11 +222,9 @@ export const AuthProvider: React.FC<{ await AsyncStorage.multiRemove(['accessToken', 'refreshToken', 'authMethods']); if (errorMessage) { - Alert.alert( - i18n.t('common.error'), - errorMessage, - [{ text: i18n.t('common.ok'), style: 'default' }] - ); + // Use event emitter to show dialog via DialogContext + // This allows Android to use the custom styled dialog + dialogEventEmitter.emitAlert(i18n.t('common.error'), errorMessage); } setIsLoggedIn(false); diff --git a/apps/mobile-app/context/DialogContext.tsx b/apps/mobile-app/context/DialogContext.tsx index b7009d06c..a415fb769 100644 --- a/apps/mobile-app/context/DialogContext.tsx +++ b/apps/mobile-app/context/DialogContext.tsx @@ -1,7 +1,8 @@ -import React, { createContext, useContext, useState, useCallback, useMemo } from 'react'; +import React, { createContext, useContext, useState, useCallback, useMemo, useRef, useEffect } from 'react'; import { Alert, Platform } from 'react-native'; import { ConfirmDialog, type IConfirmDialogButton } from '@/components/common/ConfirmDialog'; +import { dialogEventEmitter } from '@/events/DialogEventEmitter'; interface DialogConfig { title: string; @@ -81,10 +82,52 @@ interface DialogProviderProps { export function DialogProvider({ children }: DialogProviderProps): React.ReactNode { const [dialogConfig, setDialogConfig] = useState(null); + // Use a ref to store pending dialog config that survives re-renders/navigation + const pendingDialogRef = useRef(null); + + // Check for pending dialogs on every render + useEffect(() => { + if (pendingDialogRef.current && !dialogConfig) { + setDialogConfig(pendingDialogRef.current); + pendingDialogRef.current = null; + } + }); + + // Subscribe to dialog events from outside React (e.g., AuthContext logout) + useEffect(() => { + const unsubscribe = dialogEventEmitter.subscribe((title, message) => { + // On iOS, use native Alert + if (Platform.OS === 'ios') { + Alert.alert(title, message, [{ text: 'OK', style: 'default' }]); + return; + } + + // On Android, use custom dialog with ref persistence + const config: DialogConfig = { + title, + message, + buttons: [{ + text: 'OK', + style: 'default', + onPress: (): void => { + pendingDialogRef.current = null; + setDialogConfig(null); + }, + }], + }; + + pendingDialogRef.current = config; + setDialogConfig(config); + }); + + return unsubscribe; + }, []); + /** * Hide the dialog. */ const hideDialog = useCallback((): void => { + pendingDialogRef.current = null; setDialogConfig(null); }, []); @@ -92,7 +135,7 @@ export function DialogProvider({ children }: DialogProviderProps): React.ReactNo * Show a simple alert with an OK button. */ const showAlert = useCallback((title: string, message: string, onOk?: () => void): void => { - // On iOS, use native Alert for simple alerts too + // On iOS, use native Alert if (Platform.OS === 'ios') { Alert.alert(title, message, [{ text: 'OK', @@ -102,7 +145,8 @@ export function DialogProvider({ children }: DialogProviderProps): React.ReactNo return; } - setDialogConfig({ + // On Android, use custom dialog with ref persistence + const config: DialogConfig = { title, message, buttons: [{ @@ -110,10 +154,15 @@ export function DialogProvider({ children }: DialogProviderProps): React.ReactNo style: 'default', onPress: (): void => { onOk?.(); + pendingDialogRef.current = null; setDialogConfig(null); }, }], - }); + }; + + // Store in ref so it persists through navigation/re-renders + pendingDialogRef.current = config; + setDialogConfig(config); }, []); /** diff --git a/apps/mobile-app/events/DialogEventEmitter.ts b/apps/mobile-app/events/DialogEventEmitter.ts new file mode 100644 index 000000000..462a2244a --- /dev/null +++ b/apps/mobile-app/events/DialogEventEmitter.ts @@ -0,0 +1,30 @@ +/** + * Simple event emitter for showing dialogs from outside React components. + * This allows AuthContext to trigger dialogs in DialogContext without direct coupling. + */ + +type AlertListener = (title: string, message: string) => void; + +class DialogEventEmitter { + private listeners: AlertListener[] = []; + + /** + * Subscribe to alert events. + * @returns Unsubscribe function + */ + subscribe(listener: AlertListener): () => void { + this.listeners.push(listener); + return () => { + this.listeners = this.listeners.filter(l => l !== listener); + }; + } + + /** + * Emit an alert event to all listeners. + */ + emitAlert(title: string, message: string): void { + this.listeners.forEach(listener => listener(title, message)); + } +} + +export const dialogEventEmitter = new DialogEventEmitter(); diff --git a/apps/mobile-app/hooks/useVaultSync.ts b/apps/mobile-app/hooks/useVaultSync.ts index 1ea977f2d..3ec962aff 100644 --- a/apps/mobile-app/hooks/useVaultSync.ts +++ b/apps/mobile-app/hooks/useVaultSync.ts @@ -104,6 +104,7 @@ export const useVaultSync = (): { if (result.wasOffline) { await dbContext.setIsOffline(true); + console.log('[useVaultSync] Set offline mode'); onOffline?.(); // Return true to continue with local vault return true; @@ -155,7 +156,8 @@ export const useVaultSync = (): { console.warn('Vault sync: Failed to register credential identities:', error); } - return hasNewVault; + // Return true for successful sync (regardless of whether vault changed) + return true; } catch (err) { if (err instanceof VaultVersionIncompatibleError) { await app.logout(t(err.message)); @@ -224,6 +226,10 @@ function getVaultSyncErrorCodeFromString(error: string): VaultSyncErrorCode | nu /** * Handle sync errors by mapping error codes to appropriate actions. + * + * For critical errors requiring logout (auth, version), we ALWAYS use app.logout(message) + * which shows a native Alert.alert that persists through navigation on both platforms. + * The onError callback is only used for non-critical errors that don't require logout. */ async function handleSyncError( err: unknown, @@ -235,23 +241,26 @@ async function handleSyncError( onOffline?: () => void ): Promise { switch (errorCode) { + // Authentication errors - logout with message (shows native alert) case VaultSyncErrorCode.SESSION_EXPIRED: case VaultSyncErrorCode.AUTHENTICATION_FAILED: - await app.logout('Your session has expired. Please login again.'); + await app.logout(t('auth.errors.sessionExpired')); return false; case VaultSyncErrorCode.PASSWORD_CHANGED: await app.logout(t('vault.errors.passwordChanged')); return false; + // Version compatibility errors - logout with message (shows native alert) case VaultSyncErrorCode.CLIENT_VERSION_NOT_SUPPORTED: - onError?.(t('vault.errors.versionNotSupported')); + await app.logout(t('vault.errors.versionNotSupported')); return false; case VaultSyncErrorCode.SERVER_VERSION_NOT_SUPPORTED: await app.logout(t('vault.errors.serverVersionNotSupported')); return false; + // Network errors - set offline mode, don't logout case VaultSyncErrorCode.SERVER_UNAVAILABLE: case VaultSyncErrorCode.NETWORK_ERROR: case VaultSyncErrorCode.TIMEOUT: @@ -260,8 +269,8 @@ async function handleSyncError( // Return true to continue with local vault return true; + // Unknown errors - use onError callback if provided default: - // Unknown error const errorMessage = err instanceof Error ? err.message : t('common.errors.unknownError'); onError?.(errorMessage); return false; diff --git a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift index 5059c9084..09a8af622 100644 --- a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift +++ b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift @@ -513,7 +513,8 @@ extension VaultStore { wasOffline: true, error: error.message ) - case .sessionExpired, .authenticationFailed, .passwordChanged: + case .sessionExpired, .authenticationFailed, .passwordChanged, + .clientVersionNotSupported, .serverVersionNotSupported: return VaultSyncResult( success: false, action: .error,