From ff302067de28461184656ed1a4c8cf3bc5d3cc2e Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Wed, 4 Mar 2026 19:59:11 +0100 Subject: [PATCH 01/26] Add password visibility toggle to native mobile app password unlock view (#1808) --- .../res/layout/activity_password_unlock.xml | 2 + .../ios/VaultUI/Auth/PasswordUnlockView.swift | 99 ++++++++++++++++--- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/apps/mobile-app/android/app/src/main/res/layout/activity_password_unlock.xml b/apps/mobile-app/android/app/src/main/res/layout/activity_password_unlock.xml index c6088dea5..2b0936419 100644 --- a/apps/mobile-app/android/app/src/main/res/layout/activity_password_unlock.xml +++ b/apps/mobile-app/android/app/src/main/res/layout/activity_password_unlock.xml @@ -93,6 +93,8 @@ app:hintTextColor="?android:attr/textColorSecondary" app:startIconDrawable="@drawable/ic_lock" app:startIconTint="?android:attr/textColorSecondary" + app:endIconMode="password_toggle" + app:endIconTint="@color/primary" app:boxCornerRadiusTopStart="12dp" app:boxCornerRadiusTopEnd="12dp" app:boxCornerRadiusBottomStart="12dp" diff --git a/apps/mobile-app/ios/VaultUI/Auth/PasswordUnlockView.swift b/apps/mobile-app/ios/VaultUI/Auth/PasswordUnlockView.swift index 0f794bd7c..3c39858fd 100644 --- a/apps/mobile-app/ios/VaultUI/Auth/PasswordUnlockView.swift +++ b/apps/mobile-app/ios/VaultUI/Auth/PasswordUnlockView.swift @@ -8,12 +8,18 @@ private let locBundle = Bundle.vaultUI public struct PasswordUnlockView: View { @ObservedObject public var viewModel: PasswordUnlockViewModel @Environment(\.colorScheme) var colorScheme - @FocusState private var isPasswordFocused: Bool + @FocusState private var focusTextField: Bool + @FocusState private var focusSecureField: Bool + @State private var isPasswordVisible: Bool = false public init(viewModel: PasswordUnlockViewModel) { self._viewModel = ObservedObject(wrappedValue: viewModel) } + private var isPasswordFocused: Bool { + focusTextField || focusSecureField + } + private var colors: ColorConstants.Colors.Type { ColorConstants.colors(for: colorScheme) } @@ -90,21 +96,68 @@ public struct PasswordUnlockView: View { .foregroundColor(colors.text.opacity(0.4)) .font(.system(size: 16)) - SecureField(String(localized: "password", bundle: locBundle), text: $viewModel.password) - .textFieldStyle(.plain) - .font(.system(size: 16)) - .foregroundColor(colors.text) - .focused($isPasswordFocused) - .autocapitalization(.none) - .disableAutocorrection(true) - .submitLabel(.done) - .onSubmit { - if !viewModel.password.isEmpty && !viewModel.isProcessing { - Task { - await viewModel.unlock() + ZStack(alignment: .trailing) { + // TextField (visible password) + TextField(String(localized: "password", bundle: locBundle), text: $viewModel.password) + .textFieldStyle(.plain) + .font(.system(size: 16)) + .foregroundColor(colors.text) + .focused($focusTextField) + .autocapitalization(.none) + .disableAutocorrection(true) + .submitLabel(.done) + .textContentType(.password) + .opacity(isPasswordVisible ? 1 : 0) + .onSubmit { + if !viewModel.password.isEmpty && !viewModel.isProcessing { + Task { + await viewModel.unlock() + } } } - } + + // SecureField (masked password) + SecureField(String(localized: "password", bundle: locBundle), text: $viewModel.password) + .textFieldStyle(.plain) + .font(.system(size: 16)) + .foregroundColor(colors.text) + .focused($focusSecureField) + .autocapitalization(.none) + .disableAutocorrection(true) + .submitLabel(.done) + .textContentType(.password) + .opacity(isPasswordVisible ? 0 : 1) + .onSubmit { + if !viewModel.password.isEmpty && !viewModel.isProcessing { + Task { + await viewModel.unlock() + } + } + } + + // Toggle button + Button( + action: { + // Haptic feedback + let impactFeedback = UIImpactFeedbackGenerator(style: .light) + impactFeedback.impactOccurred() + isPasswordVisible.toggle() + if isPasswordVisible { + focusTextField = true + } else { + focusSecureField = true + } + }, + label: { + Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill") + .foregroundColor(colors.primary) + .font(.system(size: 18, weight: .medium)) + .frame(width: 24, height: 24) + .contentShape(Rectangle()) + } + ) + .buttonStyle(ScaleButtonStyle()) + } } .padding(16) .background(colors.accentBackground) @@ -195,12 +248,28 @@ public struct PasswordUnlockView: View { .onAppear { // Delay focus slightly to ensure smooth animation DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - isPasswordFocused = true + if isPasswordVisible { + focusTextField = true + } else { + focusSecureField = true + } } } } } +// MARK: - Button Styles + +/// A button style that scales down when pressed for tactile feedback +private struct ScaleButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .scaleEffect(configuration.isPressed ? 0.85 : 1.0) + .opacity(configuration.isPressed ? 0.7 : 1.0) + .animation(.easeInOut(duration: 0.15), value: configuration.isPressed) + } +} + // MARK: - Previews #if DEBUG From c83c882dd72f9b41043430865baacda024c26e40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:02:09 +0000 Subject: [PATCH 02/26] Bump the npm_and_yarn group across 1 directory with 2 updates Bumps the npm_and_yarn group with 2 updates in the /apps/mobile-app directory: [serialize-javascript](https://github.com/yahoo/serialize-javascript) and [svgo](https://github.com/svg/svgo). Removes `serialize-javascript` Updates `svgo` from 3.3.2 to 3.3.3 - [Release notes](https://github.com/svg/svgo/releases) - [Commits](https://github.com/svg/svgo/compare/v3.3.2...v3.3.3) --- updated-dependencies: - dependency-name: serialize-javascript dependency-version: dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: svgo dependency-version: 3.3.3 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- apps/mobile-app/package-lock.json | 59 ++++++++----------------------- 1 file changed, 15 insertions(+), 44 deletions(-) diff --git a/apps/mobile-app/package-lock.json b/apps/mobile-app/package-lock.json index 0e99719b1..35db67ede 100644 --- a/apps/mobile-app/package-lock.json +++ b/apps/mobile-app/package-lock.json @@ -4539,15 +4539,6 @@ "node": ">= 10" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@tybys/wasm-util": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", @@ -14574,17 +14565,6 @@ ], "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -15676,10 +15656,13 @@ } }, "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/saxes": { "version": "6.0.0", @@ -15848,17 +15831,6 @@ "node": ">=0.10.0" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -16838,18 +16810,18 @@ "license": "MIT" }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" @@ -16981,9 +16953,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "dev": true, "license": "MIT", "peer": true, @@ -16991,7 +16963,6 @@ "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { From 1c9c3d0e7a6db0648f4a28d1c62200233bd8c0a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:26:47 +0000 Subject: [PATCH 03/26] Bump the npm_and_yarn group across 2 directories with 2 updates Bumps the npm_and_yarn group with 1 update in the /apps/browser-extension directory: [dompurify](https://github.com/cure53/DOMPurify). Bumps the npm_and_yarn group with 1 update in the /apps/mobile-app directory: [tar](https://github.com/isaacs/node-tar). Updates `dompurify` from 3.3.1 to 3.3.2 - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.3.1...3.3.2) Updates `tar` from 7.5.9 to 7.5.10 - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.5.9...v7.5.10) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.3.2 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: tar dependency-version: 7.5.10 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- apps/browser-extension/package-lock.json | 11 +++++++---- apps/browser-extension/package.json | 2 +- apps/mobile-app/package-lock.json | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/browser-extension/package-lock.json b/apps/browser-extension/package-lock.json index 068316c2e..af34244d8 100644 --- a/apps/browser-extension/package-lock.json +++ b/apps/browser-extension/package-lock.json @@ -16,7 +16,7 @@ "@types/dompurify": "^3.0.5", "argon2-browser": "^1.18.0", "buffer": "^6.0.3", - "dompurify": "^3.3.1", + "dompurify": "^3.3.2", "globals": "^16.0.0", "i18next": "^25.3.1", "otpauth": "^9.3.6", @@ -4993,10 +4993,13 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", + "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "license": "(MPL-2.0 OR Apache-2.0)", + "engines": { + "node": ">=20" + }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index d3e648b5f..acd0219c4 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -36,7 +36,7 @@ "@types/dompurify": "^3.0.5", "argon2-browser": "^1.18.0", "buffer": "^6.0.3", - "dompurify": "^3.3.1", + "dompurify": "^3.3.2", "globals": "^16.0.0", "i18next": "^25.3.1", "otpauth": "^9.3.6", diff --git a/apps/mobile-app/package-lock.json b/apps/mobile-app/package-lock.json index 35db67ede..1f9cb9207 100644 --- a/apps/mobile-app/package-lock.json +++ b/apps/mobile-app/package-lock.json @@ -16885,9 +16885,9 @@ } }, "node_modules/tar": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", - "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", From ec4aba99678d23995173f32c09e8264ef02dd8f5 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 5 Mar 2026 14:11:12 +0100 Subject: [PATCH 04/26] Add toggle to expand session name in mobile app active sessions screen (#1812) --- .../settings/security/active-sessions.tsx | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx b/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx index 4482e883c..429fc88ef 100644 --- a/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx +++ b/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx @@ -27,6 +27,7 @@ export default function ActiveSessionsScreen() : React.ReactNode { const [refreshTokens, setRefreshTokens] = useState([]); const [isLoading, setIsLoading] = useMinDurationLoading(true, 200); const [isRefreshing, setIsRefreshing] = useMinDurationLoading(false, 200); + const [expandedSessions, setExpandedSessions] = useState>(new Set()); const styles = StyleSheet.create({ detailText: { @@ -158,6 +159,27 @@ export default function ActiveSessionsScreen() : React.ReactNode { return dateObject.toISOString().slice(0, 16).replace('T', ' '); }; + /** + * Toggle the expansion state of a session. + * @param sessionId - The session ID to toggle + */ + const toggleSessionExpansion = (sessionId: string) : void => { + setExpandedSessions((prev) => { + const newSet = new Set(prev); + if (newSet.has(sessionId)) { + newSet.delete(sessionId); + } else { + newSet.add(sessionId); + } + return newSet; + }); + + // Trigger haptic feedback when toggling + if (Platform.OS === 'ios' || Platform.OS === 'android') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } + }; + return ( ( - {item.deviceIdentifier} + toggleSessionExpansion(item.id)} + activeOpacity={0.7} + > + + {item.deviceIdentifier} + + handleRevokeSession(item.id)}> {t('settings.securitySettings.activeSessions.revoke')} From b589e1eb68ed169b5369fda623b6529a68c75b61 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 5 Mar 2026 16:41:39 +0100 Subject: [PATCH 05/26] Add sanity check to biometric unlock attempt and refactor vault unlock method logic (#1809) --- .../nativevaultmanager/NativeVaultManager.kt | 18 ++ .../aliasvault/app/vaultstore/VaultCrypto.kt | 2 +- .../aliasvault/app/vaultstore/VaultStore.kt | 2 +- .../AndroidKeystoreProvider.kt | 77 ++++++- apps/mobile-app/app/(tabs)/settings/index.tsx | 7 +- .../app/(tabs)/settings/vault-unlock.tsx | 98 ++++---- apps/mobile-app/app/login.tsx | 7 +- apps/mobile-app/app/unlock.tsx | 9 +- apps/mobile-app/context/AppContext.tsx | 15 -- apps/mobile-app/context/AuthContext.tsx | 150 +----------- .../RCTNativeVaultManager.mm | 4 + .../ios/NativeVaultManager/VaultManager.swift | 10 + apps/mobile-app/specs/NativeVaultManager.ts | 6 + apps/mobile-app/utils/AppUnlockUtility.ts | 216 ++++++++++++++++++ 14 files changed, 392 insertions(+), 229 deletions(-) create mode 100644 apps/mobile-app/utils/AppUnlockUtility.ts diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt index 78e922b19..f906df7ab 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt @@ -1346,6 +1346,24 @@ class NativeVaultManager(reactContext: ReactApplicationContext) : } } + /** + * Check if biometric unlock is actually available (device + key validation). + * This checks not only if biometrics are configured in auth methods, + * but also validates that the encryption key in KeyStore is valid. + * Returns false if key has been invalidated (e.g., biometric enrollment changed). + * @param promise The promise to resolve with boolean result. + */ + @ReactMethod + override fun isBiometricUnlockAvailable(promise: Promise) { + try { + val available = vaultStore.isBiometricAuthEnabled() + promise.resolve(available) + } catch (e: Exception) { + Log.e(TAG, "Error checking biometric unlock availability", e) + promise.reject("ERR_BIOMETRIC_CHECK", "Failed to check biometric unlock availability: ${e.message}", e) + } + } + /** * Show native PIN setup UI. * Launches the native PinUnlockActivity in setup mode. diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultCrypto.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultCrypto.kt index 24b4a99c1..4689ed036 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultCrypto.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultCrypto.kt @@ -128,7 +128,7 @@ class VaultCrypto( fun storeEncryptionKey(base64EncryptionKey: String, authMethods: String) { this.encryptionKey = Base64.decode(base64EncryptionKey, Base64.NO_WRAP) - if (authMethods.contains(BIOMETRICS_AUTH_METHOD) && keystoreProvider.isBiometricAvailable()) { + if (authMethods.contains(BIOMETRICS_AUTH_METHOD)) { try { val latch = java.util.concurrent.CountDownLatch(1) var error: Exception? = null diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultStore.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultStore.kt index befffc599..c79733f04 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultStore.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultStore.kt @@ -478,7 +478,7 @@ class VaultStore( auth.setAuthMethods(authMethods) - if (!wasBiometricEnabled && isBiometricEnabled && crypto.encryptionKey != null && keystoreProvider.isBiometricAvailable()) { + if (!wasBiometricEnabled && isBiometricEnabled) { try { crypto.storeEncryptionKey( android.util.Base64.encodeToString(crypto.encryptionKey, android.util.Base64.NO_WRAP), diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/keystoreprovider/AndroidKeystoreProvider.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/keystoreprovider/AndroidKeystoreProvider.kt index 18c012290..fe5118496 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/keystoreprovider/AndroidKeystoreProvider.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/keystoreprovider/AndroidKeystoreProvider.kt @@ -66,14 +66,87 @@ class AndroidKeystoreProvider( /** * Whether the biometric is available. - * @return Whether the biometric is available + * Checks both device biometric support AND validates that the keystore key is valid. + * This prevents showing biometric unlock when the key has been invalidated + * (e.g., after biometric enrollment changes). + * @return Whether the biometric is available and key is valid */ override fun isBiometricAvailable(): Boolean { - return _biometricManager.canAuthenticate( + // First check if device supports biometrics + val deviceSupported = _biometricManager.canAuthenticate( BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL, ) == BiometricManager.BIOMETRIC_SUCCESS + + if (!deviceSupported) { + return false + } + + // Check if we have the encrypted key file + val keyFile = File(context.filesDir, ENCRYPTED_KEY_FILE) + if (!keyFile.exists()) { + return false + } + + // Validate that the keystore key exists and is not invalidated + try { + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + + // Check if key alias exists + if (!keyStore.containsAlias(KEYSTORE_ALIAS)) { + // Key doesn't exist - clean up orphaned encrypted key file + Log.d(TAG, "Keystore key not found, removing orphaned encrypted key file") + keyFile.delete() + return false + } + + val secretKey = keyStore.getKey(KEYSTORE_ALIAS, null) as? SecretKey + if (secretKey == null) { + Log.d(TAG, "Failed to retrieve keystore key") + keyFile.delete() + return false + } + + // Try to initialize cipher to detect key invalidation + val cipher = Cipher.getInstance( + "${KeyProperties.KEY_ALGORITHM_AES}/" + + "${KeyProperties.BLOCK_MODE_GCM}/" + + KeyProperties.ENCRYPTION_PADDING_NONE, + ) + + // Read IV from encrypted key file for validation + val encryptedKeyB64 = keyFile.readText() + val combined = Base64.decode(encryptedKeyB64, Base64.NO_WRAP) + val byteBuffer = ByteBuffer.wrap(combined) + val iv = ByteArray(12) + byteBuffer.get(iv) + val spec = GCMParameterSpec(128, iv) + + // Attempt to initialize cipher - this will throw KeyPermanentlyInvalidatedException + // if biometric enrollment has changed + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + + // Key is valid + return true + } catch (e: KeyPermanentlyInvalidatedException) { + // Key has been invalidated due to biometric enrollment change + Log.w(TAG, "Keystore key permanently invalidated, cleaning up", e) + try { + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + keyStore.deleteEntry(KEYSTORE_ALIAS) + keyFile.delete() + } catch (cleanupError: Exception) { + Log.e(TAG, "Error during cleanup of invalidated key", cleanupError) + } + return false + } catch (e: Exception) { + // Any other error means biometric is not available + Log.e(TAG, "Error validating biometric availability: ${e.message}", e) + return false + } } /** diff --git a/apps/mobile-app/app/(tabs)/settings/index.tsx b/apps/mobile-app/app/(tabs)/settings/index.tsx index 56e4023c9..afc5e5f24 100644 --- a/apps/mobile-app/app/(tabs)/settings/index.tsx +++ b/apps/mobile-app/app/(tabs)/settings/index.tsx @@ -6,6 +6,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useApiUrl } from '@/utils/ApiUrlUtility'; import { AppInfo } from '@/utils/AppInfo'; +import { AppUnlockUtility } from '@/utils/AppUnlockUtility'; import { useColors } from '@/hooks/useColorScheme'; import { useLogout } from '@/hooks/useLogout'; @@ -31,7 +32,7 @@ export default function SettingsScreen() : React.ReactNode { const { t } = useTranslation(); const { showAlert, showConfirm } = useDialog(); const insets = useSafeAreaInsets(); - const { getAuthMethodDisplayKey, shouldShowAutofillReminder } = useApp(); + const { shouldShowAutofillReminder } = useApp(); const { getAutoLockTimeout } = useApp(); const { logoutUserInitiated } = useLogout(); const { loadApiUrl, getDisplayUrl } = useApiUrl(); @@ -97,7 +98,7 @@ export default function SettingsScreen() : React.ReactNode { * Load the auth method display. */ const loadAuthMethodDisplay = async () : Promise => { - const authMethodKey = await getAuthMethodDisplayKey(); + const authMethodKey = await AppUnlockUtility.getAuthMethodDisplayKey(); setAuthMethodDisplay(t(authMethodKey)); }; @@ -110,7 +111,7 @@ export default function SettingsScreen() : React.ReactNode { }; loadData(); - }, [getAutoLockTimeout, getAuthMethodDisplayKey, setIsFirstLoad, loadApiUrl, t]) + }, [getAutoLockTimeout, setIsFirstLoad, loadApiUrl, t]) ); /** diff --git a/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx b/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx index 1387c7fdc..04272b6aa 100644 --- a/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx +++ b/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx @@ -1,15 +1,15 @@ -import * as LocalAuthentication from 'expo-local-authentication'; import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { StyleSheet, View, Platform, Linking, Switch, TouchableOpacity } from 'react-native'; import Toast from 'react-native-toast-message'; +import { AppUnlockUtility } from '@/utils/AppUnlockUtility'; + import { useColors } from '@/hooks/useColorScheme'; import { ThemedContainer } from '@/components/themed/ThemedContainer'; import { ThemedScrollView } from '@/components/themed/ThemedScrollView'; import { ThemedText } from '@/components/themed/ThemedText'; -import { AuthMethod, useAuth } from '@/context/AuthContext'; import { useDialog } from '@/context/DialogContext'; import NativeVaultManager from '@/specs/NativeVaultManager'; @@ -20,12 +20,9 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { const colors = useColors(); const { t } = useTranslation(); const { showAlert, showDialog } = useDialog(); - const [initialized, setInitialized] = useState(false); - const { setAuthMethods, getEnabledAuthMethods, getBiometricDisplayName } = useAuth(); const [hasBiometrics, setHasBiometrics] = useState(false); const [isBiometricsEnabled, setIsBiometricsEnabled] = useState(false); const [biometricDisplayName, setBiometricDisplayName] = useState(''); - const [_, setEnabledAuthMethods] = useState([]); // PIN state const [pinEnabled, setPinEnabled] = useState(false); @@ -36,64 +33,44 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { */ const initializeAuth = async () : Promise => { try { - // Check for hardware support - const compatible = await LocalAuthentication.hasHardwareAsync(); + // Check if device has biometric hardware and enrollment + const deviceAvailable = await AppUnlockUtility.isBiometricsAvailableOnDevice(); + setHasBiometrics(deviceAvailable); - // Check if any biometrics are enrolled - const enrolled = await LocalAuthentication.isEnrolledAsync(); - - // Set biometric availability based on all checks - const isBiometricAvailable = compatible && enrolled; - setHasBiometrics(isBiometricAvailable); - - // Get appropriate display name from auth context - const displayName = await getBiometricDisplayName(); + // Get appropriate display name + const displayName = await AppUnlockUtility.getBiometricDisplayName(); setBiometricDisplayName(displayName); - const methods = await getEnabledAuthMethods(); - setEnabledAuthMethods(methods); + const methods = await AppUnlockUtility.getEnabledAuthMethods(); - if (methods.includes('faceid') && enrolled) { - setIsBiometricsEnabled(true); + // Check if biometric unlock is actually functional (validates stored key) + if (methods.includes('faceid') && deviceAvailable) { + const unlockAvailable = await AppUnlockUtility.isBiometricUnlockAvailable(); + + if (!unlockAvailable) { + /* + * Key is invalid (e.g., biometric enrollment changed) + * Remove biometrics from auth methods so user must re-enable it + */ + console.info('Biometric key invalid, removing from auth methods'); + await AppUnlockUtility.disableAuthMethod('faceid'); + setIsBiometricsEnabled(false); + } else { + setIsBiometricsEnabled(true); + } } - // Load PIN settings (locked state removed - automatically handled by native code) + // Load PIN settings const enabled = await NativeVaultManager.isPinEnabled(); setPinEnabled(enabled); - - setInitialized(true); } catch (error) { console.error('Failed to initialize auth:', error); setHasBiometrics(false); - setInitialized(true); } }; initializeAuth(); - }, [getEnabledAuthMethods, getBiometricDisplayName, t]); - - useEffect(() => { - if (!initialized) { - return; - } - - /** - * Update the auth methods. - */ - const updateAuthMethods = async () : Promise => { - const currentAuthMethods = await getEnabledAuthMethods(); - const newAuthMethods = isBiometricsEnabled ? ['faceid', 'password'] : ['password']; - - if (currentAuthMethods.length === newAuthMethods.length && - currentAuthMethods.every(method => newAuthMethods.includes(method))) { - return; - } - - setAuthMethods(newAuthMethods as AuthMethod[]); - }; - - updateAuthMethods(); - }, [isBiometricsEnabled, setAuthMethods, getEnabledAuthMethods, initialized]); + }, [t]); const handleBiometricsToggle = useCallback(async (value: boolean) : Promise => { if (value && !hasBiometrics) { @@ -107,9 +84,9 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { /** * Handle the open settings press. */ - onPress: () : void => { + onPress: async () : Promise => { + await AppUnlockUtility.enableAuthMethod('faceid'); setIsBiometricsEnabled(true); - setAuthMethods(['faceid', 'password']); if (Platform.OS === 'ios') { Linking.openURL('app-settings:'); } else { @@ -123,9 +100,9 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { /** * Handle the cancel press. */ - onPress: () : void => { + onPress: async () : Promise => { + await AppUnlockUtility.disableAuthMethod('faceid'); setIsBiometricsEnabled(false); - setAuthMethods(['password']); }, }, ] @@ -133,8 +110,8 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { return; } - // Check if keystore is available when enabling biometrics (iOS requires device passcode) - if (value && Platform.OS === 'ios') { + // Check if keystore is available when enabling biometrics (requires device passcode) + if (value) { const keystoreAvailable = await NativeVaultManager.isKeystoreAvailable(); if (!keystoreAvailable) { showAlert( @@ -146,13 +123,16 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { } /* - * Biometrics and PIN can now both be enabled simultaneously. - * Biometrics takes priority during unlock, PIN serves as fallback. + * Save new biometrics state. */ + if (value) { + await AppUnlockUtility.enableAuthMethod('faceid'); + } else { + await AppUnlockUtility.disableAuthMethod('faceid'); + } setIsBiometricsEnabled(value); - setAuthMethods(value ? ['faceid', 'password'] : ['password']); - // Show toast notification only on biometrics enabled + // Show toast notification if (value) { Toast.show({ type: 'success', @@ -161,7 +141,7 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { visibilityTime: 1200, }); } - }, [hasBiometrics, setAuthMethods, biometricDisplayName, showDialog, showAlert, t]); + }, [hasBiometrics, biometricDisplayName, showDialog, showAlert, t]); /** * Handle enable PIN - launches native PIN setup UI. diff --git a/apps/mobile-app/app/login.tsx b/apps/mobile-app/app/login.tsx index 69cd2f4e2..ac9843cd6 100644 --- a/apps/mobile-app/app/login.tsx +++ b/apps/mobile-app/app/login.tsx @@ -8,6 +8,7 @@ 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'; +import { AppUnlockUtility } from '@/utils/AppUnlockUtility'; import ConversionUtility from '@/utils/ConversionUtility'; import type { EncryptionKeyDerivationParams } from '@/utils/dist/core/models/metadata'; import type { LoginResponse } from '@/utils/dist/core/models/webapi'; @@ -166,9 +167,9 @@ export default function LoginScreen() : React.ReactNode { passwordHashBase64: string, initiateLoginResponse: LoginResponse ) : Promise => { - // Get biometric display name from auth context - const biometricDisplayName = await authContext.getBiometricDisplayName(); - const isBiometricsEnabledOnDevice = await authContext.isBiometricsEnabledOnDevice(); + // Get biometric display name + const biometricDisplayName = await AppUnlockUtility.getBiometricDisplayName(); + const isBiometricsEnabledOnDevice = await AppUnlockUtility.isBiometricsAvailableOnDevice(); const isKeystoreAvailable = await NativeVaultManager.isKeystoreAvailable(); /* diff --git a/apps/mobile-app/app/unlock.tsx b/apps/mobile-app/app/unlock.tsx index 1bc79d066..670cb49c3 100644 --- a/apps/mobile-app/app/unlock.tsx +++ b/apps/mobile-app/app/unlock.tsx @@ -4,6 +4,7 @@ import { router } from 'expo-router'; import { useState, useEffect, useCallback } from 'react'; import { StyleSheet, View, KeyboardAvoidingView, Platform, ScrollView, Dimensions, Text } from 'react-native'; +import { AppUnlockUtility } from '@/utils/AppUnlockUtility'; import { AppErrorCode, getAppErrorCode, getErrorTranslationKey, formatErrorWithCode } from '@/utils/types/errors/AppErrorCodes'; import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError'; @@ -25,7 +26,7 @@ import NativeVaultManager from '@/specs/NativeVaultManager'; * Unlock screen. */ export default function UnlockScreen() : React.ReactNode { - const { isLoggedIn, username, isBiometricsEnabled, getBiometricDisplayName, getEncryptionKeyDerivationParams } = useApp(); + const { isLoggedIn, username, getEncryptionKeyDerivationParams } = useApp(); const { logoutUserInitiated, logoutForced } = useLogout(); const dbContext = useDb(); const [isLoading, setIsLoading] = useState(true); @@ -74,13 +75,13 @@ export default function UnlockScreen() : React.ReactNode { } // Check if biometrics is available - const enabled = await isBiometricsEnabled(); + const enabled = await AppUnlockUtility.isBiometricUnlockAvailable(); if (!isMounted) { return; } setIsBiometricsAvailable(enabled); - const displayName = await getBiometricDisplayName(); + const displayName = await AppUnlockUtility.getBiometricDisplayName(); if (!isMounted) { return; } @@ -162,7 +163,7 @@ export default function UnlockScreen() : React.ReactNode { return (): void => { isMounted = false; }; - }, [isBiometricsEnabled, getKeyDerivationParams, getBiometricDisplayName, dbContext, isLoggedIn, username, t, logoutForced]); + }, [getKeyDerivationParams, dbContext, isLoggedIn, username, t, logoutForced]); /** * Hide the alert dialog. diff --git a/apps/mobile-app/context/AppContext.tsx b/apps/mobile-app/context/AppContext.tsx index b44ee4f7f..9cb314e5a 100644 --- a/apps/mobile-app/context/AppContext.tsx +++ b/apps/mobile-app/context/AppContext.tsx @@ -18,14 +18,9 @@ type AppContextType = { setAuthTokens: (username: string, accessToken: string, refreshToken: string) => Promise; login: () => Promise; // Auth methods from AuthContext - getEnabledAuthMethods: () => Promise; - isBiometricsEnabled: () => Promise; setAuthMethods: (methods: AuthMethod[]) => Promise; - getAuthMethodDisplayKey: () => Promise; getAutoLockTimeout: () => Promise; setAutoLockTimeout: (timeout: number) => Promise; - getBiometricDisplayName: () => Promise; - isBiometricsEnabledOnDevice: () => Promise; setOfflineMode: (isOffline: boolean) => void; verifyPassword: (password: string) => Promise; getEncryptionKeyDerivationParams: () => Promise<{ salt: string; encryptionType: string; encryptionSettings: string } | null>; @@ -108,14 +103,9 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children setAuthTokens: auth.setAuthTokens, login: auth.login, // Pass through other auth methods - getEnabledAuthMethods: auth.getEnabledAuthMethods, - isBiometricsEnabled: auth.isBiometricsEnabled, setAuthMethods: auth.setAuthMethods, - getAuthMethodDisplayKey: auth.getAuthMethodDisplayKey, getAutoLockTimeout: auth.getAutoLockTimeout, setAutoLockTimeout: auth.setAutoLockTimeout, - getBiometricDisplayName: auth.getBiometricDisplayName, - isBiometricsEnabledOnDevice: auth.isBiometricsEnabledOnDevice, setOfflineMode: auth.setOfflineMode, verifyPassword: auth.verifyPassword, getEncryptionKeyDerivationParams: auth.getEncryptionKeyDerivationParams, @@ -129,14 +119,9 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children auth.initializeAuth, auth.setAuthTokens, auth.login, - auth.getEnabledAuthMethods, - auth.isBiometricsEnabled, auth.setAuthMethods, - auth.getAuthMethodDisplayKey, auth.getAutoLockTimeout, auth.setAutoLockTimeout, - auth.getBiometricDisplayName, - auth.isBiometricsEnabledOnDevice, auth.setOfflineMode, auth.verifyPassword, auth.getEncryptionKeyDerivationParams, diff --git a/apps/mobile-app/context/AuthContext.tsx b/apps/mobile-app/context/AuthContext.tsx index 1496b1853..6cac7a39d 100644 --- a/apps/mobile-app/context/AuthContext.tsx +++ b/apps/mobile-app/context/AuthContext.tsx @@ -2,10 +2,9 @@ import { Buffer } from 'buffer'; 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 { Platform } from 'react-native'; import EncryptionUtility from '@/utils/EncryptionUtility'; +import type { AuthMethod } from '@/utils/AppUnlockUtility'; import { useDb } from '@/context/DbContext'; import { dialogEventEmitter } from '@/events/DialogEventEmitter'; @@ -13,18 +12,14 @@ import NativeVaultManager from '@/specs/NativeVaultManager'; import i18n from '@/i18n'; import { LocalPreferencesService } from '@/services/LocalPreferencesService'; -// Create a navigation reference export const navigationRef = React.createRef>(); - -export type AuthMethod = 'faceid' | 'password'; +export type { AuthMethod } from '@/utils/AppUnlockUtility'; type AuthContextType = { isLoggedIn: boolean; isInitialized: boolean; username: string | null; isOffline: boolean; - getEnabledAuthMethods: () => Promise; - isBiometricsEnabled: () => Promise; setAuthTokens: (username: string, accessToken: string, refreshToken: string) => Promise; initializeAuth: () => Promise<{ isLoggedIn: boolean; enabledAuthMethods: AuthMethod[] }>; login: () => Promise; @@ -39,11 +34,8 @@ type AuthContextType = { */ clearAuthForced: (errorMessage?: string) => Promise; setAuthMethods: (methods: AuthMethod[]) => Promise; - getAuthMethodDisplayKey: () => Promise; getAutoLockTimeout: () => Promise; setAutoLockTimeout: (timeout: number) => Promise; - getBiometricDisplayName: () => Promise; - isBiometricsEnabledOnDevice: () => Promise; setOfflineMode: (isOffline: boolean) => void; verifyPassword: (password: string) => Promise; getEncryptionKeyDerivationParams: () => Promise<{ salt: string; encryptionType: string; encryptionSettings: string } | null>; @@ -71,52 +63,6 @@ export const AuthProvider: React.FC<{ const [isOffline, setIsOffline] = useState(false); const dbContext = useDb(); - /** - * Get enabled auth methods from the native module - */ - const getEnabledAuthMethods = useCallback(async (): Promise => { - try { - let methods = await NativeVaultManager.getAuthMethods() as AuthMethod[]; - // Check if Face ID is actually available despite being enabled - if (methods.includes('faceid')) { - const isEnrolled = await LocalAuthentication.isEnrolledAsync(); - if (!isEnrolled) { - // Remove Face ID from the list of enabled auth methods - methods = methods.filter(method => method !== 'faceid'); - } - } - return methods; - } catch (error) { - console.error('Failed to get enabled auth methods:', error); - return ['password']; - } - }, []); - - /** - * Check if biometrics is enabled on the device (regardless of whether it's enabled in the AliasVault app). - */ - const isBiometricsEnabledOnDevice = useCallback(async (): Promise => { - const hasBiometrics = await LocalAuthentication.hasHardwareAsync(); - if (!hasBiometrics) { - return false; - } - - return await LocalAuthentication.isEnrolledAsync(); - }, []); - - /** - * Check if biometrics is enabled based on enabled auth methods - */ - const isBiometricsEnabled = useCallback(async (): Promise => { - const deviceHasBiometrics = await isBiometricsEnabledOnDevice(); - if (!deviceHasBiometrics) { - return false; - } - - const methods = await getEnabledAuthMethods(); - return methods.includes('faceid'); - }, [getEnabledAuthMethods, isBiometricsEnabledOnDevice]); - /** * Set auth tokens in storage as part of the login process. After db is initialized, the login method should be called as well. */ @@ -149,7 +95,8 @@ export const AuthProvider: React.FC<{ setUsername(username); setIsLoggedIn(true); isAuthenticated = true; - methods = await getEnabledAuthMethods(); + const { AppUnlockUtility } = await import('@/utils/AppUnlockUtility'); + methods = await AppUnlockUtility.getEnabledAuthMethods(); } const offline = await NativeVaultManager.getOfflineMode(); @@ -157,7 +104,7 @@ export const AuthProvider: React.FC<{ setIsInitialized(true); setIsOffline(offline); return { isLoggedIn: isAuthenticated, enabledAuthMethods: methods }; - }, [getEnabledAuthMethods]); + }, []); /** * Sync legacy config to native layer @@ -248,83 +195,14 @@ export const AuthProvider: React.FC<{ }, [dbContext, clearAuthForced]); /** - * Set the authentication methods and save them to storage + * Set the authentication methods and save them to storage. + * Delegates to AppUnlockUtility for consistent auth method management. */ const setAuthMethods = useCallback(async (methods: AuthMethod[]): Promise => { - // Ensure password is always included - const methodsToSave = methods.includes('password') ? methods : [...methods, 'password']; - - // Update native credentials manager - try { - await NativeVaultManager.setAuthMethods(methodsToSave); - } catch (error) { - console.error('Failed to update native auth methods:', error); - } + const { AppUnlockUtility } = await import('@/utils/AppUnlockUtility'); + await AppUnlockUtility.setAuthMethods(methods); }, []); - /** - * Get the appropriate biometric display name translation key based on device capabilities - */ - const getBiometricDisplayName = useCallback(async (): Promise => { - try { - const hasBiometrics = await LocalAuthentication.hasHardwareAsync(); - const enrolled = await LocalAuthentication.isEnrolledAsync(); - - // For Android, we use the term "Biometrics" for facial recognition and fingerprint. - if (Platform.OS === 'android') { - return i18n.t('settings.vaultUnlockSettings.biometrics'); - } - - // For iOS, we check if the device has explicit Face ID or Touch ID support. - if (!hasBiometrics || !enrolled) { - return i18n.t('settings.vaultUnlockSettings.faceIdTouchId'); - } - - const types = await LocalAuthentication.supportedAuthenticationTypesAsync(); - const hasFaceIDSupport = types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION); - const hasTouchIDSupport = types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT); - - if (hasFaceIDSupport) { - return i18n.t('settings.vaultUnlockSettings.faceId'); - } else if (hasTouchIDSupport) { - return i18n.t('settings.vaultUnlockSettings.touchId'); - } - - return i18n.t('settings.vaultUnlockSettings.faceIdTouchId'); - } catch (error) { - console.error('Failed to get biometric display name:', error); - return i18n.t('settings.vaultUnlockSettings.faceIdTouchId'); - } - }, []); - - /** - * Get the display label translation key for the current auth method - * Priority: Biometrics > PIN > Password - */ - const getAuthMethodDisplayKey = useCallback(async (): Promise => { - try { - // Check for biometrics first (highest priority) - const methods = await getEnabledAuthMethods(); - if (methods.includes('faceid')) { - if (await isBiometricsEnabledOnDevice()) { - return await getBiometricDisplayName(); - } - } - - // Check for PIN (second priority) - const pinEnabled = await NativeVaultManager.isPinEnabled(); - if (pinEnabled) { - return 'settings.vaultUnlockSettings.pin'; - } - - // Fallback to password - return 'items.password'; - } catch (error) { - console.error('Failed to get auth method display key:', error); - return 'items.password'; - } - }, [getEnabledAuthMethods, getBiometricDisplayName, isBiometricsEnabledOnDevice]); - /** * Get the auto-lock timeout from the iOS credentials manager */ @@ -433,19 +311,14 @@ export const AuthProvider: React.FC<{ username, shouldShowAutofillReminder, isOffline, - getEnabledAuthMethods, - isBiometricsEnabled, setAuthTokens, initializeAuth, login, clearAuthUserInitiated, clearAuthForced, setAuthMethods, - getAuthMethodDisplayKey, - isBiometricsEnabledOnDevice, getAutoLockTimeout, setAutoLockTimeout, - getBiometricDisplayName, markAutofillConfigured, verifyPassword, getEncryptionKeyDerivationParams, @@ -456,19 +329,14 @@ export const AuthProvider: React.FC<{ username, shouldShowAutofillReminder, isOffline, - getEnabledAuthMethods, - isBiometricsEnabled, setAuthTokens, initializeAuth, login, clearAuthUserInitiated, clearAuthForced, setAuthMethods, - getAuthMethodDisplayKey, - isBiometricsEnabledOnDevice, getAutoLockTimeout, setAutoLockTimeout, - getBiometricDisplayName, markAutofillConfigured, verifyPassword, getEncryptionKeyDerivationParams, diff --git a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm index 69b60735e..bcda5625d 100644 --- a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm +++ b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm @@ -273,6 +273,10 @@ [vaultManager isKeystoreAvailable:resolve rejecter:reject]; } +- (void)isBiometricUnlockAvailable:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [vaultManager isBiometricUnlockAvailable:resolve rejecter:reject]; +} + - (void)removeAndDisablePin:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [vaultManager removeAndDisablePin:resolve rejecter:reject]; } diff --git a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift index 7c19aa06c..aa9f79bf5 100644 --- a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift +++ b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift @@ -732,6 +732,16 @@ public class VaultManager: NSObject { resolve(vaultStore.isKeystoreAvailable()) } + /// Check if biometric unlock is actually available (device + key validation). + /// This checks not only if biometrics are configured in auth methods, + /// but also validates that the encryption key in Keychain is valid. + /// Returns false if key has been invalidated (e.g., biometric enrollment changed). + @objc + func isBiometricUnlockAvailable(_ resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + resolve(vaultStore.isBiometricAuthEnabled()) + } + @objc func getPinFailedAttempts(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { diff --git a/apps/mobile-app/specs/NativeVaultManager.ts b/apps/mobile-app/specs/NativeVaultManager.ts index 149d9a005..93ed77b0d 100644 --- a/apps/mobile-app/specs/NativeVaultManager.ts +++ b/apps/mobile-app/specs/NativeVaultManager.ts @@ -94,6 +94,12 @@ export interface Spec extends TurboModule { // PIN unlock methods isPinEnabled(): Promise; isKeystoreAvailable(): Promise; + + // Biometric unlock validation - checks if biometric unlock is actually available + // Returns true only if device supports biometrics AND the encryption key is valid + // Returns false if key has been invalidated (e.g., biometric enrollment changed) + isBiometricUnlockAvailable(): Promise; + removeAndDisablePin(): Promise; showPinUnlock(): Promise; showPinSetup(): Promise; diff --git a/apps/mobile-app/utils/AppUnlockUtility.ts b/apps/mobile-app/utils/AppUnlockUtility.ts new file mode 100644 index 000000000..6f5083adb --- /dev/null +++ b/apps/mobile-app/utils/AppUnlockUtility.ts @@ -0,0 +1,216 @@ +import { Platform } from 'react-native'; +import * as LocalAuthentication from 'expo-local-authentication'; +import NativeVaultManager from '@/specs/NativeVaultManager'; +import i18n from '@/i18n'; + +export type AuthMethod = 'faceid' | 'password'; + +/** + * Comprehensive utility for all app unlock-related functionality. + * Centralizes biometric availability checks, display name logic, auth method management, + * and unlock configuration. + */ +export class AppUnlockUtility { + /** + * Get enabled auth methods from the native module. + * Filters out Face ID if biometrics are not enrolled. + */ + static async getEnabledAuthMethods(): Promise { + try { + let methods = await NativeVaultManager.getAuthMethods() as AuthMethod[]; + // Check if Face ID is actually available despite being enabled + if (methods.includes('faceid')) { + const isEnrolled = await LocalAuthentication.isEnrolledAsync(); + if (!isEnrolled) { + // Remove Face ID from the list of enabled auth methods + methods = methods.filter(method => method !== 'faceid'); + } + } + return methods; + } catch (error) { + console.error('Failed to get enabled auth methods:', error); + return ['password']; + } + } + + /** + * Set the authentication methods and save them to native storage. + * Always ensures password is included as a fallback. + */ + static async setAuthMethods(methods: AuthMethod[]): Promise { + // Ensure password is always included + const methodsToSave = methods.includes('password') ? methods : [...methods, 'password']; + + // Update native credentials manager + try { + await NativeVaultManager.setAuthMethods(methodsToSave); + } catch (error) { + console.error('Failed to update native auth methods:', error); + throw error; + } + } + + /** + * Enable an authentication method by adding it to the current auth methods. + * This handles getting current methods, adding the new one, and saving. + */ + static async enableAuthMethod(method: AuthMethod): Promise { + try { + const currentMethods = await this.getEnabledAuthMethods(); + if (!currentMethods.includes(method)) { + await this.setAuthMethods([...currentMethods, method]); + } + } catch (error) { + console.error(`Failed to enable auth method ${method}:`, error); + throw error; + } + } + + /** + * Disable an authentication method by removing it from the current auth methods. + * This handles getting current methods, removing the specified one, and saving. + * Password cannot be disabled as it's always required as a fallback. + */ + static async disableAuthMethod(method: AuthMethod): Promise { + if (method === 'password') { + console.warn('Cannot disable password auth method - it is always required'); + return; + } + + try { + const currentMethods = await this.getEnabledAuthMethods(); + const updatedMethods = currentMethods.filter(m => m !== method); + await this.setAuthMethods(updatedMethods); + } catch (error) { + console.error(`Failed to disable auth method ${method}:`, error); + throw error; + } + } + /** + * Check if biometrics are available on the device (hardware + enrollment). + * This only checks device capabilities, not key validity. + */ + static async isBiometricsAvailableOnDevice(): Promise { + try { + const hasHardware = await LocalAuthentication.hasHardwareAsync(); + if (!hasHardware) { + return false; + } + + return await LocalAuthentication.isEnrolledAsync(); + } catch (error) { + console.error('Error checking biometric device availability:', error); + return false; + } + } + + /** + * Check if biometric unlock is actually available and functional. + * This performs comprehensive validation: + * - Device has biometric hardware + * - Biometrics are enrolled + * - Biometrics are enabled in auth methods + * - Encryption key in native KeyStore/Keychain is valid + * + * Returns false if the key has been invalidated (e.g., biometric enrollment changed). + * Use this method when determining whether to show biometric unlock UI. + * + * IMPORTANT: If the key is invalid but 'faceid' is still in auth methods, + * this method will automatically remove it to keep state consistent. + */ + static async isBiometricUnlockAvailable(): Promise { + try { + // First check device capabilities + const deviceAvailable = await this.isBiometricsAvailableOnDevice(); + if (!deviceAvailable) { + return false; + } + + // Then validate that biometric unlock is actually functional + // This checks auth methods AND validates the encryption key + const isAvailable = await NativeVaultManager.isBiometricUnlockAvailable(); + + // If biometric unlock is NOT available but 'faceid' is still in auth methods, + // automatically remove it to keep state consistent + if (!isAvailable) { + const currentMethods = await this.getEnabledAuthMethods(); + if (currentMethods.includes('faceid')) { + console.log('Biometric key invalid but faceid still in auth methods - cleaning up'); + const methodsWithoutBiometric = currentMethods.filter(m => m !== 'faceid'); + await this.setAuthMethods(methodsWithoutBiometric); + } + } + + return isAvailable; + } catch (error) { + console.error('Error checking biometric unlock availability:', error); + return false; + } + } + + /** + * Get the appropriate biometric display name translation key based on device capabilities. + * Returns localization keys like 'settings.vaultUnlockSettings.faceId', 'biometrics', etc. + */ + static async getBiometricDisplayName(): Promise { + try { + const hasBiometrics = await LocalAuthentication.hasHardwareAsync(); + const enrolled = await LocalAuthentication.isEnrolledAsync(); + + // For Android, we use the term "Biometrics" for facial recognition and fingerprint. + if (Platform.OS === 'android') { + return i18n.t('settings.vaultUnlockSettings.biometrics'); + } + + // For iOS, we check if the device has explicit Face ID or Touch ID support. + if (!hasBiometrics || !enrolled) { + return i18n.t('settings.vaultUnlockSettings.faceIdTouchId'); + } + + const types = await LocalAuthentication.supportedAuthenticationTypesAsync(); + const hasFaceIDSupport = types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION); + const hasTouchIDSupport = types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT); + + if (hasFaceIDSupport) { + return i18n.t('settings.vaultUnlockSettings.faceId'); + } else if (hasTouchIDSupport) { + return i18n.t('settings.vaultUnlockSettings.touchId'); + } + + return i18n.t('settings.vaultUnlockSettings.faceIdTouchId'); + } catch (error) { + console.error('Failed to get biometric display name:', error); + return i18n.t('settings.vaultUnlockSettings.faceIdTouchId'); + } + } + + /** + * Get the display label translation key for the current auth method. + * Priority: Biometrics > PIN > Password + * + * @returns Translation key for the primary unlock method + */ + static async getAuthMethodDisplayKey(): Promise { + try { + // Check for biometrics first (highest priority) + const methods = await this.getEnabledAuthMethods(); + if (methods.includes('faceid')) { + if (await this.isBiometricUnlockAvailable()) { + return await this.getBiometricDisplayName(); + } + } + + // Check for PIN (second priority) + const pinEnabled = await NativeVaultManager.isPinEnabled(); + if (pinEnabled) { + return 'settings.vaultUnlockSettings.pin'; + } + + // Fallback to password + return 'items.password'; + } catch (error) { + console.error('Failed to get auth method display key:', error); + return 'items.password'; + } + } +} From ae8c995d16fc69bfaf5f53d9304531e0484c9fd2 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 5 Mar 2026 16:13:58 +0100 Subject: [PATCH 06/26] Refactor haptics usage to shared utility (#1812) --- apps/mobile-app/app/(tabs)/emails/index.tsx | 10 ++-- apps/mobile-app/app/(tabs)/items/add-edit.tsx | 14 ++---- .../app/(tabs)/items/folder/[id].tsx | 8 ++-- apps/mobile-app/app/(tabs)/items/index.tsx | 8 ++-- .../settings/security/active-sessions.tsx | 13 ++--- .../(tabs)/settings/security/auth-logs.tsx | 9 ++-- apps/mobile-app/app/unlock.tsx | 28 +++-------- .../components/folders/FolderModal.tsx | 7 ++- .../components/form/AdvancedPasswordField.tsx | 8 ++-- .../form/DraggableCustomFieldsList.tsx | 3 +- .../form/FormInputCopyToClipboard.tsx | 8 ++-- apps/mobile-app/utils/HapticsUtility.ts | 47 +++++++++++++++++++ 12 files changed, 88 insertions(+), 75 deletions(-) create mode 100644 apps/mobile-app/utils/HapticsUtility.ts diff --git a/apps/mobile-app/app/(tabs)/emails/index.tsx b/apps/mobile-app/app/(tabs)/emails/index.tsx index 1dd9190da..63f98f2ab 100644 --- a/apps/mobile-app/app/(tabs)/emails/index.tsx +++ b/apps/mobile-app/app/(tabs)/emails/index.tsx @@ -1,14 +1,14 @@ -import * as Haptics from 'expo-haptics'; import { useNavigation } from 'expo-router'; import React, { useEffect, useState, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { StyleSheet, View, ScrollView, RefreshControl, Animated , Platform } from 'react-native'; +import { StyleSheet, Platform, View, ScrollView, RefreshControl, Animated } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Toast from 'react-native-toast-message'; import type { MailboxBulkRequest, MailboxBulkResponse, MailboxEmail } from '@/utils/dist/core/models/webapi'; import EncryptionUtility from '@/utils/EncryptionUtility'; import emitter from '@/utils/EventEmitter'; +import { HapticsUtility } from '@/utils/HapticsUtility'; import { useColors } from '@/hooks/useColorScheme'; import { useMinDurationLoading } from '@/hooks/useMinDurationLoading'; @@ -145,11 +145,7 @@ export default function EmailsScreen() : React.ReactNode { */ const onRefresh = useCallback(async () : Promise => { // Trigger haptic feedback when pull-to-refresh is activated - if (Platform.OS === 'ios') { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } else if (Platform.OS === 'android') { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } + HapticsUtility.impact(); setIsLoading(true); setIsRefreshing(true); diff --git a/apps/mobile-app/app/(tabs)/items/add-edit.tsx b/apps/mobile-app/app/(tabs)/items/add-edit.tsx index 28f171453..30edb2d5c 100644 --- a/apps/mobile-app/app/(tabs)/items/add-edit.tsx +++ b/apps/mobile-app/app/(tabs)/items/add-edit.tsx @@ -9,6 +9,8 @@ import { useTranslation } from 'react-i18next'; import { StyleSheet, View, Keyboard, Platform, ScrollView, KeyboardAvoidingView, TouchableOpacity } from 'react-native'; import Toast from 'react-native-toast-message'; +import { HapticsUtility } from '@/utils/HapticsUtility'; + import type { Folder } from '@/utils/db/repositories/FolderRepository'; import { CreateIdentityGenerator, CreateUsernameEmailGenerator, UsernameEmailGenerator, Gender, Identity, IdentityHelperUtils, convertAgeRangeToBirthdateOptions } from '@/utils/dist/core/identity-generator'; import type { Attachment, Item, ItemField, TotpCode, ItemType, FieldType, PasswordSettings } from '@/utils/dist/core/models/vault'; @@ -671,9 +673,7 @@ export default function AddEditItemScreen(): React.ReactNode { Fields: [] }); - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } + HapticsUtility.impact(); }, [item, isEditMode]); /** @@ -915,9 +915,7 @@ export default function AddEditItemScreen(): React.ReactNode { setIsSaveDisabled(false); // Haptic feedback for successful save - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - } + HapticsUtility.notification(Haptics.NotificationFeedbackType.Success); // Navigate immediately - sync continues in background if (itemUrl && !isEditMode) { @@ -982,9 +980,7 @@ export default function AddEditItemScreen(): React.ReactNode { emitter.emit('credentialChanged', id); // Haptic feedback for delete action (warning type for destructive action) - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); - } + HapticsUtility.notification(Haptics.NotificationFeedbackType.Warning); setTimeout(() => { Toast.show({ diff --git a/apps/mobile-app/app/(tabs)/items/folder/[id].tsx b/apps/mobile-app/app/(tabs)/items/folder/[id].tsx index 5fb3657fd..fb8e0e08e 100644 --- a/apps/mobile-app/app/(tabs)/items/folder/[id].tsx +++ b/apps/mobile-app/app/(tabs)/items/folder/[id].tsx @@ -1,6 +1,5 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { useNavigation } from '@react-navigation/native'; -import * as Haptics from 'expo-haptics'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,6 +12,7 @@ import type { CredentialSortOrder } from '@/utils/db/repositories/SettingsReposi import type { Item, ItemType } from '@/utils/dist/core/models/vault'; import { getFieldValue, FieldKey, ItemTypes } from '@/utils/dist/core/models/vault'; import emitter from '@/utils/EventEmitter'; +import { HapticsUtility } from '@/utils/HapticsUtility'; import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError'; import { useColors } from '@/hooks/useColorScheme'; @@ -227,9 +227,7 @@ export default function FolderViewScreen(): React.ReactNode { * Handle pull-to-refresh. */ const onRefresh = useCallback(async () => { - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } + HapticsUtility.impact(); setRefreshing(true); setIsLoadingItems(true); @@ -404,7 +402,7 @@ export default function FolderViewScreen(): React.ReactNode { const handleAddItem = useCallback(() => { navigate(() => { router.push(`/(tabs)/items/add-edit?folderId=${folderId}` as '/(tabs)/items/add-edit'); - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + HapticsUtility.impact(); }); }, [folderId, router, navigate]); diff --git a/apps/mobile-app/app/(tabs)/items/index.tsx b/apps/mobile-app/app/(tabs)/items/index.tsx index dc724fe5f..78235b610 100644 --- a/apps/mobile-app/app/(tabs)/items/index.tsx +++ b/apps/mobile-app/app/(tabs)/items/index.tsx @@ -1,6 +1,5 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { useNavigation } from '@react-navigation/native'; -import * as Haptics from 'expo-haptics'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,6 +12,7 @@ import type { CredentialSortOrder } from '@/utils/db/repositories/SettingsReposi import type { Item, ItemType } from '@/utils/dist/core/models/vault'; import { getFieldValue, FieldKey, ItemTypes } from '@/utils/dist/core/models/vault'; import emitter from '@/utils/EventEmitter'; +import { HapticsUtility } from '@/utils/HapticsUtility'; import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError'; import { useColors } from '@/hooks/useColorScheme'; @@ -367,9 +367,7 @@ export default function ItemsScreen(): React.ReactNode { * Handle pull-to-refresh. */ const onRefresh = useCallback(async () => { - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } + HapticsUtility.impact(); setRefreshing(true); setIsLoadingItems(true); @@ -549,7 +547,7 @@ export default function ItemsScreen(): React.ReactNode { } else { router.push('/(tabs)/items/add-edit'); } - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + HapticsUtility.impact(); }); }, [router, searchQuery, navigate]); diff --git a/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx b/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx index 429fc88ef..4a68c7e36 100644 --- a/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx +++ b/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx @@ -1,9 +1,10 @@ -import * as Haptics from 'expo-haptics'; import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { StyleSheet, View, TouchableOpacity, RefreshControl, Platform } from 'react-native'; +import { StyleSheet, View, TouchableOpacity, RefreshControl } from 'react-native'; import Toast from 'react-native-toast-message'; +import { HapticsUtility } from '@/utils/HapticsUtility'; + import type { RefreshToken } from '@/utils/dist/core/models/webapi'; import { useColors } from '@/hooks/useColorScheme'; @@ -136,9 +137,7 @@ export default function ActiveSessionsScreen() : React.ReactNode { */ const onRefresh = async () : Promise => { // Trigger haptic feedback when pull-to-refresh is activated - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } + HapticsUtility.impact(); setIsRefreshing(true); await loadSessions(); @@ -175,9 +174,7 @@ export default function ActiveSessionsScreen() : React.ReactNode { }); // Trigger haptic feedback when toggling - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } + HapticsUtility.impact(); }; return ( diff --git a/apps/mobile-app/app/(tabs)/settings/security/auth-logs.tsx b/apps/mobile-app/app/(tabs)/settings/security/auth-logs.tsx index 87cee8451..892de1a35 100644 --- a/apps/mobile-app/app/(tabs)/settings/security/auth-logs.tsx +++ b/apps/mobile-app/app/(tabs)/settings/security/auth-logs.tsx @@ -1,9 +1,10 @@ -import * as Haptics from 'expo-haptics'; import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { StyleSheet, View, RefreshControl, Platform } from 'react-native'; +import { StyleSheet, View, RefreshControl } from 'react-native'; import Toast from 'react-native-toast-message'; +import { HapticsUtility } from '@/utils/HapticsUtility'; + import type { AuthLogModel } from '@/utils/dist/core/models/webapi'; import { AuthEventType } from '@/utils/dist/core/models/webapi'; @@ -109,9 +110,7 @@ export default function AuthLogsScreen() : React.ReactNode { */ const onRefresh = async () : Promise => { // Trigger haptic feedback when pull-to-refresh is activated - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } + HapticsUtility.impact(); setIsRefreshing(true); await loadLogs(); diff --git a/apps/mobile-app/app/unlock.tsx b/apps/mobile-app/app/unlock.tsx index 1bc79d066..9c7f6fbea 100644 --- a/apps/mobile-app/app/unlock.tsx +++ b/apps/mobile-app/app/unlock.tsx @@ -124,9 +124,7 @@ export default function UnlockScreen() : React.ReactNode { } // Haptic feedback for successful unlock - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - } + HapticsUtility.notification(Haptics.NotificationFeedbackType.Success); router.replace('/reinitialize'); } catch (err) { @@ -139,9 +137,7 @@ export default function UnlockScreen() : React.ReactNode { const errorCode = getAppErrorCode(err); // Haptic feedback for authentication error - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); - } + HapticsUtility.notification(Haptics.NotificationFeedbackType.Error); if (!errorCode || errorCode === AppErrorCode.VAULT_DECRYPT_FAILED) { setError(t('auth.errors.incorrectPassword')); @@ -204,9 +200,7 @@ export default function UnlockScreen() : React.ReactNode { } // Haptic feedback for successful unlock - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - } + HapticsUtility.notification(Haptics.NotificationFeedbackType.Success); /* * Navigate to reinitialize which will sync vault with server @@ -224,9 +218,7 @@ export default function UnlockScreen() : React.ReactNode { const errorCode = getAppErrorCode(err); // Haptic feedback for authentication error - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); - } + HapticsUtility.notification(Haptics.NotificationFeedbackType.Error); /* * During unlock, VAULT_DECRYPT_FAILED indicates wrong password. @@ -263,9 +255,7 @@ export default function UnlockScreen() : React.ReactNode { } // Haptic feedback for successful unlock - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - } + HapticsUtility.notification(Haptics.NotificationFeedbackType.Success); router.replace('/reinitialize'); return true; @@ -301,9 +291,7 @@ export default function UnlockScreen() : React.ReactNode { } // Haptic feedback for successful unlock - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - } + HapticsUtility.notification(Haptics.NotificationFeedbackType.Success); router.replace('/reinitialize'); } catch (err) { @@ -321,9 +309,7 @@ export default function UnlockScreen() : React.ReactNode { await performPinUnlock(); } else if (errorCode) { // Haptic feedback for authentication error - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); - } + HapticsUtility.notification(Haptics.NotificationFeedbackType.Error); // Show the error with code if no PIN fallback const translationKey = getErrorTranslationKey(errorCode); diff --git a/apps/mobile-app/components/folders/FolderModal.tsx b/apps/mobile-app/components/folders/FolderModal.tsx index 0d081004d..aa808062a 100644 --- a/apps/mobile-app/components/folders/FolderModal.tsx +++ b/apps/mobile-app/components/folders/FolderModal.tsx @@ -8,9 +8,10 @@ import { TouchableOpacity, View, ActivityIndicator, - Platform, } from 'react-native'; +import { HapticsUtility } from '@/utils/HapticsUtility'; + import { useColors } from '@/hooks/useColorScheme'; import { ModalWrapper } from '@/components/common/ModalWrapper'; @@ -62,9 +63,7 @@ export const FolderModal: React.FC = ({ await onSave(trimmedName); // Haptic feedback for successful folder creation/edit - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - } + HapticsUtility.notification(Haptics.NotificationFeedbackType.Success); onClose(); } catch (err) { diff --git a/apps/mobile-app/components/form/AdvancedPasswordField.tsx b/apps/mobile-app/components/form/AdvancedPasswordField.tsx index 4eb88511a..63d63ed4c 100644 --- a/apps/mobile-app/components/form/AdvancedPasswordField.tsx +++ b/apps/mobile-app/components/form/AdvancedPasswordField.tsx @@ -1,12 +1,12 @@ import { MaterialIcons } from '@expo/vector-icons'; import Slider from '@react-native-community/slider'; -import * as Haptics from 'expo-haptics'; import React, { forwardRef, useImperativeHandle, useMemo, useRef, useState, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { View, TextInput, TextInputProps, StyleSheet, TouchableOpacity, Platform, ScrollView, Switch } from 'react-native'; +import { View, TextInput, TextInputProps, StyleSheet, Platform, TouchableOpacity, ScrollView, Switch } from 'react-native'; import type { PasswordSettings } from '@/utils/dist/core/models/vault'; import { CreatePasswordGenerator } from '@/utils/dist/core/password-generator'; +import { HapticsUtility } from '@/utils/HapticsUtility'; import { sliderToLength, lengthToSlider, SLIDER_MIN, SLIDER_MAX } from '@/utils/passwordLengthSlider'; import { useColors } from '@/hooks/useColorScheme'; @@ -145,9 +145,7 @@ const AdvancedPasswordFieldComponent = forwardRef * Handle drag begin */ const handleDragBegin = useCallback(() => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + HapticsUtility.impact(Haptics.ImpactFeedbackStyle.Medium); }, []); /** diff --git a/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx b/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx index f6d7d5567..1116d3733 100644 --- a/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx +++ b/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx @@ -1,11 +1,11 @@ import { MaterialIcons } from '@expo/vector-icons'; -import * as Haptics from 'expo-haptics'; import React, { useState, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { View, Text, TouchableOpacity, StyleSheet, Platform, Animated, Easing } from 'react-native'; +import { View, Text, Platform, TouchableOpacity, StyleSheet, Animated, Easing } from 'react-native'; import Toast from 'react-native-toast-message'; import { copyToClipboardWithExpiration } from '@/utils/ClipboardUtility'; +import { HapticsUtility } from '@/utils/HapticsUtility'; import { useColors } from '@/hooks/useColorScheme'; @@ -107,9 +107,7 @@ const FormInputCopyToClipboard: React.FC = ({ await copyToClipboardWithExpiration(value, timeoutSeconds); // Haptic feedback for successful copy - if (Platform.OS === 'ios' || Platform.OS === 'android') { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - } + HapticsUtility.impact(); // Handle animation state if (timeoutSeconds > 0) { diff --git a/apps/mobile-app/utils/HapticsUtility.ts b/apps/mobile-app/utils/HapticsUtility.ts new file mode 100644 index 000000000..128add6d4 --- /dev/null +++ b/apps/mobile-app/utils/HapticsUtility.ts @@ -0,0 +1,47 @@ +import * as Haptics from 'expo-haptics'; +import { Platform } from 'react-native'; + +/** + * Utility class for managing haptic feedback across the app. + * Provides a centralized way to trigger haptic feedback with platform checks. + */ +export class HapticsUtility { + /** + * Checks if the current platform supports haptics. + * @returns true if platform is iOS or Android, false otherwise + */ + private static isHapticsAvailable(): boolean { + return Platform.OS === 'ios' || Platform.OS === 'android'; + } + + /** + * Triggers impact haptic feedback (for button presses, toggles, etc.) + * Automatically checks if the platform supports haptics (iOS/Android). + * + * @param style - The style of impact feedback (Light, Medium, Heavy, Rigid, Soft) + */ + static impact(style: Haptics.ImpactFeedbackStyle = Haptics.ImpactFeedbackStyle.Light): void { + if (!this.isHapticsAvailable()) return; + Haptics.impactAsync(style); + } + + /** + * Triggers notification haptic feedback (for success, error, warning states). + * Automatically checks if the platform supports haptics (iOS/Android). + * + * @param type - The type of notification feedback (Success, Warning, Error) + */ + static notification(type: Haptics.NotificationFeedbackType): void { + if (!this.isHapticsAvailable()) return; + Haptics.notificationAsync(type); + } + + /** + * Triggers selection haptic feedback (for picker scrolls, slider movements, etc.) + * Automatically checks if the platform supports haptics (iOS/Android). + */ + static selection(): void { + if (!this.isHapticsAvailable()) return; + Haptics.selectionAsync(); + } +} From 5aa7ab1c4b0ab419e7051fdc87d9e197ac10485f Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 5 Mar 2026 16:19:17 +0100 Subject: [PATCH 07/26] Update confirm dialog to show text in center when it wraps due to translations (#1812) --- apps/mobile-app/components/common/ConfirmDialog.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/mobile-app/components/common/ConfirmDialog.tsx b/apps/mobile-app/components/common/ConfirmDialog.tsx index 7e01a1a69..984a1096f 100644 --- a/apps/mobile-app/components/common/ConfirmDialog.tsx +++ b/apps/mobile-app/components/common/ConfirmDialog.tsx @@ -187,6 +187,7 @@ export const ConfirmDialog: React.FC = ({ gap: 8, justifyContent: 'center', paddingVertical: 12, + paddingHorizontal: 8, }, buttonDisabled: { opacity: 0.6, @@ -194,6 +195,8 @@ export const ConfirmDialog: React.FC = ({ buttonText: { fontSize: 16, fontWeight: '500', + textAlign: 'center', + flexShrink: 1, }, }); From f947dd77421923d88f462981dfd1bc5b7128a2de Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 5 Mar 2026 16:47:56 +0100 Subject: [PATCH 08/26] Update linting (#1812) --- apps/mobile-app/app/(tabs)/items/add-edit.tsx | 3 +-- .../app/(tabs)/settings/security/active-sessions.tsx | 3 +-- apps/mobile-app/app/(tabs)/settings/security/auth-logs.tsx | 3 +-- apps/mobile-app/app/unlock.tsx | 1 + 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/mobile-app/app/(tabs)/items/add-edit.tsx b/apps/mobile-app/app/(tabs)/items/add-edit.tsx index 30edb2d5c..4635adf52 100644 --- a/apps/mobile-app/app/(tabs)/items/add-edit.tsx +++ b/apps/mobile-app/app/(tabs)/items/add-edit.tsx @@ -9,8 +9,6 @@ import { useTranslation } from 'react-i18next'; import { StyleSheet, View, Keyboard, Platform, ScrollView, KeyboardAvoidingView, TouchableOpacity } from 'react-native'; import Toast from 'react-native-toast-message'; -import { HapticsUtility } from '@/utils/HapticsUtility'; - import type { Folder } from '@/utils/db/repositories/FolderRepository'; import { CreateIdentityGenerator, CreateUsernameEmailGenerator, UsernameEmailGenerator, Gender, Identity, IdentityHelperUtils, convertAgeRangeToBirthdateOptions } from '@/utils/dist/core/identity-generator'; import type { Attachment, Item, ItemField, TotpCode, ItemType, FieldType, PasswordSettings } from '@/utils/dist/core/models/vault'; @@ -18,6 +16,7 @@ import { ItemTypes, getSystemFieldsForItemType, getOptionalFieldsForItemType, is import type { FaviconExtractModel } from '@/utils/dist/core/models/webapi'; import { CreatePasswordGenerator, PasswordGenerator } from '@/utils/dist/core/password-generator'; import emitter from '@/utils/EventEmitter'; +import { HapticsUtility } from '@/utils/HapticsUtility'; import { extractServiceNameFromUrl } from '@/utils/UrlUtility'; import { useColors } from '@/hooks/useColorScheme'; diff --git a/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx b/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx index 4a68c7e36..9202a4bc2 100644 --- a/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx +++ b/apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx @@ -3,9 +3,8 @@ import { useTranslation } from 'react-i18next'; import { StyleSheet, View, TouchableOpacity, RefreshControl } from 'react-native'; import Toast from 'react-native-toast-message'; -import { HapticsUtility } from '@/utils/HapticsUtility'; - import type { RefreshToken } from '@/utils/dist/core/models/webapi'; +import { HapticsUtility } from '@/utils/HapticsUtility'; import { useColors } from '@/hooks/useColorScheme'; import { useMinDurationLoading } from '@/hooks/useMinDurationLoading'; diff --git a/apps/mobile-app/app/(tabs)/settings/security/auth-logs.tsx b/apps/mobile-app/app/(tabs)/settings/security/auth-logs.tsx index 892de1a35..de7a14830 100644 --- a/apps/mobile-app/app/(tabs)/settings/security/auth-logs.tsx +++ b/apps/mobile-app/app/(tabs)/settings/security/auth-logs.tsx @@ -3,10 +3,9 @@ import { useTranslation } from 'react-i18next'; import { StyleSheet, View, RefreshControl } from 'react-native'; import Toast from 'react-native-toast-message'; -import { HapticsUtility } from '@/utils/HapticsUtility'; - import type { AuthLogModel } from '@/utils/dist/core/models/webapi'; import { AuthEventType } from '@/utils/dist/core/models/webapi'; +import { HapticsUtility } from '@/utils/HapticsUtility'; import { useColors } from '@/hooks/useColorScheme'; import { useMinDurationLoading } from '@/hooks/useMinDurationLoading'; diff --git a/apps/mobile-app/app/unlock.tsx b/apps/mobile-app/app/unlock.tsx index 9c7f6fbea..4981928e6 100644 --- a/apps/mobile-app/app/unlock.tsx +++ b/apps/mobile-app/app/unlock.tsx @@ -4,6 +4,7 @@ import { router } from 'expo-router'; import { useState, useEffect, useCallback } from 'react'; import { StyleSheet, View, KeyboardAvoidingView, Platform, ScrollView, Dimensions, Text } from 'react-native'; +import { HapticsUtility } from '@/utils/HapticsUtility'; import { AppErrorCode, getAppErrorCode, getErrorTranslationKey, formatErrorWithCode } from '@/utils/types/errors/AppErrorCodes'; import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError'; From 4241a5f454a6f5c653fa17fb0093c2777c80a3e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:24:52 +0000 Subject: [PATCH 09/26] Bump the nuget group with 1 update Bumps MimeKit from 4.14.0 to 4.15.1 --- updated-dependencies: - dependency-name: MimeKit dependency-version: 4.15.1 dependency-type: direct:production dependency-group: nuget ... Signed-off-by: dependabot[bot] --- .../AliasVault.SmtpService/AliasVault.SmtpService.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/Services/AliasVault.SmtpService/AliasVault.SmtpService.csproj b/apps/server/Services/AliasVault.SmtpService/AliasVault.SmtpService.csproj index 2e8414dde..15c153ce0 100644 --- a/apps/server/Services/AliasVault.SmtpService/AliasVault.SmtpService.csproj +++ b/apps/server/Services/AliasVault.SmtpService/AliasVault.SmtpService.csproj @@ -27,7 +27,7 @@ - + From 54be022761bfaa1471899bad20d029a4e451bf39 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 6 Mar 2026 09:14:38 +0100 Subject: [PATCH 10/26] Update DatabaseMessageStore.cs --- .../AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs b/apps/server/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs index bfd696698..1428d3db4 100644 --- a/apps/server/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs +++ b/apps/server/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs @@ -275,11 +275,11 @@ public class DatabaseMessageStore(ILogger logger, Config c if (attachment is MimePart mimePartAttachment) { - mimePartAttachment.Content.DecodeTo(memory); + mimePartAttachment.Content?.DecodeTo(memory); } else { - ((MessagePart)attachment).Message.WriteTo(memory); + ((MessagePart)attachment).Message?.WriteTo(memory); } return memory.ToArray(); From e0d05a840d90ffd0f688ca4fcb856f22ba8b9484 Mon Sep 17 00:00:00 2001 From: Leendert de Borst <6917405+lanedirt@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:18:23 +0100 Subject: [PATCH 11/26] New Crowdin updates (#1781) * New translations en.json (French) Update translations from Crowdin [ci skip] * New translations en.json (Spanish) Update translations from Crowdin [ci skip] * New translations en.json (Catalan) Update translations from Crowdin [ci skip] * New translations en.json (German) Update translations from Crowdin [ci skip] * New translations en.json (Finnish) Update translations from Crowdin [ci skip] * New translations en.json (Hebrew) Update translations from Crowdin [ci skip] * New translations en.json (Italian) Update translations from Crowdin [ci skip] * New translations en.json (Dutch) Update translations from Crowdin [ci skip] * New translations en.json (Polish) Update translations from Crowdin [ci skip] * New translations en.json (Russian) Update translations from Crowdin [ci skip] * New translations en.json (Swedish) Update translations from Crowdin [ci skip] * New translations en.json (Turkish) Update translations from Crowdin [ci skip] * New translations en.json (Ukrainian) Update translations from Crowdin [ci skip] * New translations en.json (Chinese Simplified) Update translations from Crowdin [ci skip] * New translations en.json (Portuguese, Brazilian) Update translations from Crowdin [ci skip] * New translations strings.xml (French) Update translations from Crowdin [ci skip] * New translations strings.xml (Spanish) Update translations from Crowdin [ci skip] * New translations strings.xml (Catalan) Update translations from Crowdin [ci skip] * New translations strings.xml (German) Update translations from Crowdin [ci skip] * New translations strings.xml (Finnish) Update translations from Crowdin [ci skip] * New translations strings.xml (Hebrew) Update translations from Crowdin [ci skip] * New translations strings.xml (Italian) Update translations from Crowdin [ci skip] * New translations strings.xml (Dutch) Update translations from Crowdin [ci skip] * New translations strings.xml (Polish) Update translations from Crowdin [ci skip] * New translations strings.xml (Russian) Update translations from Crowdin [ci skip] * New translations strings.xml (Swedish) Update translations from Crowdin [ci skip] * New translations strings.xml (Turkish) Update translations from Crowdin [ci skip] * New translations strings.xml (Ukrainian) Update translations from Crowdin [ci skip] * New translations strings.xml (Chinese Simplified) Update translations from Crowdin [ci skip] * New translations strings.xml (Portuguese, Brazilian) Update translations from Crowdin [ci skip] * New translations localizable.strings (French) Update translations from Crowdin [ci skip] * New translations localizable.strings (Spanish) Update translations from Crowdin [ci skip] * New translations localizable.strings (Catalan) Update translations from Crowdin [ci skip] * New translations localizable.strings (German) Update translations from Crowdin [ci skip] * New translations localizable.strings (Finnish) Update translations from Crowdin [ci skip] * New translations localizable.strings (Hebrew) Update translations from Crowdin [ci skip] * New translations localizable.strings (Italian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Dutch) Update translations from Crowdin [ci skip] * New translations localizable.strings (Polish) Update translations from Crowdin [ci skip] * New translations localizable.strings (Russian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Swedish) Update translations from Crowdin [ci skip] * New translations localizable.strings (Turkish) Update translations from Crowdin [ci skip] * New translations localizable.strings (Ukrainian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Chinese Simplified) Update translations from Crowdin [ci skip] * New translations localizable.strings (Portuguese, Brazilian) Update translations from Crowdin [ci skip] * New translations en.json (Romanian) Update translations from Crowdin [ci skip] * New translations strings.xml (Romanian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Romanian) Update translations from Crowdin [ci skip] * New translations en.json (Persian) Update translations from Crowdin [ci skip] * New translations strings.xml (Persian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Persian) Update translations from Crowdin [ci skip] * New translations en.json (Urdu (Pakistan)) Update translations from Crowdin [ci skip] * New translations en.json (Bulgarian) Update translations from Crowdin [ci skip] * New translations strings.xml (Urdu (Pakistan)) Update translations from Crowdin [ci skip] * New translations localizable.strings (Urdu (Pakistan)) Update translations from Crowdin [ci skip] * New translations strings.xml (Bulgarian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Bulgarian) Update translations from Crowdin [ci skip] * New translations en.json (Danish) Update translations from Crowdin [ci skip] * New translations strings.xml (Danish) Update translations from Crowdin [ci skip] * New translations localizable.strings (Danish) Update translations from Crowdin [ci skip] * New translations en.json (Korean) Update translations from Crowdin [ci skip] * New translations strings.xml (Korean) Update translations from Crowdin [ci skip] * New translations en.json (Czech) Update translations from Crowdin [ci skip] * New translations localizable.strings (Korean) Update translations from Crowdin [ci skip] * New translations strings.xml (Czech) Update translations from Crowdin [ci skip] * New translations localizable.strings (Czech) Update translations from Crowdin [ci skip] * New translations en.json (French) Update translations from Crowdin [ci skip] * New translations strings.xml (French) Update translations from Crowdin [ci skip] * New translations localizable.strings (French) Update translations from Crowdin [ci skip] * New translations en.json (Romanian) Update translations from Crowdin [ci skip] * New translations unlock.en.resx (Romanian) Update translations from Crowdin [ci skip] * New translations strings.xml (Romanian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Romanian) Update translations from Crowdin [ci skip] * New translations en.json (French) Update translations from Crowdin [ci skip] * New translations en.json (Spanish) Update translations from Crowdin [ci skip] * New translations en.json (Catalan) Update translations from Crowdin [ci skip] * New translations en.json (German) Update translations from Crowdin [ci skip] * New translations en.json (Finnish) Update translations from Crowdin [ci skip] * New translations en.json (Hebrew) Update translations from Crowdin [ci skip] * New translations en.json (Italian) Update translations from Crowdin [ci skip] * New translations en.json (Dutch) Update translations from Crowdin [ci skip] * New translations en.json (Polish) Update translations from Crowdin [ci skip] * New translations en.json (Russian) Update translations from Crowdin [ci skip] * New translations en.json (Swedish) Update translations from Crowdin [ci skip] * New translations en.json (Turkish) Update translations from Crowdin [ci skip] * New translations en.json (Ukrainian) Update translations from Crowdin [ci skip] * New translations en.json (Chinese Simplified) Update translations from Crowdin [ci skip] * New translations en.json (Portuguese, Brazilian) Update translations from Crowdin [ci skip] * New translations strings.xml (Dutch) Update translations from Crowdin [ci skip] * New translations localizable.strings (Dutch) Update translations from Crowdin [ci skip] * New translations en.json (Romanian) Update translations from Crowdin [ci skip] * New translations en.json (Persian) Update translations from Crowdin [ci skip] * New translations en.json (Urdu (Pakistan)) Update translations from Crowdin [ci skip] * New translations en.json (Bulgarian) Update translations from Crowdin [ci skip] * New translations en.json (Danish) Update translations from Crowdin [ci skip] * New translations en.json (Korean) Update translations from Crowdin [ci skip] * New translations en.json (Czech) Update translations from Crowdin [ci skip] * New translations en.json (Italian) Update translations from Crowdin [ci skip] * New translations strings.xml (Italian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Italian) Update translations from Crowdin [ci skip] * New translations en.json (Romanian) Update translations from Crowdin [ci skip] * New translations en.json (Catalan) Update translations from Crowdin [ci skip] * New translations en.json (German) Update translations from Crowdin [ci skip] * New translations en.json (Finnish) Update translations from Crowdin [ci skip] * New translations en.json (Hebrew) Update translations from Crowdin [ci skip] * New translations en.json (Dutch) Update translations from Crowdin [ci skip] * New translations en.json (Russian) Update translations from Crowdin [ci skip] * New translations en.json (Turkish) Update translations from Crowdin [ci skip] * New translations en.json (Ukrainian) Update translations from Crowdin [ci skip] * New translations en.json (Catalan) Update translations from Crowdin [ci skip] * New translations en.json (German) Update translations from Crowdin [ci skip] * New translations en.json (Finnish) Update translations from Crowdin [ci skip] * New translations en.json (Hebrew) Update translations from Crowdin [ci skip] * New translations en.json (Dutch) Update translations from Crowdin [ci skip] * New translations en.json (Russian) Update translations from Crowdin [ci skip] * New translations en.json (Turkish) Update translations from Crowdin [ci skip] * New translations en.json (Ukrainian) Update translations from Crowdin [ci skip] * New translations strings.xml (Spanish) Update translations from Crowdin [ci skip] * New translations strings.xml (Catalan) Update translations from Crowdin [ci skip] * New translations strings.xml (German) Update translations from Crowdin [ci skip] * New translations strings.xml (Finnish) Update translations from Crowdin [ci skip] * New translations strings.xml (Hebrew) Update translations from Crowdin [ci skip] * New translations strings.xml (Polish) Update translations from Crowdin [ci skip] * New translations strings.xml (Russian) Update translations from Crowdin [ci skip] * New translations strings.xml (Swedish) Update translations from Crowdin [ci skip] * New translations strings.xml (Turkish) Update translations from Crowdin [ci skip] * New translations strings.xml (Ukrainian) Update translations from Crowdin [ci skip] * New translations strings.xml (Chinese Simplified) Update translations from Crowdin [ci skip] * New translations strings.xml (Portuguese, Brazilian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Spanish) Update translations from Crowdin [ci skip] * New translations localizable.strings (German) Update translations from Crowdin [ci skip] * New translations localizable.strings (Finnish) Update translations from Crowdin [ci skip] * New translations localizable.strings (Hebrew) Update translations from Crowdin [ci skip] * New translations localizable.strings (Polish) Update translations from Crowdin [ci skip] * New translations localizable.strings (Russian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Swedish) Update translations from Crowdin [ci skip] * New translations localizable.strings (Turkish) Update translations from Crowdin [ci skip] * New translations localizable.strings (Ukrainian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Chinese Simplified) Update translations from Crowdin [ci skip] * New translations localizable.strings (Portuguese, Brazilian) Update translations from Crowdin [ci skip] * New translations en.json (Urdu (Pakistan)) Update translations from Crowdin [ci skip] * New translations en.json (Urdu (Pakistan)) Update translations from Crowdin [ci skip] * New translations strings.xml (Urdu (Pakistan)) Update translations from Crowdin [ci skip] * New translations en.json (Bulgarian) Update translations from Crowdin [ci skip] * New translations strings.xml (Bulgarian) Update translations from Crowdin [ci skip] * New translations localizable.strings (Bulgarian) Update translations from Crowdin [ci skip] * New translations strings.xml (Danish) Update translations from Crowdin [ci skip] * New translations localizable.strings (Danish) Update translations from Crowdin [ci skip] * New translations en.json (Chinese Simplified) Update translations from Crowdin [ci skip] * New translations strings.xml (Chinese Simplified) Update translations from Crowdin [ci skip] * New translations en.json (French) Update translations from Crowdin [ci skip] * New translations createnewidentitywidget.en.resx (French) Update translations from Crowdin [ci skip] * New translations en.json (French) Update translations from Crowdin [ci skip] * New translations en.json (Danish) Update translations from Crowdin [ci skip] * New translations strings.xml (Danish) Update translations from Crowdin [ci skip] * New translations en.json (Swedish) Update translations from Crowdin [ci skip] * New translations en.json (Swedish) Update translations from Crowdin [ci skip] * New translations en.json (Swedish) Update translations from Crowdin [ci skip] * New translations strings.xml (Swedish) Update translations from Crowdin [ci skip] * New translations importservices.en.resx (Swedish) Update translations from Crowdin [ci skip] * New translations quickvaultunlocksection.en.resx (Swedish) Update translations from Crowdin [ci skip] * New translations twofactorauthenticationsection.en.resx (Swedish) Update translations from Crowdin [ci skip] * New translations unlock.en.resx (Swedish) Update translations from Crowdin [ci skip] * New translations apps.en.resx (Swedish) Update translations from Crowdin [ci skip] * New translations en.json (Swedish) Update translations from Crowdin [ci skip] * New translations en.json (French) Update translations from Crowdin [ci skip] * New translations searchwidget.en.resx (French) Update translations from Crowdin [ci skip] * New translations en.json (French) Update translations from Crowdin [ci skip] * New translations en.json (Spanish) Update translations from Crowdin [ci skip] * New translations strings.xml (Spanish) Update translations from Crowdin [ci skip] * New translations en.json (French) Update translations from Crowdin [ci skip] * New translations en.json (Spanish) Update translations from Crowdin [ci skip] * New translations en.json (Catalan) Update translations from Crowdin [ci skip] * New translations en.json (German) Update translations from Crowdin [ci skip] * New translations en.json (Finnish) Update translations from Crowdin [ci skip] * New translations en.json (Hebrew) Update translations from Crowdin [ci skip] * New translations en.json (Italian) Update translations from Crowdin [ci skip] * New translations en.json (Dutch) Update translations from Crowdin [ci skip] * New translations en.json (Polish) Update translations from Crowdin [ci skip] * New translations en.json (Russian) Update translations from Crowdin [ci skip] * New translations en.json (Swedish) Update translations from Crowdin [ci skip] * New translations en.json (Turkish) Update translations from Crowdin [ci skip] * New translations en.json (Ukrainian) Update translations from Crowdin [ci skip] * New translations en.json (Chinese Simplified) Update translations from Crowdin [ci skip] * New translations en.json (Portuguese, Brazilian) Update translations from Crowdin [ci skip] * New translations en.json (Romanian) Update translations from Crowdin [ci skip] * New translations en.json (Persian) Update translations from Crowdin [ci skip] * New translations en.json (Urdu (Pakistan)) Update translations from Crowdin [ci skip] * New translations en.json (Bulgarian) Update translations from Crowdin [ci skip] * New translations en.json (Danish) Update translations from Crowdin [ci skip] * New translations en.json (Korean) Update translations from Crowdin [ci skip] * New translations en.json (Czech) Update translations from Crowdin [ci skip] * New translations en.json (French) Update translations from Crowdin [ci skip] * New translations en.json (Dutch) Update translations from Crowdin [ci skip] * New translations en.json (Swedish) Update translations from Crowdin [ci skip] * New translations en.json (Chinese Simplified) Update translations from Crowdin [ci skip] * New translations en.json (Romanian) Update translations from Crowdin [ci skip] * New translations sharedresources.en.resx (Russian) Update translations from Crowdin [ci skip] * New translations en.json (Italian) Update translations from Crowdin [ci skip] * New translations en.json (Russian) Update translations from Crowdin [ci skip] * New translations home.en.resx (Russian) Update translations from Crowdin [ci skip] * New translations en.json (Russian) Update translations from Crowdin [ci skip] * New translations strings.xml (Russian) Update translations from Crowdin [ci skip] * New translations en.json (Spanish) Update translations from Crowdin [ci skip] --- .../src/i18n/locales/bg.json | 2 +- .../src/i18n/locales/ca.json | 4 +-- .../src/i18n/locales/de.json | 2 +- .../src/i18n/locales/fi.json | 4 +-- .../src/i18n/locales/fr.json | 6 ++-- .../src/i18n/locales/he.json | 4 +-- .../src/i18n/locales/nl.json | 2 +- .../src/i18n/locales/ru.json | 20 +++++------ .../src/i18n/locales/sv.json | 4 +-- .../src/i18n/locales/tr.json | 4 +-- .../src/i18n/locales/uk.json | 2 +- .../src/i18n/locales/ur.json | 4 +-- .../app/src/main/res/values-bg/strings.xml | 8 +++++ .../app/src/main/res/values-ca/strings.xml | 8 +++++ .../app/src/main/res/values-cs/strings.xml | 8 +++++ .../app/src/main/res/values-da/strings.xml | 8 +++++ .../app/src/main/res/values-de/strings.xml | 8 +++++ .../app/src/main/res/values-es/strings.xml | 8 +++++ .../app/src/main/res/values-fa/strings.xml | 8 +++++ .../app/src/main/res/values-fi/strings.xml | 8 +++++ .../app/src/main/res/values-fr/strings.xml | 8 +++++ .../app/src/main/res/values-he/strings.xml | 8 +++++ .../app/src/main/res/values-it/strings.xml | 8 +++++ .../app/src/main/res/values-ko/strings.xml | 8 +++++ .../app/src/main/res/values-nl/strings.xml | 8 +++++ .../app/src/main/res/values-pl/strings.xml | 8 +++++ .../app/src/main/res/values-pt/strings.xml | 8 +++++ .../app/src/main/res/values-ro/strings.xml | 8 +++++ .../app/src/main/res/values-ru/strings.xml | 8 +++++ .../app/src/main/res/values-sv/strings.xml | 8 +++++ .../app/src/main/res/values-tr/strings.xml | 8 +++++ .../app/src/main/res/values-uk/strings.xml | 8 +++++ .../app/src/main/res/values-ur/strings.xml | 8 +++++ .../app/src/main/res/values-zh/strings.xml | 8 +++++ apps/mobile-app/i18n/locales/bg.json | 12 ++++--- apps/mobile-app/i18n/locales/ca.json | 14 +++++--- apps/mobile-app/i18n/locales/cs.json | 12 ++++--- apps/mobile-app/i18n/locales/da.json | 12 ++++--- apps/mobile-app/i18n/locales/de.json | 14 +++++--- apps/mobile-app/i18n/locales/es.json | 12 ++++--- apps/mobile-app/i18n/locales/fa.json | 12 ++++--- apps/mobile-app/i18n/locales/fi.json | 16 +++++---- apps/mobile-app/i18n/locales/fr.json | 16 +++++---- apps/mobile-app/i18n/locales/he.json | 16 +++++---- apps/mobile-app/i18n/locales/it.json | 12 ++++--- apps/mobile-app/i18n/locales/ko.json | 12 ++++--- apps/mobile-app/i18n/locales/nl.json | 12 ++++--- apps/mobile-app/i18n/locales/pl.json | 12 ++++--- apps/mobile-app/i18n/locales/pt.json | 12 ++++--- apps/mobile-app/i18n/locales/ro.json | 12 ++++--- apps/mobile-app/i18n/locales/ru.json | 34 +++++++++++-------- apps/mobile-app/i18n/locales/sv.json | 12 ++++--- apps/mobile-app/i18n/locales/tr.json | 14 +++++--- apps/mobile-app/i18n/locales/uk.json | 14 +++++--- apps/mobile-app/i18n/locales/ur.json | 16 +++++---- apps/mobile-app/i18n/locales/zh.json | 12 ++++--- .../ios/VaultUI/bg.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/ca.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/cs.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/da.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/de.lproj/Localizable.strings | 7 +++- .../ios/VaultUI/es.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/fa.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/fi.lproj/Localizable.strings | 7 +++- .../ios/VaultUI/fr.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/he.lproj/Localizable.strings | 7 +++- .../ios/VaultUI/it.lproj/Localizable.strings | 7 +++- .../ios/VaultUI/ko.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/nl.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/pl.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/pt.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/ro.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/ru.lproj/Localizable.strings | 7 +++- .../ios/VaultUI/sv.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/tr.lproj/Localizable.strings | 7 +++- .../ios/VaultUI/uk.lproj/Localizable.strings | 7 +++- .../ios/VaultUI/ur.lproj/Localizable.strings | 5 +++ .../ios/VaultUI/zh.lproj/Localizable.strings | 5 +++ .../ImportExport/ImportServices.sv.resx | 2 +- .../Security/QuickVaultUnlockSection.sv.resx | 6 ++-- .../TwoFactorAuthenticationSection.sv.resx | 2 +- .../Widgets/CreateNewIdentityWidget.fr.resx | 2 +- .../Main/Widgets/SearchWidget.fr.resx | 2 +- .../Resources/Pages/Auth/Unlock.ro.resx | 2 +- .../Resources/Pages/Auth/Unlock.sv.resx | 2 +- .../Resources/Pages/Main/Items/Home.ru.resx | 2 +- .../Pages/Main/Settings/Apps.sv.resx | 2 +- .../Resources/SharedResources.ru.resx | 4 +-- .../AliasVault.Client/wwwroot/locales/sv.json | 2 +- 89 files changed, 535 insertions(+), 161 deletions(-) diff --git a/apps/browser-extension/src/i18n/locales/bg.json b/apps/browser-extension/src/i18n/locales/bg.json index 28a348281..19833da1f 100644 --- a/apps/browser-extension/src/i18n/locales/bg.json +++ b/apps/browser-extension/src/i18n/locales/bg.json @@ -172,7 +172,7 @@ "vaultUpgradeRequired": "Vault upgrade required.", "dismissPopup": "Dismiss popup", "noTotpItemsFound": "No 2FA code matches found", - "close": "Close", + "close": "Затвори", "savePrompt": { "title": "Save to AliasVault?", "neverForThisSite": "Never for this site", diff --git a/apps/browser-extension/src/i18n/locales/ca.json b/apps/browser-extension/src/i18n/locales/ca.json index 8ba6b2639..2f66cf6df 100644 --- a/apps/browser-extension/src/i18n/locales/ca.json +++ b/apps/browser-extension/src/i18n/locales/ca.json @@ -51,7 +51,7 @@ "undo": "Undo", "save": "Desa", "saving": "Saving...", - "edit": "Edit", + "edit": "Edita", "create": "Create", "or": "Or", "close": "Tanca", @@ -172,7 +172,7 @@ "vaultUpgradeRequired": "Vault upgrade required.", "dismissPopup": "Dismiss popup", "noTotpItemsFound": "No 2FA code matches found", - "close": "Close", + "close": "Tanca", "savePrompt": { "title": "Save to AliasVault?", "neverForThisSite": "Never for this site", diff --git a/apps/browser-extension/src/i18n/locales/de.json b/apps/browser-extension/src/i18n/locales/de.json index cf8f1edb5..10b3b900d 100644 --- a/apps/browser-extension/src/i18n/locales/de.json +++ b/apps/browser-extension/src/i18n/locales/de.json @@ -172,7 +172,7 @@ "vaultUpgradeRequired": "Aktualisierung des Tresors erforderlich.", "dismissPopup": "Popup schliessen", "noTotpItemsFound": "No 2FA code matches found", - "close": "Close", + "close": "Schließen", "savePrompt": { "title": "Save to AliasVault?", "neverForThisSite": "Never for this site", diff --git a/apps/browser-extension/src/i18n/locales/fi.json b/apps/browser-extension/src/i18n/locales/fi.json index a6327c686..6c0f1542d 100644 --- a/apps/browser-extension/src/i18n/locales/fi.json +++ b/apps/browser-extension/src/i18n/locales/fi.json @@ -51,7 +51,7 @@ "undo": "Kumoa", "save": "Tallenna", "saving": "Saving...", - "edit": "Edit", + "edit": "Muokkaa", "create": "Luo", "or": "Tai", "close": "Sulje", @@ -172,7 +172,7 @@ "vaultUpgradeRequired": "Holvin päivitys vaaditaan.", "dismissPopup": "Hylkää ponnahdusikkuna", "noTotpItemsFound": "No 2FA code matches found", - "close": "Close", + "close": "Sulje", "savePrompt": { "title": "Save to AliasVault?", "neverForThisSite": "Never for this site", diff --git a/apps/browser-extension/src/i18n/locales/fr.json b/apps/browser-extension/src/i18n/locales/fr.json index e56bcb465..eea50d8ba 100644 --- a/apps/browser-extension/src/i18n/locales/fr.json +++ b/apps/browser-extension/src/i18n/locales/fr.json @@ -141,11 +141,11 @@ }, "content": { "or": "ou", - "new": "Nouveautés", + "new": "Nouveau", "vaultLocked": "AliasVault est verrouillé.", "creatingNewAlias": "Création de nouveaux alias...", "noMatchesFound": "Aucun résultat trouvé", - "searchVault": "Rechercher dans le coffre...", + "searchVault": "Rechercher élément...", "enterServiceName": "Entrez le nom du service", "enterEmailAddress": "Entrer l'adresse email", "enterUsername": "Entrez le nom d'utilisateur", @@ -266,7 +266,7 @@ "textArea": "Zone de texte" }, "login": { - "title": "Se connecter" + "title": "Identifiant" }, "alias": { "title": "Alias" diff --git a/apps/browser-extension/src/i18n/locales/he.json b/apps/browser-extension/src/i18n/locales/he.json index 48a976faa..685988698 100644 --- a/apps/browser-extension/src/i18n/locales/he.json +++ b/apps/browser-extension/src/i18n/locales/he.json @@ -51,7 +51,7 @@ "undo": "הסגה", "save": "שמירה", "saving": "Saving...", - "edit": "Edit", + "edit": "עריכה", "create": "יצירה", "or": "או", "close": "סגירה", @@ -172,7 +172,7 @@ "vaultUpgradeRequired": "יש לשדרג את הכספת.", "dismissPopup": "התעלמות מחלונית", "noTotpItemsFound": "No 2FA code matches found", - "close": "Close", + "close": "סגירה", "savePrompt": { "title": "Save to AliasVault?", "neverForThisSite": "Never for this site", diff --git a/apps/browser-extension/src/i18n/locales/nl.json b/apps/browser-extension/src/i18n/locales/nl.json index 6ba839971..de2f4ab0b 100644 --- a/apps/browser-extension/src/i18n/locales/nl.json +++ b/apps/browser-extension/src/i18n/locales/nl.json @@ -177,7 +177,7 @@ "title": "Opslaan in AliasVault?", "neverForThisSite": "Nooit voor deze site", "addUrlTitle": "URL naar inloggegevens toevoegen?", - "addUrl": "Url toevoegen" + "addUrl": "URL toevoegen" } }, "items": { diff --git a/apps/browser-extension/src/i18n/locales/ru.json b/apps/browser-extension/src/i18n/locales/ru.json index 07bee3e67..c968f4d23 100644 --- a/apps/browser-extension/src/i18n/locales/ru.json +++ b/apps/browser-extension/src/i18n/locales/ru.json @@ -51,7 +51,7 @@ "undo": "Отменить", "save": "Сохранить", "saving": "Сохраняем...", - "edit": "Edit", + "edit": "Редактировать", "create": "Создать", "or": "Или", "close": "Закрыть", @@ -63,8 +63,8 @@ "disabled": "Отключено", "showPassword": "Показать пароль", "hidePassword": "Скрыть пароль", - "show": "Show", - "hide": "Hide", + "show": "Показать", + "hide": "Скрыть", "showDetails": "Показать подробности", "hideDetails": "Скрыть подробности", "copyToClipboard": "Скопировать в буфер обмена", @@ -171,13 +171,13 @@ "openAliasVaultToUpgrade": "Откройте AliasVault для обновления", "vaultUpgradeRequired": "Требуется обновление хранилища.", "dismissPopup": "Закрыть окно", - "noTotpItemsFound": "No 2FA code matches found", - "close": "Close", + "noTotpItemsFound": "2FA-коды не найдены", + "close": "Закрыть", "savePrompt": { "title": "Сохранить в AliasVault?", "neverForThisSite": "Никогда для этого сайта", - "addUrlTitle": "Add URL to credential?", - "addUrl": "Add URL" + "addUrlTitle": "Добавить URL к этой записи?", + "addUrl": "Добавить URL" } }, "items": { @@ -215,7 +215,7 @@ "filters": { "folders": "Папки", "passkeys": "Ключи доступа", - "totp": "2FA Codes" + "totp": "2FA-коды" }, "sort": { "title": "Сортировка", @@ -302,8 +302,8 @@ "secretKey": "Секретный ключ", "saveToViewCode": "Сохранить для просмотра кода", "defaultName": "Аутентификатор", - "deleteTotpCodeTitle": "Delete 2FA Code", - "deleteTotpCodeConfirmation": "Are you sure you want to delete the 2FA code \"{{name}}\"?", + "deleteTotpCodeTitle": "Удаление 2FA-кода", + "deleteTotpCodeConfirmation": "Вы уверены, что хотите удалить 2FA-код для \"{{name}}\"?", "errors": { "invalidSecretKey": "Неверный формат секретного ключа." } diff --git a/apps/browser-extension/src/i18n/locales/sv.json b/apps/browser-extension/src/i18n/locales/sv.json index 6cf788316..4fd0fdca8 100644 --- a/apps/browser-extension/src/i18n/locales/sv.json +++ b/apps/browser-extension/src/i18n/locales/sv.json @@ -302,7 +302,7 @@ "secretKey": "Hemlig nyckel", "saveToViewCode": "Spara för att visa kod", "defaultName": "Autentiserare", - "deleteTotpCodeTitle": "Ta bort 2FA-kod", + "deleteTotpCodeTitle": "Radera 2FA-kod", "deleteTotpCodeConfirmation": "Är du säker på att du vill radera 2FA-koden\"{{name}}\"?", "errors": { "invalidSecretKey": "Ogiltigt format för hemlig nyckel." @@ -469,7 +469,7 @@ "helpText": "Passkeys skapas på webbplatsen när du blir tillfrågad. De kan inte redigeras manuellt. För att radera denna passkey, kan du ta bort det från denna posten. För att byta ut denna passkey eller skapa en ny, besök webbplatsen och följ dess anvisningar.", "passkeyMarkedForDeletion": "Passkey markerad för borttagning", "passkeyWillBeDeleted": "Denna passkey kommer att tas bort när du sparar dessa uppgifter.", - "useBrowserPasskey": "Använd Webbläsar Passkey", + "useBrowserPasskey": "Använd webbläsarens Passkey", "bypass": { "description": "Hur länge vill du använda webbläsarens passkey-leverantör till {{origin}}?", "thisTimeOnly": "Endast denna gång", diff --git a/apps/browser-extension/src/i18n/locales/tr.json b/apps/browser-extension/src/i18n/locales/tr.json index d36dc7c90..e1fcd7064 100644 --- a/apps/browser-extension/src/i18n/locales/tr.json +++ b/apps/browser-extension/src/i18n/locales/tr.json @@ -51,7 +51,7 @@ "undo": "Undo", "save": "Kaydet", "saving": "Saving...", - "edit": "Edit", + "edit": "Düzenle", "create": "Create", "or": "Or", "close": "Kapat", @@ -172,7 +172,7 @@ "vaultUpgradeRequired": "Vault upgrade required.", "dismissPopup": "Dismiss popup", "noTotpItemsFound": "No 2FA code matches found", - "close": "Close", + "close": "Kapat", "savePrompt": { "title": "Save to AliasVault?", "neverForThisSite": "Never for this site", diff --git a/apps/browser-extension/src/i18n/locales/uk.json b/apps/browser-extension/src/i18n/locales/uk.json index e17b4867a..007d74a46 100644 --- a/apps/browser-extension/src/i18n/locales/uk.json +++ b/apps/browser-extension/src/i18n/locales/uk.json @@ -172,7 +172,7 @@ "vaultUpgradeRequired": "Потрібне оновлення сховища.", "dismissPopup": "Закрити спливаюче вікно", "noTotpItemsFound": "No 2FA code matches found", - "close": "Close", + "close": "Закрити", "savePrompt": { "title": "Save to AliasVault?", "neverForThisSite": "Never for this site", diff --git a/apps/browser-extension/src/i18n/locales/ur.json b/apps/browser-extension/src/i18n/locales/ur.json index c895482cf..7b858a747 100644 --- a/apps/browser-extension/src/i18n/locales/ur.json +++ b/apps/browser-extension/src/i18n/locales/ur.json @@ -51,7 +51,7 @@ "undo": "Undo", "save": "محفوظ کریں", "saving": "Saving...", - "edit": "Edit", + "edit": "ترمیم کریں", "create": "Create", "or": "Or", "close": "بند کریں", @@ -172,7 +172,7 @@ "vaultUpgradeRequired": "Vault upgrade required.", "dismissPopup": "Dismiss popup", "noTotpItemsFound": "No 2FA code matches found", - "close": "Close", + "close": "بند کریں", "savePrompt": { "title": "Save to AliasVault?", "neverForThisSite": "Never for this site", diff --git a/apps/mobile-app/android/app/src/main/res/values-bg/strings.xml b/apps/mobile-app/android/app/src/main/res/values-bg/strings.xml index 2f4f4af05..28028d5f5 100644 --- a/apps/mobile-app/android/app/src/main/res/values-bg/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-bg/strings.xml @@ -7,6 +7,7 @@ Затвори Следващ Отмени + Back An unknown error occurred Failed to retrieve, open app @@ -81,4 +82,11 @@ Confirm PIN Re-enter your PIN to confirm PINs do not match. Please try again. + + Unlock Vault + Enter your master password + Password + Отключи + Incorrect password. Please try again. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-ca/strings.xml b/apps/mobile-app/android/app/src/main/res/values-ca/strings.xml index cdf5207b4..733c86f82 100644 --- a/apps/mobile-app/android/app/src/main/res/values-ca/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-ca/strings.xml @@ -7,6 +7,7 @@ Tanca Següent Cancel·la + Enrere An unknown error occurred Failed to retrieve, open app @@ -81,4 +82,11 @@ Confirm PIN Re-enter your PIN to confirm PINs do not match. Please try again. + + Unlock Vault + Enter your master password + Contrasenya + Unlock + Incorrect password. Please try again. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-cs/strings.xml b/apps/mobile-app/android/app/src/main/res/values-cs/strings.xml index 60d79e56f..e3969f370 100644 --- a/apps/mobile-app/android/app/src/main/res/values-cs/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-cs/strings.xml @@ -7,6 +7,7 @@ Close Next Cancel + Back An unknown error occurred Failed to retrieve, open app @@ -81,4 +82,11 @@ Confirm PIN Re-enter your PIN to confirm PINs do not match. Please try again. + + Unlock Vault + Enter your master password + Password + Unlock + Incorrect password. Please try again. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-da/strings.xml b/apps/mobile-app/android/app/src/main/res/values-da/strings.xml index f8baf5e9b..6895a6d73 100644 --- a/apps/mobile-app/android/app/src/main/res/values-da/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-da/strings.xml @@ -7,6 +7,7 @@ Luk Næste Annuller + Tilbage En ukendt fejl opstod Kunne ikke hente, åben app @@ -81,4 +82,11 @@ Bekræft PIN-kode Indtast din PIN-kode igen for at bekræfte Pinkoden stemmer ikke overens. Prøv venligst igen. + + Lås boks op + Indtast din hovedadgangskode + Adgangskode + Lås op + Forkert adgangskode. Prøv venligst igen. + Kunne ikke bekræfte adgangskode diff --git a/apps/mobile-app/android/app/src/main/res/values-de/strings.xml b/apps/mobile-app/android/app/src/main/res/values-de/strings.xml index 7dfbe94aa..b1239fce3 100644 --- a/apps/mobile-app/android/app/src/main/res/values-de/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-de/strings.xml @@ -7,6 +7,7 @@ Schließen Weiter Abbrechen + Zurück Ein unbekannter Fehler ist aufgetreten Abruf der Daten fehlgeschlagen. Öffne die App @@ -81,4 +82,11 @@ PIN bestätigen Zur Bestätigung PIN erneut eingeben PINs stimmen nicht überein. Bitte versuche es erneut. + + Tresor entsperren + Gib Dein Master-Passwort ein + Passwort + Entsperren + Falsches Passwort. Bitte versuche es erneut. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-es/strings.xml b/apps/mobile-app/android/app/src/main/res/values-es/strings.xml index 8007fe4c9..af895315e 100644 --- a/apps/mobile-app/android/app/src/main/res/values-es/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-es/strings.xml @@ -7,6 +7,7 @@ Cerrar Siguiente Cancelar + Atrás Se produjo un error desconocido Fallo al recuperar, abrir aplicación @@ -81,4 +82,11 @@ Confirmar PIN Vuelva a introducir su PIN para confirmar Los PIN no coinciden. Por favor, inténtalo de nuevo. + + Desbloquear bóveda + Ingrese su contraseña maestra + Contraseña + Desbloquear + Contraseña incorrecta. Por favor, inténtelo de nuevo. + Error al verificar la contraseña diff --git a/apps/mobile-app/android/app/src/main/res/values-fa/strings.xml b/apps/mobile-app/android/app/src/main/res/values-fa/strings.xml index 60d79e56f..e3969f370 100644 --- a/apps/mobile-app/android/app/src/main/res/values-fa/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-fa/strings.xml @@ -7,6 +7,7 @@ Close Next Cancel + Back An unknown error occurred Failed to retrieve, open app @@ -81,4 +82,11 @@ Confirm PIN Re-enter your PIN to confirm PINs do not match. Please try again. + + Unlock Vault + Enter your master password + Password + Unlock + Incorrect password. Please try again. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-fi/strings.xml b/apps/mobile-app/android/app/src/main/res/values-fi/strings.xml index e09cab71d..5f6d78dcb 100644 --- a/apps/mobile-app/android/app/src/main/res/values-fi/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-fi/strings.xml @@ -7,6 +7,7 @@ Sulje Seuraava Peruuta + Takaisin Tapahtui tuntematon virhe Nouto epäonnistui, avaa sovellus @@ -81,4 +82,11 @@ Vahvista PIN Syötä PIN-koodi uudelleen vahvistaaksesi PIN-koodit eivät täsmää. Yritä uudelleen. + + Avaa holvin lukitus + Syötä pääsalasanasi + Salasana: + Avaa lukitus + Virheellinen salasana. Yritä uudelleen. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-fr/strings.xml b/apps/mobile-app/android/app/src/main/res/values-fr/strings.xml index 6e86daeb0..d9b3425c4 100644 --- a/apps/mobile-app/android/app/src/main/res/values-fr/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-fr/strings.xml @@ -7,6 +7,7 @@ Fermer Suivant Annuler + Retour Une erreur inconnue s\'est produite Échec de la récupération, ouvrez l\'application @@ -81,4 +82,11 @@ Confirmer le code PIN Entrez à nouveau votre code PIN pour confirmer Les codes PIN ne correspondent pas. Veuillez réessayer. + + Déverrouiller le coffre + Entrez votre mot de passe maître + Mot de passe + Déverrouiller + Mot de passe incorrect. Veuillez réessayer. + Échec de la vérification du mot de passe diff --git a/apps/mobile-app/android/app/src/main/res/values-he/strings.xml b/apps/mobile-app/android/app/src/main/res/values-he/strings.xml index 0e674d401..4c2cb3eef 100644 --- a/apps/mobile-app/android/app/src/main/res/values-he/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-he/strings.xml @@ -7,6 +7,7 @@ סגירה הבא ביטול + חזרה אירעה שגיאה לא ידועה המשיכה נכשלה, נא לפתוח את היישום @@ -81,4 +82,11 @@ אישור קוד אישי נא למלא את הקוד האישי שלך מחדש לאישור הקודים האישיים לא תואמים. נא לנסות שוב. + + שחרור נעילת כספת + נא למלא את סיסמת העל שלך + סיסמה + שחרור נעילה + סיסמה שגויה. נא לנסות שוב. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-it/strings.xml b/apps/mobile-app/android/app/src/main/res/values-it/strings.xml index 82633b63d..5ae685e26 100644 --- a/apps/mobile-app/android/app/src/main/res/values-it/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-it/strings.xml @@ -7,6 +7,7 @@ Chiudi Avanti Annulla + Indietro Si è verificato un errore sconosciuto Impossibile recuperare, aprire l\'app @@ -81,4 +82,11 @@ Conferma PIN Reinserisci il tuo PIN per confermare. I PIN non corrispondono. Riprova. + + Sblocca Cassaforte + Inserisci la password principale + Password + Sblocca + Password errata. Riprovare. + Verifica della password non riuscita diff --git a/apps/mobile-app/android/app/src/main/res/values-ko/strings.xml b/apps/mobile-app/android/app/src/main/res/values-ko/strings.xml index 60d79e56f..e3969f370 100644 --- a/apps/mobile-app/android/app/src/main/res/values-ko/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-ko/strings.xml @@ -7,6 +7,7 @@ Close Next Cancel + Back An unknown error occurred Failed to retrieve, open app @@ -81,4 +82,11 @@ Confirm PIN Re-enter your PIN to confirm PINs do not match. Please try again. + + Unlock Vault + Enter your master password + Password + Unlock + Incorrect password. Please try again. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-nl/strings.xml b/apps/mobile-app/android/app/src/main/res/values-nl/strings.xml index 0a9e593a6..92bcbbc3b 100644 --- a/apps/mobile-app/android/app/src/main/res/values-nl/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-nl/strings.xml @@ -7,6 +7,7 @@ Sluiten Volgende Annuleren + Terug Er is een onbekende fout opgetreden Ophalen mislukt, open app @@ -81,4 +82,11 @@ Bevestig pincode Voer je pincode opnieuw in om te bevestigen Pincodes komen niet overeen. Probeer het opnieuw. + + Ontgrendel je vault + Voer je hoofdwachtwoord in + Wachtwoord + Ontgrendelen + Onjuist wachtwoord. Probeer het opnieuw. + Verifiëren wachtwoord mislukt diff --git a/apps/mobile-app/android/app/src/main/res/values-pl/strings.xml b/apps/mobile-app/android/app/src/main/res/values-pl/strings.xml index 983c15d87..93bf12ce1 100644 --- a/apps/mobile-app/android/app/src/main/res/values-pl/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-pl/strings.xml @@ -7,6 +7,7 @@ Zamknij Dalej Anuluj + Wstecz Wystąpił nieznany błąd Nie udało się pobrać, otwórz aplikację @@ -81,4 +82,11 @@ Potwierdź kod PIN Wprowadź ponownie swój kod PIN, aby potwierdzić Kody PIN nie pasują. Spróbuj jeszcze raz. + + Odblokuj sejf + Wprowadź swoje hasło główne + Hasło + Odblokuj + Nieprawidłowe hasło. Spróbuj ponownie. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-pt/strings.xml b/apps/mobile-app/android/app/src/main/res/values-pt/strings.xml index 810b92113..826035678 100644 --- a/apps/mobile-app/android/app/src/main/res/values-pt/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-pt/strings.xml @@ -7,6 +7,7 @@ Fechar Próximo Cancelar + Voltar Ocorreu um erro desconhecido Falha ao recuperar, abra o aplicativo @@ -81,4 +82,11 @@ Confirmar PIN Digite seu PIN novamente para confirmar PINs não correspondem. Por favor, tente novamente. + + Desbloquear Cofre + Digite sua senha mestre + Senha + Desbloquear + Senha incorreta. Por favor tente novamente. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-ro/strings.xml b/apps/mobile-app/android/app/src/main/res/values-ro/strings.xml index c789bc4b3..1725aa5e7 100644 --- a/apps/mobile-app/android/app/src/main/res/values-ro/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-ro/strings.xml @@ -7,6 +7,7 @@ Închide Înainte Anulează + Înapoi A apărut o eroare necunoscută Preluare eșuată, deschide aplicația @@ -81,4 +82,11 @@ Confirmă PIN Introdu PIN-ul din nou pentru confirmare Codurile PIN nu se potrivesc. Încearcă din nou. + + Deblochează seiful + Introdu parola principală + Parolă + Deblochează + Parolă incorectă. Încearcă din nou. + Verificarea parolei a eșuat diff --git a/apps/mobile-app/android/app/src/main/res/values-ru/strings.xml b/apps/mobile-app/android/app/src/main/res/values-ru/strings.xml index 1afb78694..c29e95967 100644 --- a/apps/mobile-app/android/app/src/main/res/values-ru/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-ru/strings.xml @@ -7,6 +7,7 @@ Закрыть Далее Отмена + Назад Произошла неизвестная ошибка Не удалось извлечь, открыть приложение @@ -81,4 +82,11 @@ Подтвердите ПИН-код Введите PIN-код еще раз для подтверждения PIN-коды не совпадают. Повторите попытку. + + Разблокировать хранилище + Введите свой мастер-пароль + Пароль + Разблокировать + Неверный пароль. Повторите попытку. + Не удалось проверить пароль diff --git a/apps/mobile-app/android/app/src/main/res/values-sv/strings.xml b/apps/mobile-app/android/app/src/main/res/values-sv/strings.xml index 4b2f0ea30..bc4ffde12 100644 --- a/apps/mobile-app/android/app/src/main/res/values-sv/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-sv/strings.xml @@ -7,6 +7,7 @@ Stäng Nästa Avbryt + Tillbaka Ett okänt fel har inträffat Det gick inte att hämta, öppna app @@ -81,4 +82,11 @@ Bekräfta PIN-kod Ange din PIN-kod igen för att bekräfta PIN-koderna matchar inte. Var god försök igen. + + Lås upp valv + Ange ditt huvudlösenord + Lösenord + Lås upp + Felaktigt lösenord. Försök igen. + Kunde inte verifiera lösenord diff --git a/apps/mobile-app/android/app/src/main/res/values-tr/strings.xml b/apps/mobile-app/android/app/src/main/res/values-tr/strings.xml index fa58c60f6..65865a5e3 100644 --- a/apps/mobile-app/android/app/src/main/res/values-tr/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-tr/strings.xml @@ -7,6 +7,7 @@ Kapat İleri İptal + Geri An unknown error occurred Failed to retrieve, open app @@ -81,4 +82,11 @@ Confirm PIN Re-enter your PIN to confirm PINs do not match. Please try again. + + Kasa Kilidini Aç + Enter your master password + Parola + Kilidi aç + Parola yanlış. Lütfen yeniden deneyin. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-uk/strings.xml b/apps/mobile-app/android/app/src/main/res/values-uk/strings.xml index 4b60d2dd5..a56fa55e0 100644 --- a/apps/mobile-app/android/app/src/main/res/values-uk/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-uk/strings.xml @@ -7,6 +7,7 @@ Закрити Далі Скасувати + Назад Сталася невідома помилка Не вдалося отримати, відкрийте додаток @@ -81,4 +82,11 @@ Підтвердити ПІН-код Введіть PIN-код ще раз для підтвердження PIN-коди не збігаються. Спробуйте ще раз. + + Розблокувати Vault + Введіть ваш головний пароль + Пароль + Розблокувати + Невірний пароль. Будь ласка, спробуйте ще раз. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-ur/strings.xml b/apps/mobile-app/android/app/src/main/res/values-ur/strings.xml index bf601fe82..b901fc851 100644 --- a/apps/mobile-app/android/app/src/main/res/values-ur/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-ur/strings.xml @@ -7,6 +7,7 @@ بند کریں آگے جائیں منسوخ کریں + واپس جائیں An unknown error occurred Failed to retrieve, open app @@ -81,4 +82,11 @@ Confirm PIN Re-enter your PIN to confirm PINs do not match. Please try again. + + Unlock Vault + Enter your master password + پاس ورڈ + Unlock + Incorrect password. Please try again. + Failed to verify password diff --git a/apps/mobile-app/android/app/src/main/res/values-zh/strings.xml b/apps/mobile-app/android/app/src/main/res/values-zh/strings.xml index c00a29ee4..f47a20e5a 100644 --- a/apps/mobile-app/android/app/src/main/res/values-zh/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values-zh/strings.xml @@ -7,6 +7,7 @@ 关闭 下一步 取消 + 返回 发生未知错误 检索失败,请打开应用 @@ -81,4 +82,11 @@ 确认 PIN 重新输入您的 PIN 以确认 PIN 不匹配,请重试。 + + 解锁密码库 + 输入您的主密码 + 密码 + 解锁 + 密码错误,请重试。 + 密码验证失败 diff --git a/apps/mobile-app/i18n/locales/bg.json b/apps/mobile-app/i18n/locales/bg.json index 92957ec71..fe65b4127 100644 --- a/apps/mobile-app/i18n/locales/bg.json +++ b/apps/mobile-app/i18n/locales/bg.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Verifying authentication code", "verify": "Провери", "unlockVault": "Unlock Vault", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Unlock with PIN", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Enter your password to unlock your vault", "enterPasswordPlaceholder": "Password", "enterAuthCode": "Enter 6-digit code", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Enter your password", "enableBiometric": "Enable {{biometric}}?", "biometricPrompt": "Would you like to use {{biometric}} to unlock your vault?", - "tryBiometricAgain": "Try {{biometric}} Again", - "tryPinAgain": "Try PIN Again", "authCodeNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.", "errors": { "credentialsRequired": "Username and password are required", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} is disabled for AliasVault. In order to use it, please enable it in your device settings first.", "biometricHelp": "Use biometrics to unlock your vault, which is secured by the {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} is not available. Tap to open settings and/or go to your device settings to enable and configure it.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "PIN Code", "pinDescription": "Use a custom PIN code to unlock your vault more quickly.", "pinEnabled": "PIN unlock enabled successfully", @@ -321,7 +322,8 @@ "confirmSubtitle": "Re-authenticate to approve login on another device.", "confirmMessage": "You are about to log in on a remote device with your account. This other device will have full access to your vault. Only proceed if you trust this device.", "successDescription": "The remote device has been successfully logged in.", - "requestExpired": "This login request has expired. Please generate a new QR code." + "requestExpired": "This login request has expired. Please generate a new QR code.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Username copied to clipboard", "emailCopied": "Email copied to clipboard", "passwordCopied": "Password copied to clipboard", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copied to clipboard" }, "createNewAliasFor": "Create new alias for", @@ -482,7 +485,8 @@ "delete": "Delete", "copyUsername": "Copy Username", "copyEmail": "Copy Email", - "copyPassword": "Copy Password" + "copyPassword": "Copy Password", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "URL Options", diff --git a/apps/mobile-app/i18n/locales/ca.json b/apps/mobile-app/i18n/locales/ca.json index 154d02369..895024e7e 100644 --- a/apps/mobile-app/i18n/locales/ca.json +++ b/apps/mobile-app/i18n/locales/ca.json @@ -4,7 +4,7 @@ "close": "Tanca", "delete": "Suprimeix", "save": "Desa", - "edit": "Edit", + "edit": "Edita", "yes": "Sí", "no": "No", "ok": "D'acord", @@ -48,7 +48,9 @@ "verifyingAuthCode": "Verifying authentication code", "verify": "Verifica", "unlockVault": "Unlock Vault", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Unlock with PIN", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Enter your password to unlock your vault", "enterPasswordPlaceholder": "Contrasenya", "enterAuthCode": "Introduïu el codi de 6 dígits", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Enter your password", "enableBiometric": "Enable {{biometric}}?", "biometricPrompt": "Would you like to use {{biometric}} to unlock your vault?", - "tryBiometricAgain": "Try {{biometric}} Again", - "tryPinAgain": "Try PIN Again", "authCodeNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.", "errors": { "credentialsRequired": "Username and password are required", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} is disabled for AliasVault. In order to use it, please enable it in your device settings first.", "biometricHelp": "Use biometrics to unlock your vault, which is secured by the {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} is not available. Tap to open settings and/or go to your device settings to enable and configure it.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "PIN Code", "pinDescription": "Use a custom PIN code to unlock your vault more quickly.", "pinEnabled": "PIN unlock enabled successfully", @@ -321,7 +322,8 @@ "confirmSubtitle": "Re-authenticate to approve login on another device.", "confirmMessage": "You are about to log in on a remote device with your account. This other device will have full access to your vault. Only proceed if you trust this device.", "successDescription": "The remote device has been successfully logged in.", - "requestExpired": "This login request has expired. Please generate a new QR code." + "requestExpired": "This login request has expired. Please generate a new QR code.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Username copied to clipboard", "emailCopied": "Email copied to clipboard", "passwordCopied": "Password copied to clipboard", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copied to clipboard" }, "createNewAliasFor": "Create new alias for", @@ -482,7 +485,8 @@ "delete": "Suprimeix", "copyUsername": "Copy Username", "copyEmail": "Copy Email", - "copyPassword": "Copy Password" + "copyPassword": "Copy Password", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "URL Options", diff --git a/apps/mobile-app/i18n/locales/cs.json b/apps/mobile-app/i18n/locales/cs.json index c1abb7eac..2098e5f2d 100644 --- a/apps/mobile-app/i18n/locales/cs.json +++ b/apps/mobile-app/i18n/locales/cs.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Verifying authentication code", "verify": "Verify", "unlockVault": "Unlock Vault", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Unlock with PIN", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Enter your password to unlock your vault", "enterPasswordPlaceholder": "Password", "enterAuthCode": "Enter 6-digit code", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Enter your password", "enableBiometric": "Enable {{biometric}}?", "biometricPrompt": "Would you like to use {{biometric}} to unlock your vault?", - "tryBiometricAgain": "Try {{biometric}} Again", - "tryPinAgain": "Try PIN Again", "authCodeNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.", "errors": { "credentialsRequired": "Username and password are required", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} is disabled for AliasVault. In order to use it, please enable it in your device settings first.", "biometricHelp": "Use biometrics to unlock your vault, which is secured by the {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} is not available. Tap to open settings and/or go to your device settings to enable and configure it.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "PIN Code", "pinDescription": "Use a custom PIN code to unlock your vault more quickly.", "pinEnabled": "PIN unlock enabled successfully", @@ -321,7 +322,8 @@ "confirmSubtitle": "Re-authenticate to approve login on another device.", "confirmMessage": "You are about to log in on a remote device with your account. This other device will have full access to your vault. Only proceed if you trust this device.", "successDescription": "The remote device has been successfully logged in.", - "requestExpired": "This login request has expired. Please generate a new QR code." + "requestExpired": "This login request has expired. Please generate a new QR code.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Username copied to clipboard", "emailCopied": "Email copied to clipboard", "passwordCopied": "Password copied to clipboard", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copied to clipboard" }, "createNewAliasFor": "Create new alias for", @@ -482,7 +485,8 @@ "delete": "Delete", "copyUsername": "Copy Username", "copyEmail": "Copy Email", - "copyPassword": "Copy Password" + "copyPassword": "Copy Password", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "URL Options", diff --git a/apps/mobile-app/i18n/locales/da.json b/apps/mobile-app/i18n/locales/da.json index 9c3d4c368..f66730d3b 100644 --- a/apps/mobile-app/i18n/locales/da.json +++ b/apps/mobile-app/i18n/locales/da.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Verificerer autentificeringskode", "verify": "Verificer", "unlockVault": "Lås boks op", + "unlockWithBiometric": "Lås op med {{biometric}}", "unlockWithPin": "Lås op med PIN", + "unlockWithPassword": "Lås op med adgangskode", "enterPassword": "Indtast din adgangskode for at låse din boks op", "enterPasswordPlaceholder": "Adgangskode", "enterAuthCode": "Indtast 6-cifret kode", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Indtast din adgangskode", "enableBiometric": "Aktiver {{biometric}}?", "biometricPrompt": "Vil du bruge {{biometric}} til at låse din boks op?", - "tryBiometricAgain": "Prøv {{biometric}} igen", - "tryPinAgain": "Prøv PIN igen", "authCodeNote": "Bemærk: Hvis du ikke har adgang til din autentificeringsenhed, kan du nulstille din 2FA med en gendannelseskode ved at logge ind via hjemmesiden.", "errors": { "credentialsRequired": "Brugernavn og adgangskode er påkrævet", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} er deaktiveret for AliasVault. For at bruge det skal du først aktivere det i din enheds indstillinger.", "biometricHelp": "Brug biometri til at låse din boks op, som er sikret af {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} er ikke tilgængelig. Tryk for at åbne indstillinger og/eller gå til din enheds indstillinger for at aktivere og konfigurere det.", + "featureRequiresPasscode": "Denne funktion kræver, at en adgangskode er aktiveret på din enhed.", "pin": "PIN-kode", "pinDescription": "Brug en brugerdefineret PIN-kode til at låse din boks hurtigere op.", "pinEnabled": "PIN-oplåsning aktiveret", @@ -321,7 +322,8 @@ "confirmSubtitle": "Autentificer igen for at godkende login på en anden enhed.", "confirmMessage": "Du er ved at logge ind på en ekstern enhed med din konto. Denne enhed vil have fuld adgang til din boks. Fortsæt kun, hvis du har tillid til denne enhed.", "successDescription": "Den eksterne enhed er blevet logget ind korrekt.", - "requestExpired": "Denne loginanmodning er udløbet. Generer venligst en ny QR-kode." + "requestExpired": "Denne loginanmodning er udløbet. Generer venligst en ny QR-kode.", + "noPinOrBiometricError": "Mobiloplåsning kræver PIN-kode eller biometrisk godkendelse. Aktivér PIN-kode eller Face ID/Touch ID i indstillinger for oplåsning af boksen for at bruge denne funktion." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Brugernavn kopieret til udklipsholder", "emailCopied": "E-mail kopieret til udklipsholder", "passwordCopied": "Adgangskode kopieret til udklipsholder", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL kopieret til udklipsholder" }, "createNewAliasFor": "Opret nyt alias for", @@ -482,7 +485,8 @@ "delete": "Slet", "copyUsername": "Kopier brugernavn", "copyEmail": "Kopier e-mail", - "copyPassword": "Kopier adgangskode" + "copyPassword": "Kopier adgangskode", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "Url Indstillinger", diff --git a/apps/mobile-app/i18n/locales/de.json b/apps/mobile-app/i18n/locales/de.json index 5c69e19f9..4169fac2b 100644 --- a/apps/mobile-app/i18n/locales/de.json +++ b/apps/mobile-app/i18n/locales/de.json @@ -25,7 +25,7 @@ "add": "Hinzufügen", "generate": "Generieren", "attachments": "Anhänge", - "or": "or", + "or": "oder", "deleteItemConfirmTitle": "Eintrag löschen", "deleteItemConfirmDescription": "Bist Du sicher, dass Du diesen Eintrag löschen möchtest?", "errors": { @@ -48,7 +48,9 @@ "verifyingAuthCode": "Sicherheits-Code wird überprüft", "verify": "Bestätige", "unlockVault": "Tresor entsperren", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Mit PIN entsperren", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Bitte gib Dein Passwort zum Entsperren des Tresors ein", "enterPasswordPlaceholder": "Passwort", "enterAuthCode": "Gib den 6-stelligen Sicherheits-Code ein.", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Gib Dein Passwort ein", "enableBiometric": "{{biometric}} aktivieren?", "biometricPrompt": "Möchtest Du Deinen Tresor mit {{biometric}} entsperren?", - "tryBiometricAgain": "{{biometric}} erneut versuchen", - "tryPinAgain": "PIN erneut versuchen", "authCodeNote": "Hinweis: Wenn Du keinen Zugriff auf Dein Authentifizierungsgerät hast, kannst Du Deine Zwei-Faktor-Authentifizierung (2FA) mit einem Wiederherstellungscode zurücksetzen, indem Du Dich über die Website anmeldest.", "errors": { "credentialsRequired": "Benutzername und Passwort sind erforderlich", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} ist für AliasVault deaktiviert. Bitte aktiviere es zuerst in den Geräteeinstellungen, um es zu verwenden.", "biometricHelp": "Benutze Biometrie, um Deinen Tresor zu entsperren. Dieser ist durch den {{keystore}} gesichert ist.", "biometricUnavailableHelp": "{{biometric}} ist nicht verfügbar. Tippe, um die Einstellungen zu öffnen und/oder gehe zu den Geräteeinstellungen, um es zu aktivieren und zu konfigurieren.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "PIN-Code", "pinDescription": "Benutze einen eigenen PIN-Code, um Deinen Tresor schneller zu entsperren.", "pinEnabled": "PIN-Sperre erfolgreich aktiviert", @@ -321,7 +322,8 @@ "confirmSubtitle": "Authentifizieren Dich erneut, um die Anmeldung auf einem anderen Gerät zu genehmigen.", "confirmMessage": "Du bist dabei, Dich mit Deinem Konto auf einem anderen Gerät anzumelden. Dieses Gerät wird vollen Zugriff auf Deinen Tresor haben. Fahre nur fort, wenn Du diesem Gerät vertraust.", "successDescription": "Das andere Gerät wurde erfolgreich angemeldet.", - "requestExpired": "Diese Anmeldeanfrage ist abgelaufen. Bitte generiere einen neuen QR-Code." + "requestExpired": "Diese Anmeldeanfrage ist abgelaufen. Bitte generiere einen neuen QR-Code.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Nutzername in die Zwischenablage kopiert", "emailCopied": "E-Mail in die Zwischenablage kopiert", "passwordCopied": "Passwort in die Zwischenablage kopiert", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copied to clipboard" }, "createNewAliasFor": "Neuen Alias für", @@ -482,7 +485,8 @@ "delete": "Löschen", "copyUsername": "Benutzername kopieren", "copyEmail": "E-Mail-Adresse kopieren", - "copyPassword": "Passwort kopieren" + "copyPassword": "Passwort kopieren", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "URL Options", diff --git a/apps/mobile-app/i18n/locales/es.json b/apps/mobile-app/i18n/locales/es.json index 335ab22bb..0c1211c1d 100644 --- a/apps/mobile-app/i18n/locales/es.json +++ b/apps/mobile-app/i18n/locales/es.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Verificando código de autenticación", "verify": "Verificar", "unlockVault": "Desbloquear bóveda", + "unlockWithBiometric": "Desbloquear con {{biometric}}", "unlockWithPin": "Desbloquear con PIN", + "unlockWithPassword": "Desbloquear con contraseña", "enterPassword": "Introduzca su contraseña para desbloquear su bóveda", "enterPasswordPlaceholder": "Contraseña", "enterAuthCode": "Introduzca código de 6 dígitos", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Introduzca su contraseña", "enableBiometric": "¿Activar {{biometric}}?", "biometricPrompt": "¿Quieres usar {{biometric}} para desbloquear tu bóveda?", - "tryBiometricAgain": "Prueba {{biometric}} de nuevo", - "tryPinAgain": "Intentar PIN de nuevo", "authCodeNote": "Nota: si no tiene acceso a su dispositivo de autenticación, puede restablecer su 2FA con un código de recuperación iniciando sesión a través del sitio web.", "errors": { "credentialsRequired": "Se requiere nombre de usuario y contraseña", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} está deshabilitado para AliasVault. Para usarlo, por favor habilítelo en la configuración de tu dispositivo.", "biometricHelp": "Usa los datos biométricos para desbloquear tu bóveda que está asegurada por la {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} no está disponible. Toca para abrir ajustes y/o ve a la configuración de tu dispositivo para activarlo y configurarlo.", + "featureRequiresPasscode": "Esta característica requiere que se habilite un código de acceso en su dispositivo.", "pin": "Código PIN", "pinDescription": "Usa un código PIN personalizado para desbloquear tu bóveda más rápidamente.", "pinEnabled": "Desbloqueo de PIN activado correctamente", @@ -321,7 +322,8 @@ "confirmSubtitle": "Volver a autenticar para aprobar el inicio de sesión en otro dispositivo.", "confirmMessage": "Estás a punto de iniciar sesión en un dispositivo remoto con tu cuenta. Este otro dispositivo tendrá acceso completo a su bóveda. Sólo proceda si confía en este dispositivo.", "successDescription": "El dispositivo remoto se ha conectado correctamente.", - "requestExpired": "Esta solicitud de inicio de sesión ha caducado. Por favor, genere un nuevo código QR." + "requestExpired": "Esta solicitud de inicio de sesión ha caducado. Por favor, genere un nuevo código QR.", + "noPinOrBiometricError": "El desbloqueo móvil requiere PIN o autenticación biométrica. Por favor, habilite PIN o Face ID/Touch ID en la configuración de desbloqueo de bóveda para utilizar esta función." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Nombre de usuario copiado al portapapeles", "emailCopied": "Correo copiado al portapapeles", "passwordCopied": "Contraseña copiada al portapapeles", + "totpCodeCopied": "Código TOTP copiado al portapapeles", "urlCopied": "URL copiada al portapapeles" }, "createNewAliasFor": "Crear nuevo alias para", @@ -482,7 +485,8 @@ "delete": "Eliminar", "copyUsername": "Copiar nombre de usuario", "copyEmail": "Copiar correo", - "copyPassword": "Copiar Contraseña" + "copyPassword": "Copiar Contraseña", + "copyTotpCode": "Copiar código TOTP" }, "urlContextMenu": { "title": "Opciones de URL", diff --git a/apps/mobile-app/i18n/locales/fa.json b/apps/mobile-app/i18n/locales/fa.json index c1abb7eac..2098e5f2d 100644 --- a/apps/mobile-app/i18n/locales/fa.json +++ b/apps/mobile-app/i18n/locales/fa.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Verifying authentication code", "verify": "Verify", "unlockVault": "Unlock Vault", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Unlock with PIN", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Enter your password to unlock your vault", "enterPasswordPlaceholder": "Password", "enterAuthCode": "Enter 6-digit code", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Enter your password", "enableBiometric": "Enable {{biometric}}?", "biometricPrompt": "Would you like to use {{biometric}} to unlock your vault?", - "tryBiometricAgain": "Try {{biometric}} Again", - "tryPinAgain": "Try PIN Again", "authCodeNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.", "errors": { "credentialsRequired": "Username and password are required", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} is disabled for AliasVault. In order to use it, please enable it in your device settings first.", "biometricHelp": "Use biometrics to unlock your vault, which is secured by the {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} is not available. Tap to open settings and/or go to your device settings to enable and configure it.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "PIN Code", "pinDescription": "Use a custom PIN code to unlock your vault more quickly.", "pinEnabled": "PIN unlock enabled successfully", @@ -321,7 +322,8 @@ "confirmSubtitle": "Re-authenticate to approve login on another device.", "confirmMessage": "You are about to log in on a remote device with your account. This other device will have full access to your vault. Only proceed if you trust this device.", "successDescription": "The remote device has been successfully logged in.", - "requestExpired": "This login request has expired. Please generate a new QR code." + "requestExpired": "This login request has expired. Please generate a new QR code.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Username copied to clipboard", "emailCopied": "Email copied to clipboard", "passwordCopied": "Password copied to clipboard", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copied to clipboard" }, "createNewAliasFor": "Create new alias for", @@ -482,7 +485,8 @@ "delete": "Delete", "copyUsername": "Copy Username", "copyEmail": "Copy Email", - "copyPassword": "Copy Password" + "copyPassword": "Copy Password", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "URL Options", diff --git a/apps/mobile-app/i18n/locales/fi.json b/apps/mobile-app/i18n/locales/fi.json index aaae8c3e8..e8d2d12bc 100644 --- a/apps/mobile-app/i18n/locales/fi.json +++ b/apps/mobile-app/i18n/locales/fi.json @@ -4,7 +4,7 @@ "close": "Sulje", "delete": "Poista", "save": "Tallenna", - "edit": "Edit", + "edit": "Muokkaa", "yes": "Kyllä", "no": "Ei", "ok": "Hyvä on", @@ -25,7 +25,7 @@ "add": "Lisää", "generate": "Generate", "attachments": "Liitteet", - "or": "or", + "or": "tai", "deleteItemConfirmTitle": "Poista kohde", "deleteItemConfirmDescription": "Haluatko varmasti poistaa tämän kohteen?", "errors": { @@ -48,7 +48,9 @@ "verifyingAuthCode": "Todennuskoodia tarkistetaan", "verify": "Tarkista", "unlockVault": "Avaa holvin lukitus", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Avaa PIN-koodilla", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Syötä salasanasi avataksesi holvisi lukituksen", "enterPasswordPlaceholder": "Salasana", "enterAuthCode": "Syötä 6-numeroinen koodi", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Syötä salasanasi", "enableBiometric": "Otetaanko {{biometric}} käyttöön?", "biometricPrompt": "Haluatko käyttää {{biometric}} avataksesi holvisi lukituksen?", - "tryBiometricAgain": "Yritä {{biometric}} uudelleen", - "tryPinAgain": "Kokeile PIN-koodia uudelleen", "authCodeNote": "Huomautus: Jos sinulla ei ole pääsyä varmennuslaitteeseesi, voit palauttaa kaksivaiheisen varmennuksen (2FA) kirjautumalla palautuskoodilla sisään verkkosivuston kautta.", "errors": { "credentialsRequired": "Käyttäjänimi ja salasana vaaditaan", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} on poistettu käytöstä AliasVaultin osalta. Ota se ensin käyttöön laitteen asetuksissa.", "biometricHelp": "Käytä biometriikkaa avataksesi holvisi, joka on suojattu {{keystore}} -tunnuksella.", "biometricUnavailableHelp": "{{biometric}} ei ole käytettävissä. Avaa asetukset napauttamalla ja/tai siirry laitteen asetuksiin ottaaksesi sen käyttöön ja määrittääksesi sen.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "PIN-koodi", "pinDescription": "Käytä mukautettua PIN-koodia avataksesi holvisi nopeammin.", "pinEnabled": "PIN-lukituksen avaus käytössä onnistuneesti", @@ -321,7 +322,8 @@ "confirmSubtitle": "Tunnistaudu uudelleen hyväksyäksesi kirjautuminen toisella laitteella.", "confirmMessage": "Olet kirjautumamassa sisään etälaitteeseen, jossa on oma tilisi. Tällä toisella laitteella on täysi pääsy holviisi. Jatka vain, jos luotat tähän laitteeseen.", "successDescription": "Etälaite on onnistuneesti kirjautunut sisään.", - "requestExpired": "Tämä kirjautumispyyntö on vanhentunut. Luo uusi QR-koodi." + "requestExpired": "Tämä kirjautumispyyntö on vanhentunut. Luo uusi QR-koodi.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Käyttäjänimi kopioitu leikepöydälle", "emailCopied": "Sähköpostiosoite kopioitu leikepöydälle", "passwordCopied": "Salasana kopioitu leikepöydälle", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copied to clipboard" }, "createNewAliasFor": "Luo uusi alias kohteelle", @@ -482,7 +485,8 @@ "delete": "Poista", "copyUsername": "Kopioi käyttäjänimi", "copyEmail": "Kopioi sähköposti", - "copyPassword": "Kopioi salasana" + "copyPassword": "Kopioi salasana", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "URL Options", diff --git a/apps/mobile-app/i18n/locales/fr.json b/apps/mobile-app/i18n/locales/fr.json index 69545b1b6..d3e40a2e1 100644 --- a/apps/mobile-app/i18n/locales/fr.json +++ b/apps/mobile-app/i18n/locales/fr.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Vérification du code d'authentification", "verify": "Vérifier", "unlockVault": "Déverrouiller le coffre", + "unlockWithBiometric": "Déverrouiller avec {{biometric}}", "unlockWithPin": "Déverrouiller avec un code PIN", + "unlockWithPassword": "Déverrouiller avec le mot de passe", "enterPassword": "Entrez votre mot de passe principal pour déverrouiller votre coffre-fort", "enterPasswordPlaceholder": "Mot de passe", "enterAuthCode": "Saisissez le code à 6 chiffres", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Saisissez votre mot de passe", "enableBiometric": "Activer {{biometric}}?", "biometricPrompt": "Voulez-vous utiliser {{biometric}} pour déverrouiller votre coffre ?", - "tryBiometricAgain": "Réessayez {{biometric}}", - "tryPinAgain": "Réessayez le PIN", "authCodeNote": "Remarque : si vous n'avez pas accès à votre appareil d'authentification, vous pouvez réinitialiser votre authentification 2FA avec un code de récupération en vous connectant via le site web.", "errors": { "credentialsRequired": "Le nom d'utilisateur et le mot de passe sont requis", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} est désactivé pour AliasVault. Pour l'utiliser, veuillez d'abord l'activer dans les paramètres de votre appareil.", "biometricHelp": "Utilisez la biométrie pour déverrouiller votre coffre, qui est sécurisé par l’ {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} n'est pas disponible. Appuyez pour ouvrir les paramètres et/ou allez dans les paramètres de votre appareil pour l'activer et le configurer.", + "featureRequiresPasscode": "Cette fonctionnalité nécessite un code d'accès pour être activée sur votre appareil.", "pin": "Code PIN", "pinDescription": "Utilisez un code PIN personnalisé pour déverrouiller votre coffre plus rapidement.", "pinEnabled": "Déverrouillage par code PIN activé avec succès", @@ -321,7 +322,8 @@ "confirmSubtitle": "Se ré-authentifier pour approuver la connexion sur un autre appareil.", "confirmMessage": "Vous êtes sur le point de vous connecter sur un appareil distant avec votre compte. Cet autre appareil aura un accès complet à votre coffre. Ne procédez que si vous faites confiance à cet appareil.", "successDescription": "L'appareil distant a été connecté avec succès.", - "requestExpired": "Cette demande de connexion a expiré. Veuillez générer un nouveau code QR." + "requestExpired": "Cette demande de connexion a expiré. Veuillez générer un nouveau code QR.", + "noPinOrBiometricError": "Le déverrouillage par mobile nécessite une authentification par code PIN ou biométrique. Veuillez activer le code PIN ou Face ID/Touch ID dans les paramètres de déverrouillage du coffre pour utiliser cette fonctionnalité." } } }, @@ -334,7 +336,7 @@ "creating": "Création", "editing": "Édition", "login": { - "title": "Se connecter" + "title": "Identifiant" }, "alias": { "title": "Alias" @@ -413,7 +415,7 @@ "privateEmailDescription": "E2E chiffré, entièrement privé.", "publicEmailTitle": "Fournisseurs d'email public temporaires", "publicEmailDescription": "Anonyme mais confidentiel limitée. Le contenu des e-mails est lisible par toute personne qui connaît l'adresse.", - "searchPlaceholder": "Rechercher dans le coffre...", + "searchPlaceholder": "Rechercher élément...", "noMatchingItems": "Aucun élément ne correspond au filtre sélectionné.", "noMatchingItemsSearch": "Aucun élément correspondant à \"{{search}}\"", "noMatchingItemsWithFilter": "Aucun élément {{filter}} correspondant à \"{{search}}\"", @@ -473,6 +475,7 @@ "usernameCopied": "Nom d'utilisateur copié dans le presse-papiers", "emailCopied": "E-mail copié dans le presse-papiers", "passwordCopied": "Mot de passe copié dans le presse-papiers", + "totpCodeCopied": "Code TOTP copié dans le presse-papiers", "urlCopied": "URL copiée dans le presse-papier" }, "createNewAliasFor": "Créer un nouvel alias pour", @@ -482,7 +485,8 @@ "delete": "Supprimer", "copyUsername": "Copier le nom d'utilisateur", "copyEmail": "Copier l'e-mail", - "copyPassword": "Copier le mot de passe" + "copyPassword": "Copier le mot de passe", + "copyTotpCode": "Copier le code TOTP" }, "urlContextMenu": { "title": "Options de l'URL", diff --git a/apps/mobile-app/i18n/locales/he.json b/apps/mobile-app/i18n/locales/he.json index 19a6396de..bda4a465e 100644 --- a/apps/mobile-app/i18n/locales/he.json +++ b/apps/mobile-app/i18n/locales/he.json @@ -4,7 +4,7 @@ "close": "סגירה", "delete": "מחיקה", "save": "שמירה", - "edit": "Edit", + "edit": "עריכה", "yes": "כן", "no": "לא", "ok": "אישור", @@ -25,7 +25,7 @@ "add": "Add", "generate": "Generate", "attachments": "צרופות", - "or": "or", + "or": "או", "deleteItemConfirmTitle": "Delete Item", "deleteItemConfirmDescription": "Are you sure you want to delete this item?", "errors": { @@ -48,7 +48,9 @@ "verifyingAuthCode": "קוד האימות נבדק", "verify": "אימות", "unlockVault": "שחרור נעילת כספת", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "שחרור נעילה עם קוד אישי", + "unlockWithPassword": "Unlock with Password", "enterPassword": "נא למלא את הסיסמה שלך כדי לשחרר את הכספת שלך", "enterPasswordPlaceholder": "סיסמה", "enterAuthCode": "נא למלא קוד באורך 6 ספרות", @@ -56,8 +58,6 @@ "passwordPlaceholder": "נא למלא את הסיסמה שלך", "enableBiometric": "להפעיל את {{biometric}}?", "biometricPrompt": "להשתמש ב־{{biometric}} כדי לשחרר את נעילת הכספת שלך?", - "tryBiometricAgain": "לנסות {{biometric}} שוב", - "tryPinAgain": "Try PIN Again", "authCodeNote": "לתשומת ליבך: אם אין לך גישה להתקן המאמת (authenticator) שלך, אפשר לאפס אימות דו־שלבי עם קוד שחזור על ידי כניסה דרך האתר.", "errors": { "credentialsRequired": "שם המשתמש והסיסמה הם חובה", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} מושבת עבור AliasVault. כדי להשתמש בו, נא להפעיל אותו בהגדרות ההתקן תחילה.", "biometricHelp": "Use biometrics to unlock your vault, which is secured by the {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} לא זמין. נא לגעת כדי לפתוח את ההגדרות ו/או לגשת להגדרות המכשיר שלך כדי להפעיל ולהגדיר אותו.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "קוד גישה אישי", "pinDescription": "Use a custom PIN code to unlock your vault more quickly.", "pinEnabled": "שחרור קוד אישי הופעל בהצלחה", @@ -321,7 +322,8 @@ "confirmSubtitle": "Re-authenticate to approve login on another device.", "confirmMessage": "You are about to log in on a remote device with your account. This other device will have full access to your vault. Only proceed if you trust this device.", "successDescription": "The remote device has been successfully logged in.", - "requestExpired": "This login request has expired. Please generate a new QR code." + "requestExpired": "This login request has expired. Please generate a new QR code.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "שם המשתמש הועתק ללוח הגזירים", "emailCopied": "הדוא״ל הועתק ללוח הגזירים", "passwordCopied": "הסיסמה הועתקה ללוח הגזירים", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copied to clipboard" }, "createNewAliasFor": "יצירת כינוי חדש עבור", @@ -482,7 +485,8 @@ "delete": "מחיקה", "copyUsername": "העתקת שם משתמש", "copyEmail": "העתקת דוא״ל", - "copyPassword": "העתקת סיסמה" + "copyPassword": "העתקת סיסמה", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "URL Options", diff --git a/apps/mobile-app/i18n/locales/it.json b/apps/mobile-app/i18n/locales/it.json index 281def49f..d29694356 100644 --- a/apps/mobile-app/i18n/locales/it.json +++ b/apps/mobile-app/i18n/locales/it.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Verifica codice di autenticazione", "verify": "Verifica", "unlockVault": "Sblocca Cassaforte", + "unlockWithBiometric": "Sblocca con {{biometric}}", "unlockWithPin": "Sblocca con PIN", + "unlockWithPassword": "Sblocca con Password", "enterPassword": "Inserisci la tua password per sbloccare la cassaforte", "enterPasswordPlaceholder": "Password", "enterAuthCode": "Inserire codice a 6 cifre", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Inserisci la tua password", "enableBiometric": "Abilitare {{biometric}}?", "biometricPrompt": "Vuoi usare {{biometric}} per sbloccare la tua cassaforte?", - "tryBiometricAgain": "Riprova {{biometric}}", - "tryPinAgain": "Prova di nuovo il PIN", "authCodeNote": "Nota: se non hai accesso al tuo dispositivo di autenticazione, puoi reimpostare il 2FA con un codice di recupero accedendo via sito web.", "errors": { "credentialsRequired": "Nome utente e password sono richiesti", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} è disabilitato per AliasVault. Per usarlo, abilitalo prima nelle impostazioni del dispositivo.", "biometricHelp": "Usa la biometria per sbloccare la tua cassaforte, protetta dall' {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} non è disponibile. Tocca per aprire le impostazioni o vai alle impostazioni del dispositivo per abilitarlo e configurarlo.", + "featureRequiresPasscode": "Questa funzione richiede che un codice di accesso sia abilitato sul tuo dispositivo.", "pin": "Codice PIN", "pinDescription": "Usa un codice PIN personalizzato per sbloccare la cassaforte più rapidamente.", "pinEnabled": "Sblocco PIN abilitato correttamente", @@ -321,7 +322,8 @@ "confirmSubtitle": "Ri-autenticazione per approvare il login su un altro dispositivo.", "confirmMessage": "Stai per accedere a un dispositivo remoto con il tuo account. Questo altro dispositivo avrà pieno accesso alla tua cassaforte. Procedi solo se ti fidi di questo dispositivo.", "successDescription": "Il dispositivo remoto è stato loggato con successo.", - "requestExpired": "Questa richiesta di accesso è scaduta. Si prega di generare un nuovo codice QR." + "requestExpired": "Questa richiesta di accesso è scaduta. Si prega di generare un nuovo codice QR.", + "noPinOrBiometricError": "Sblocco mobile richiede PIN o autenticazione biometrica. Si prega di abilitare il PIN o Face ID/Touch ID nelle impostazioni di sblocco cassaforte per utilizzare questa funzionalità." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Nome utente copiato negli appunti", "emailCopied": "Email copiata negli appunti", "passwordCopied": "Password copiata negli appunti", + "totpCodeCopied": "Codice TOTP copiato negli appunti", "urlCopied": "URL copiato negli appunti" }, "createNewAliasFor": "Crea un nuovo alias per", @@ -482,7 +485,8 @@ "delete": "Elimina", "copyUsername": "Copia Nome Utente", "copyEmail": "Copia E-mail", - "copyPassword": "Copia Password" + "copyPassword": "Copia Password", + "copyTotpCode": "Copia Codice Totp" }, "urlContextMenu": { "title": "Opzioni URL", diff --git a/apps/mobile-app/i18n/locales/ko.json b/apps/mobile-app/i18n/locales/ko.json index c1abb7eac..2098e5f2d 100644 --- a/apps/mobile-app/i18n/locales/ko.json +++ b/apps/mobile-app/i18n/locales/ko.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Verifying authentication code", "verify": "Verify", "unlockVault": "Unlock Vault", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Unlock with PIN", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Enter your password to unlock your vault", "enterPasswordPlaceholder": "Password", "enterAuthCode": "Enter 6-digit code", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Enter your password", "enableBiometric": "Enable {{biometric}}?", "biometricPrompt": "Would you like to use {{biometric}} to unlock your vault?", - "tryBiometricAgain": "Try {{biometric}} Again", - "tryPinAgain": "Try PIN Again", "authCodeNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.", "errors": { "credentialsRequired": "Username and password are required", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} is disabled for AliasVault. In order to use it, please enable it in your device settings first.", "biometricHelp": "Use biometrics to unlock your vault, which is secured by the {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} is not available. Tap to open settings and/or go to your device settings to enable and configure it.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "PIN Code", "pinDescription": "Use a custom PIN code to unlock your vault more quickly.", "pinEnabled": "PIN unlock enabled successfully", @@ -321,7 +322,8 @@ "confirmSubtitle": "Re-authenticate to approve login on another device.", "confirmMessage": "You are about to log in on a remote device with your account. This other device will have full access to your vault. Only proceed if you trust this device.", "successDescription": "The remote device has been successfully logged in.", - "requestExpired": "This login request has expired. Please generate a new QR code." + "requestExpired": "This login request has expired. Please generate a new QR code.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Username copied to clipboard", "emailCopied": "Email copied to clipboard", "passwordCopied": "Password copied to clipboard", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copied to clipboard" }, "createNewAliasFor": "Create new alias for", @@ -482,7 +485,8 @@ "delete": "Delete", "copyUsername": "Copy Username", "copyEmail": "Copy Email", - "copyPassword": "Copy Password" + "copyPassword": "Copy Password", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "URL Options", diff --git a/apps/mobile-app/i18n/locales/nl.json b/apps/mobile-app/i18n/locales/nl.json index d4860e42d..ec59cfa45 100644 --- a/apps/mobile-app/i18n/locales/nl.json +++ b/apps/mobile-app/i18n/locales/nl.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Authenticatiecode verifiëren", "verify": "Verifiëren", "unlockVault": "Vault ontgrendelen", + "unlockWithBiometric": "Ontgrendel met {{biometric}}", "unlockWithPin": "Ontgrendelen met PIN", + "unlockWithPassword": "Ontgrendel met wachtwoord", "enterPassword": "Voer je wachtwoord in om je vault te ontgrendelen", "enterPasswordPlaceholder": "Wachtwoord", "enterAuthCode": "Voer 6-cijferige code in", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Voer je wachtwoord in", "enableBiometric": "{{biometric}} inschakelen?", "biometricPrompt": "Wil je {{biometric}} gebruiken om je vault te ontgrendelen?", - "tryBiometricAgain": "Probeer {{biometric}} Opnieuw", - "tryPinAgain": "Probeer PIN opnieuw", "authCodeNote": "Opmerking: als je geen toegang hebt tot je authenticator, kunt je je 2FA resetten door met een herstelcode in te loggen via de website.", "errors": { "credentialsRequired": "Gebruikersnaam en wachtwoord zijn vereist", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} is uitgeschakeld voor AliasVault. Ga naar de app instellingen om dit in te schakelen.", "biometricHelp": "Gebruik biometrische authenticatie om je kluis te ontgrendelen, die is beveiligd door de {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} is niet beschikbaar. Tik om instellingen te openen en/of ga naar je apparaat-instellingen om het in te schakelen en te configureren.", + "featureRequiresPasscode": "Deze functie vereist een toegangscode om te kunnen worden ingeschakeld op uw apparaat.", "pin": "Pincode", "pinDescription": "Gebruik een eigen pincode om je vault sneller te ontgrendelen.", "pinEnabled": "Pincode succesvol ingeschakeld", @@ -321,7 +322,8 @@ "confirmSubtitle": "Verifieer om login goed te keuren op een ander apparaat.", "confirmMessage": "Je staat op het punt om in te loggen op een ander apparaat met je account. Dit andere apparaat krijgt volledige toegang tot je vault. Ga alleen verder als je dit apparaat vertrouwd.", "successDescription": "Succesvol ingelogd op het andere apparaat.", - "requestExpired": "Dit loginverzoek is verlopen. Genereer een nieuwe QR-code." + "requestExpired": "Dit loginverzoek is verlopen. Genereer een nieuwe QR-code.", + "noPinOrBiometricError": "Ontgrendelen met mobiel vereist PIN of biometrische verificatie. Schakel PIN of Face ID/Touch ID in om deze functie te gebruiken." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Gebruikersnaam gekopieerd naar klembord", "emailCopied": "E-mail gekopieerd naar klembord", "passwordCopied": "Wachtwoord gekopieerd naar klembord", + "totpCodeCopied": "TOTP-code gekopieerd naar klembord", "urlCopied": "URL gekopieerd naar klembord" }, "createNewAliasFor": "Nieuwe alias maken voor", @@ -482,7 +485,8 @@ "delete": "Verwijderen", "copyUsername": "Kopieer gebruikersnaam", "copyEmail": "Kopieer e-mail", - "copyPassword": "Kopieer wachtwoord" + "copyPassword": "Kopieer wachtwoord", + "copyTotpCode": "Kopieer TOTP Code" }, "urlContextMenu": { "title": "URL opties", diff --git a/apps/mobile-app/i18n/locales/pl.json b/apps/mobile-app/i18n/locales/pl.json index ef164955d..b3db0de25 100644 --- a/apps/mobile-app/i18n/locales/pl.json +++ b/apps/mobile-app/i18n/locales/pl.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Weryfikacja kodu uwierzytelniającego", "verify": "Weryfikacja", "unlockVault": "Odblokuj sejf", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Odblokuj za pomocą kodu PIN", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Wprowadź hasło, aby odblokować sejf", "enterPasswordPlaceholder": "Hasło", "enterAuthCode": "Wprowadź 6-cyfrowy kod", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Wprowadź swoje hasło", "enableBiometric": "Włączyć {{biometric}}?", "biometricPrompt": "Czy chcesz użyć {{biometric}} do odblokowania sejfu?", - "tryBiometricAgain": "Spróbuj ponownie {{biometric}}", - "tryPinAgain": "Spróbuj ponownie wprowadzić kod PIN", "authCodeNote": "Uwaga: jeśli nie masz dostępu do urządzenia uwierzytelniającego, możesz zresetować kod 2FA za pomocą kodu odzyskiwania, logując się przez stronę internetową.", "errors": { "credentialsRequired": "Wymagana jest nazwa użytkownika i hasło", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "Funkcja {{biometric}} jest wyłączona dla AliasVault. Aby z niej skorzystać, należy najpierw włączyć ją w ustawieniach urządzenia.", "biometricHelp": "Użyj danych biometrycznych, aby odblokować sejf zabezpieczony przez {{keystore}}.", "biometricUnavailableHelp": "Funkcja {{biometric}} jest niedostępna. Dotknij, aby otworzyć ustawienia i/lub przejdź do ustawień urządzenia, aby ją włączyć.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "Kod PIN", "pinDescription": "Użyj niestandardowego kodu PIN, aby szybciej odblokować sejf.", "pinEnabled": "Odblokowanie za pomocą kodu PIN zakończone sukcesem", @@ -321,7 +322,8 @@ "confirmSubtitle": "Ponownie uwierzytelnij się, aby zatwierdzić logowanie na innym urządzeniu.", "confirmMessage": "Zamierzasz zalogować się na zdalnym urządzeniu przy użyciu swojego konta. To urządzenie będzie miało pełny dostęp do Twojego sejfu. Kontynuuj tylko wtedy, gdy ufasz temu urządzeniu.", "successDescription": "Udało się zalogować do urządzenia zdalnego.", - "requestExpired": "To żądanie logowania wygasło. Wygeneruj nowy kod QR." + "requestExpired": "To żądanie logowania wygasło. Wygeneruj nowy kod QR.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Nazwa użytkownika skopiowana do schowka", "emailCopied": "Wiadomość e-mail skopiowana do schowka", "passwordCopied": "Hasło skopiowane do schowka", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "Adres URL skopiowany do schowka" }, "createNewAliasFor": "Utwórz nowy alias dla", @@ -482,7 +485,8 @@ "delete": "Usuń", "copyUsername": "Skopiuj nazwę użytkownika", "copyEmail": "Skopiuj adres e-mail", - "copyPassword": "Skopiuj hasło" + "copyPassword": "Skopiuj hasło", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "Opcje adresu URL", diff --git a/apps/mobile-app/i18n/locales/pt.json b/apps/mobile-app/i18n/locales/pt.json index a55f04fba..6e6f1899c 100644 --- a/apps/mobile-app/i18n/locales/pt.json +++ b/apps/mobile-app/i18n/locales/pt.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Verificando código de autenticação", "verify": "Verificar", "unlockVault": "Desbloquear cofre", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Desbloquear com PIN", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Digite sua senha para desbloquear o cofre", "enterPasswordPlaceholder": "Senha", "enterAuthCode": "Digite o código de 6 dígitos", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Digite sua senha", "enableBiometric": "Habilitar {{biometric}}?", "biometricPrompt": "Gostaria de utilizar {{biometric}} para desbloquear seu cofre?", - "tryBiometricAgain": "Tente novamente com {{biometric}}", - "tryPinAgain": "Tente o PIN Novamente", "authCodeNote": "Nota: se você não tem acesso ao seu aparelho de verificação, você pode resetar seu 2FA com um código de recuperação fazendo login no site.", "errors": { "credentialsRequired": "Usuário e senha são obrigatórios", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} está desabitado para o AliasVault. Para utilizar, por favor primeiro habilite nas configurações do seu dispositivo.", "biometricHelp": "Utilize biometria para desbloquear seu cofre, que está protegido por {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} não está disponível. Clique para abrir as configurações e/ou vá às configurações do seu dispositivo para habilitar e configurar.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "Código PIN", "pinDescription": "Utilize um código PIN para desbloquear seu cofre mais rapidamente.", "pinEnabled": "Desbloqueio por PIN habilitado com sucesso", @@ -321,7 +322,8 @@ "confirmSubtitle": "Autentique novamente para aprovar login em outros dispositivos.", "confirmMessage": "Você está prestes a fazer login em um dispositivo remoto com sua conta. Este outro dispositivo terá acesso total ao seu cofre. Proceda apenas se você confia neste dispositivo.", "successDescription": "O login no dispositivo remoto foi feito com sucesso.", - "requestExpired": "Esta requisição de login expirou. Por favor, gere um novo código QR." + "requestExpired": "Esta requisição de login expirou. Por favor, gere um novo código QR.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Nome de usuário copiado para a área de transferência", "emailCopied": "E-mail copiado para a área de transferência", "passwordCopied": "Senha copiada para a área de transferência", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copiado para a área de transferência" }, "createNewAliasFor": "Criar novo alias para", @@ -482,7 +485,8 @@ "delete": "Excluir", "copyUsername": "Copiar Nome de Usuário", "copyEmail": "Copiar E-mail", - "copyPassword": "Copiar Senha" + "copyPassword": "Copiar Senha", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "Opções de URL", diff --git a/apps/mobile-app/i18n/locales/ro.json b/apps/mobile-app/i18n/locales/ro.json index 08e5c1b24..b623db1b6 100644 --- a/apps/mobile-app/i18n/locales/ro.json +++ b/apps/mobile-app/i18n/locales/ro.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Se verifică codul de autentificare", "verify": "Verifică", "unlockVault": "Deblochează seiful", + "unlockWithBiometric": "Deblochează cu {{biometric}}", "unlockWithPin": "Deblochează cu PIN", + "unlockWithPassword": "Deblochează cu parolă", "enterPassword": "Introdu parola pentru a debloca seiful", "enterPasswordPlaceholder": "Parolă", "enterAuthCode": "Introdu codul din 6 cifre", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Introdu parola", "enableBiometric": "Activezi {{biometric}}?", "biometricPrompt": "Dorești să utilizezi {{biometric}} pentru a debloca seiful?", - "tryBiometricAgain": "Încearcă din nou {{biometric}}", - "tryPinAgain": "Reîncearcă PIN-ul", "authCodeNote": "Notă: dacă nu ai acces la dispozitivul de autentificare, poți reseta 2FA cu un cod de recuperare autentificându-te prin site-ul web.", "errors": { "credentialsRequired": "Numele de utilizator și parola sunt obligatorii", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} dezactivată pentru AliasVault. Pentru a o utiliza, activează-o mai întâi din setările dispozitivului.", "biometricHelp": "Folosește biometria pentru a debloca seiful, securizat prin {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} indisponibilă. Apasă pentru a deschide setările.", + "featureRequiresPasscode": "Această funcție necesită un cod de acces pentru a fi activată pe dispozitivul tău.", "pin": "Cod PIN", "pinDescription": "Folosește un cod PIN personalizat pentru a debloca seiful mai rapid.", "pinEnabled": "Deblocarea prin PIN a fost activată cu succes", @@ -321,7 +322,8 @@ "confirmSubtitle": "Re-autentifică-te pentru a aproba logarea pe alt dispozitiv.", "confirmMessage": "Ești pe cale să te autentifici pe un dispozitiv la distanță. Acest dispozitiv va avea acces complet la seiful tău. Continuă doar dacă ai încredere în acel dispozitiv.", "successDescription": "Dispozitivul de la distanță a fost autentificat cu succes.", - "requestExpired": "Cererea de autentificare a expirat. Generează un cod QR nou." + "requestExpired": "Cererea de autentificare a expirat. Generează un cod QR nou.", + "noPinOrBiometricError": "Deblocarea pe mobil necesită PIN sau autentificare biometrică. Activează PIN-ul sau Face ID/Touch ID în setările de deblocare ale seifului pentru a folosi această funcție." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Utilizator copiat în clipboard", "emailCopied": "E-mail copiat în clipboard", "passwordCopied": "Parolă copiată în clipboard", + "totpCodeCopied": "Cod TOTP copiat în clipboard", "urlCopied": "URL copiat în clipboard" }, "createNewAliasFor": "Creează un alias nou pentru", @@ -482,7 +485,8 @@ "delete": "Șterge", "copyUsername": "Copiază utilizatorul", "copyEmail": "Copiază e-mailul", - "copyPassword": "Copiază parola" + "copyPassword": "Copiază parola", + "copyTotpCode": "Copiază codul TOTP" }, "urlContextMenu": { "title": "Opțiuni URL", diff --git a/apps/mobile-app/i18n/locales/ru.json b/apps/mobile-app/i18n/locales/ru.json index b4990b3ad..0804732f9 100644 --- a/apps/mobile-app/i18n/locales/ru.json +++ b/apps/mobile-app/i18n/locales/ru.json @@ -4,7 +4,7 @@ "close": "Закрыть", "delete": "Удалить", "save": "Сохранить", - "edit": "Edit", + "edit": "Редактировать", "yes": "Да", "no": "Нет", "ok": "ОК", @@ -25,7 +25,7 @@ "add": "Добавить", "generate": "Сгенерировать", "attachments": "Вложения", - "or": "or", + "or": "или", "deleteItemConfirmTitle": "Удалить запись", "deleteItemConfirmDescription": "Вы уверены, что хотите удалить эту запись?", "errors": { @@ -48,7 +48,9 @@ "verifyingAuthCode": "Проверка кода аутентификации", "verify": "Проверить", "unlockVault": "Разблокировать хранилище", + "unlockWithBiometric": "Разблокировать через {{biometric}}", "unlockWithPin": "Разблокировать ПИН-кодом", + "unlockWithPassword": "Разблокировать паролем", "enterPassword": "Введите ваш пароль для разблокировки вашего хранилища", "enterPasswordPlaceholder": "Пароль", "enterAuthCode": "Введите 6-значный код", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Введите ваш пароль", "enableBiometric": "Включить {{biometric}}?", "biometricPrompt": "Вы хотите использовать {{biometric}} для разблокировки вашего хранилища?", - "tryBiometricAgain": "Попробуйте {{biometric}} ещё раз", - "tryPinAgain": "Повторите ввод PIN-кода", "authCodeNote": "Примечание: если у вас нет доступа к устройству аутентификации, вы можете сбросить ваш 2FA с помощью кода восстановления, войдя в систему через сайт.", "errors": { "credentialsRequired": "Требуется имя пользователя и пароль", @@ -116,11 +116,11 @@ "secretKey": "Секретный ключ", "instructions": "Введите секретный ключ, указанный на веб-сайте, где вы хотите добавить двухфакторную аутентификацию.", "saveToViewCode": "Сохранить для просмотра кода", - "scanQrCode": "Scan QR Code", - "enterManually": "Enter a setup key", + "scanQrCode": "Отсканировать QR-код", + "enterManually": "Введите ключ настройки", "errors": { "invalidSecretKey": "Неверный формат секретного ключа.", - "scanFailed": "Failed to scan QR code. Please try again." + "scanFailed": "Не удалось отсканировать QR-код. Попробуйте еще раз." } }, "settings": { @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} отключен для AliasVault. Чтобы использовать его, пожалуйста, сначала включите его в настройках вашего устройства.", "biometricHelp": "Разблокируйте хранилище с помощью биометрии. Оно защищено {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} недоступен. Нажмите, чтобы открыть настройки, и/или перейдите в настройки своего устройства, чтобы включить и настроить его.", + "featureRequiresPasscode": "Для этой функции необходимо настроить код-пароль на вашем устройстве.", "pin": "ПИН-код", "pinDescription": "Используйте PIN-код для быстрой разблокировки хранилища.", "pinEnabled": "Вход по PIN-коду успешно включен", @@ -321,7 +322,8 @@ "confirmSubtitle": "Авторизуйтесь повторно, чтобы подтвердить вход на другом устройстве.", "confirmMessage": "Внимание! Вы входите в аккаунт на другом устройстве. Оно получит полный доступ к вашему хранилищу. Продолжайте только если полностью доверяете этому устройству.", "successDescription": "Вход на удаленном устройстве выполнен успешно.", - "requestExpired": "Срок действия запроса на вход истек. Пожалуйста, сгенерируйте новый QR-код." + "requestExpired": "Срок действия запроса на вход истек. Пожалуйста, сгенерируйте новый QR-код.", + "noPinOrBiometricError": "Требуется ПИН-код или биометрия. Пожалуйста, включите ПИН-код, Face ID или Touch ID в настройках разблокировки, чтобы использовать эту функцию." } } }, @@ -447,7 +449,7 @@ "showFolders": "Папки", "passkeys": "Ключи доступа", "attachments": "Вложения", - "totp": "2FA Codes" + "totp": "2FA-коды" }, "sort": { "title": "Сортировка", @@ -473,7 +475,8 @@ "usernameCopied": "Имя пользователя скопировано в буфер обмена", "emailCopied": "Email скопирован в буфер обмена", "passwordCopied": "Пароль скопирован в буфер обмена", - "urlCopied": "URL copied to clipboard" + "totpCodeCopied": "2FA-код скопирован в буфер обмена", + "urlCopied": "URL скопирован в буфер обмена" }, "createNewAliasFor": "Создайте новый псевдоним для", "contextMenu": { @@ -482,13 +485,14 @@ "delete": "Удалить", "copyUsername": "Скопировать имя пользователя", "copyEmail": "Скопировать Email", - "copyPassword": "Скопировать пароль" + "copyPassword": "Скопировать пароль", + "copyTotpCode": "Копировать 2FA-код" }, "urlContextMenu": { - "title": "URL Options", - "copyLink": "Copy Link", - "openLink": "Open Link", - "shareLink": "Share Link" + "title": "Действия с URL", + "copyLink": "Копировать ссылку", + "openLink": "Открыть ссылку", + "shareLink": "Поделиться ссылкой" }, "viewHistory": "Посмотреть историю", "history": "История", diff --git a/apps/mobile-app/i18n/locales/sv.json b/apps/mobile-app/i18n/locales/sv.json index 6b1fee38c..1bcad64d5 100644 --- a/apps/mobile-app/i18n/locales/sv.json +++ b/apps/mobile-app/i18n/locales/sv.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "Verifierar autentiseringskod", "verify": "Bekräfta", "unlockVault": "Lås upp valv", + "unlockWithBiometric": "Lås upp med {{biometric}}", "unlockWithPin": "Lås upp med PIN-kod", + "unlockWithPassword": "Lås upp med lösenord", "enterPassword": "Ange PIN-koden för att låsa upp ditt valv", "enterPasswordPlaceholder": "Lösenord", "enterAuthCode": "Ange 6-siffrig kod", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Ange ditt lösenord", "enableBiometric": "Aktivera {{biometric}}?", "biometricPrompt": "Vill du använda {{biometric}} för att låsa upp ditt valv?", - "tryBiometricAgain": "Försök med {{biometric}} igen", - "tryPinAgain": "Prova PIN-koden igen", "authCodeNote": "Obs! Om du inte har tillgång till din autentiseringsenhet kan du återställa din 2FA med en återställningskod genom att logga in via webbplatsen.", "errors": { "credentialsRequired": "Användarnamn och lösenord krävs", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} är inaktiverat för AliasVault. För att använda funktionen, aktivera den först i enhetens inställningar.", "biometricHelp": "Använd biometri för att låsa upp ditt valv, vilket är säkrat via {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} är inte tillgänglig. Tryck för att öppna inställningar och/eller gå till dina enhetsinställningar för att aktivera och konfigurera den.", + "featureRequiresPasscode": "Denna funktion kräver att en lösenkod aktiveras på din enhet.", "pin": "PIN-Kod", "pinDescription": "Använd en anpassad PIN-kod för att låsa upp ditt valv snabbare.", "pinEnabled": "PIN-upplåsning aktiverat", @@ -321,7 +322,8 @@ "confirmSubtitle": "Återautentisera för att godkänna inloggning på en annan enhet.", "confirmMessage": "Du är på väg att logga in på en fjärrenhet med ditt konto. Den här andra enheten kommer att ha full tillgång till ditt valv. Fortsätt endast om du litar på den här enheten.", "successDescription": "Fjärrenheten har loggats in.", - "requestExpired": "Denna inloggningsbegäran har löpt ut. Vänligen generera en ny QR-kod." + "requestExpired": "Denna inloggningsbegäran har löpt ut. Vänligen generera en ny QR-kod.", + "noPinOrBiometricError": "Mobil upplåsning kräver PIN-kod eller biometrisk autentisering. Aktivera PIN-kod eller Face ID/Touch ID i valvupplåsningsinställningar för att använda den här funktionen." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Användarnamn har kopierats till urklipp", "emailCopied": "E-post har kopierats till urklipp", "passwordCopied": "Lösenord har kopierats till urklipp", + "totpCodeCopied": "TOTP-kod kopierad till urklipp", "urlCopied": "URL kopierad till urklipp" }, "createNewAliasFor": "Skapa nytt alias för", @@ -482,7 +485,8 @@ "delete": "Radera", "copyUsername": "Kopiera användarnamn", "copyEmail": "Kopiera e-postadress", - "copyPassword": "Kopiera lösenord" + "copyPassword": "Kopiera lösenord", + "copyTotpCode": "Kopiera TOTP-kod" }, "urlContextMenu": { "title": "URL-alternativ", diff --git a/apps/mobile-app/i18n/locales/tr.json b/apps/mobile-app/i18n/locales/tr.json index 21c189059..602b948a4 100644 --- a/apps/mobile-app/i18n/locales/tr.json +++ b/apps/mobile-app/i18n/locales/tr.json @@ -4,7 +4,7 @@ "close": "Kapat", "delete": "Sil", "save": "Kaydet", - "edit": "Edit", + "edit": "Düzenle", "yes": "Onayla", "no": "Hayır", "ok": "Tamam", @@ -48,7 +48,9 @@ "verifyingAuthCode": "Doğrulama kodu kontrol ediliyor", "verify": "Doğrula", "unlockVault": "Kasa Kilidini Aç", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Unlock with PIN", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Kasayı açmak için parolanızı girin", "enterPasswordPlaceholder": "Parola", "enterAuthCode": "6 rakamlı kodu yazın", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Parolanızı girin", "enableBiometric": "{{biometric}}’i etkinleştirmek istiyor musunuz?", "biometricPrompt": "Kasayı açmak için {{biometric}} kullanmak ister misiniz?", - "tryBiometricAgain": "{{biometric}}’i tekrar deneyin", - "tryPinAgain": "Try PIN Again", "authCodeNote": "Not: Kimlik doğrulayıcı cihazınıza erişiminiz yoksa, web sitesinden giriş yaparak kurtarma kodu ile iki adımlı doğrulamayı sıfırlayabilirsiniz.", "errors": { "credentialsRequired": "Kullanıcı adı ve parola gerekiyor", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} AliasVault için devre dışı bırakılmış. Kullanabilmek için önce cihaz ayarlarınızdan etkinleştirmeniz gerekmektedir.", "biometricHelp": "Use biometrics to unlock your vault, which is secured by the {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} kullanılamıyor. Ayarları açmak için dokunun veya etkinleştirmek ve yapılandırmak için cihaz ayarlarınıza gidin.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "PIN Code", "pinDescription": "Use a custom PIN code to unlock your vault more quickly.", "pinEnabled": "PIN unlock enabled successfully", @@ -321,7 +322,8 @@ "confirmSubtitle": "Re-authenticate to approve login on another device.", "confirmMessage": "You are about to log in on a remote device with your account. This other device will have full access to your vault. Only proceed if you trust this device.", "successDescription": "The remote device has been successfully logged in.", - "requestExpired": "This login request has expired. Please generate a new QR code." + "requestExpired": "This login request has expired. Please generate a new QR code.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Username copied to clipboard", "emailCopied": "Email copied to clipboard", "passwordCopied": "Password copied to clipboard", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copied to clipboard" }, "createNewAliasFor": "Create new alias for", @@ -482,7 +485,8 @@ "delete": "Sil", "copyUsername": "Kullanıcı Adını Kopyala", "copyEmail": "E-postayı Kopyala", - "copyPassword": "Parolayı Kopyala" + "copyPassword": "Parolayı Kopyala", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "URL Options", diff --git a/apps/mobile-app/i18n/locales/uk.json b/apps/mobile-app/i18n/locales/uk.json index 40c0d1173..7d9ce772b 100644 --- a/apps/mobile-app/i18n/locales/uk.json +++ b/apps/mobile-app/i18n/locales/uk.json @@ -25,7 +25,7 @@ "add": "Add", "generate": "Generate", "attachments": "Вкладення", - "or": "or", + "or": "або", "deleteItemConfirmTitle": "Delete Item", "deleteItemConfirmDescription": "Are you sure you want to delete this item?", "errors": { @@ -48,7 +48,9 @@ "verifyingAuthCode": "Перевірка коду автентифікації", "verify": "Перевірка", "unlockVault": "Розблокувати Vault", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Розблокувати за допомогою PIN-коду", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Введіть свій пароль, щоб розблокувати сховище", "enterPasswordPlaceholder": "Пароль", "enterAuthCode": "Введіть 6-значний код", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Введіть Ваш пароль", "enableBiometric": "Увімкнути {{biometric}}?", "biometricPrompt": "Ви хочете використати {{biometric}} для розблокування вашого сховища?", - "tryBiometricAgain": "Спробуйте {{biometric}} ще раз", - "tryPinAgain": "Введіть PIN-код ще раз", "authCodeNote": "Примітка: якщо у вас немає доступу до вашого пристрою автентифікатора, ви можете скинути налаштування 2FA за допомогою коду відновлення, увійшовши через вебсайт.", "errors": { "credentialsRequired": "Ім'я користувача та пароль обов'язкові", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} вимкнено для AliasVault. Щоб використовувати його, спочатку ввімкніть в налаштуваннях вашого пристрою.", "biometricHelp": "Використовуйте біометричні дані, щоб розблокувати сховище, захищене {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} недоступний. Натисніть, щоб відкрити налаштування, та/або перейдіть до налаштувань пристрою, щоб увімкнути та налаштувати його.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "ПІН-код", "pinDescription": "Використовуйте власний PIN-код, щоб швидше розблокувати сховище.", "pinEnabled": "Розблокування PIN-кодом успішно ввімкнено", @@ -321,7 +322,8 @@ "confirmSubtitle": "Повторно пройдіть автентифікацію, щоб підтвердити вхід на іншому пристрої.", "confirmMessage": "Ви збираєтеся ввійти на віддаленому пристрої за допомогою свого облікового запису. Цей інший пристрій матиме повний доступ до вашого сховища. Продовжуйте, лише якщо ви довіряєте цьому пристрою.", "successDescription": "Віддалений пристрій успішно ввійшов у систему.", - "requestExpired": "Термін дії цього запиту на вхід минув. Будь ласка, згенеруйте новий QR-код." + "requestExpired": "Термін дії цього запиту на вхід минув. Будь ласка, згенеруйте новий QR-код.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Ім'я користувача скопійовано до буфера обміну", "emailCopied": "Електронну адресу скопійовано до буфера обміну", "passwordCopied": "Пароль скопійовано до буфера обміну", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copied to clipboard" }, "createNewAliasFor": "Створити новий псевдонім для", @@ -482,7 +485,8 @@ "delete": "Видалити", "copyUsername": "Копіювати ім'я користувача", "copyEmail": "Копіювати електронну адресу", - "copyPassword": "Копіювати пароль" + "copyPassword": "Копіювати пароль", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "URL Options", diff --git a/apps/mobile-app/i18n/locales/ur.json b/apps/mobile-app/i18n/locales/ur.json index 9ada1737f..6697dde90 100644 --- a/apps/mobile-app/i18n/locales/ur.json +++ b/apps/mobile-app/i18n/locales/ur.json @@ -4,7 +4,7 @@ "close": "بند کریں", "delete": "ڈیلیٹ کریں", "save": "محفوظ کریں", - "edit": "Edit", + "edit": "ترمیم کریں", "yes": "ہاں", "no": "نہیں", "ok": "ٹھیک ہے", @@ -25,7 +25,7 @@ "add": "Add", "generate": "جنریٹ کریں", "attachments": "Attachments", - "or": "or", + "or": "یا", "deleteItemConfirmTitle": "Delete Item", "deleteItemConfirmDescription": "Are you sure you want to delete this item?", "errors": { @@ -48,7 +48,9 @@ "verifyingAuthCode": "Verifying authentication code", "verify": "Verify", "unlockVault": "Unlock Vault", + "unlockWithBiometric": "Unlock with {{biometric}}", "unlockWithPin": "Unlock with PIN", + "unlockWithPassword": "Unlock with Password", "enterPassword": "Enter your password to unlock your vault", "enterPasswordPlaceholder": "پاس ورڈ", "enterAuthCode": "Enter 6-digit code", @@ -56,8 +58,6 @@ "passwordPlaceholder": "Enter your password", "enableBiometric": "Enable {{biometric}}?", "biometricPrompt": "Would you like to use {{biometric}} to unlock your vault?", - "tryBiometricAgain": "Try {{biometric}} Again", - "tryPinAgain": "Try PIN Again", "authCodeNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.", "errors": { "credentialsRequired": "Username and password are required", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "{{biometric}} is disabled for AliasVault. In order to use it, please enable it in your device settings first.", "biometricHelp": "Use biometrics to unlock your vault, which is secured by the {{keystore}}.", "biometricUnavailableHelp": "{{biometric}} is not available. Tap to open settings and/or go to your device settings to enable and configure it.", + "featureRequiresPasscode": "This feature requires a passcode to be enabled on your device.", "pin": "PIN Code", "pinDescription": "Use a custom PIN code to unlock your vault more quickly.", "pinEnabled": "PIN unlock enabled successfully", @@ -321,7 +322,8 @@ "confirmSubtitle": "Re-authenticate to approve login on another device.", "confirmMessage": "You are about to log in on a remote device with your account. This other device will have full access to your vault. Only proceed if you trust this device.", "successDescription": "The remote device has been successfully logged in.", - "requestExpired": "This login request has expired. Please generate a new QR code." + "requestExpired": "This login request has expired. Please generate a new QR code.", + "noPinOrBiometricError": "Mobile unlock requires PIN or biometric authentication. Please enable PIN or Face ID/Touch ID in vault unlock settings to use this feature." } } }, @@ -473,6 +475,7 @@ "usernameCopied": "Username copied to clipboard", "emailCopied": "Email copied to clipboard", "passwordCopied": "Password copied to clipboard", + "totpCodeCopied": "TOTP code copied to clipboard", "urlCopied": "URL copied to clipboard" }, "createNewAliasFor": "Create new alias for", @@ -482,7 +485,8 @@ "delete": "ڈیلیٹ کریں", "copyUsername": "Copy Username", "copyEmail": "Copy Email", - "copyPassword": "Copy Password" + "copyPassword": "Copy Password", + "copyTotpCode": "Copy TOTP Code" }, "urlContextMenu": { "title": "URL Options", diff --git a/apps/mobile-app/i18n/locales/zh.json b/apps/mobile-app/i18n/locales/zh.json index 75ce45d30..551839cbd 100644 --- a/apps/mobile-app/i18n/locales/zh.json +++ b/apps/mobile-app/i18n/locales/zh.json @@ -48,7 +48,9 @@ "verifyingAuthCode": "正在验证身份验证码", "verify": "验证", "unlockVault": "解锁密码库", + "unlockWithBiometric": "使用 {{biometric}} 解锁", "unlockWithPin": "使用 PIN 解锁", + "unlockWithPassword": "使用密码解锁", "enterPassword": "输入您的密码以解锁密码库", "enterPasswordPlaceholder": "密码", "enterAuthCode": "输入 6 位数代码", @@ -56,8 +58,6 @@ "passwordPlaceholder": "输入您的密码", "enableBiometric": "启用 {{biometric}}?", "biometricPrompt": "您想使用 {{biometric}} 来解锁密码库吗?", - "tryBiometricAgain": "重试 {{biometric}}", - "tryPinAgain": "重试 PIN", "authCodeNote": "注:如果您无法访问您的身份验证器设备,您可以通过网站登录并使用恢复码重置您的两步验证。", "errors": { "credentialsRequired": "用户名和密码是必填项", @@ -216,6 +216,7 @@ "biometricDisabledMessage": "AliasVault的{{biometric}}已禁用。如需使用,请先在设备设置中启用它。", "biometricHelp": "使用生物识别解锁您的密码库,经由 {{keystore}} 加密。", "biometricUnavailableHelp": "{{biometric}}不可用。点击打开设置和/或前往设备设置启用并配置它。", + "featureRequiresPasscode": "此功能需要在您的设备上启用密码。", "pin": "PIN 码", "pinDescription": "使用自定义 PIN 码快速解锁密码库。", "pinEnabled": "PIN 解锁已成功启用", @@ -321,7 +322,8 @@ "confirmSubtitle": "重新认证以批准在另一台设备上登录。", "confirmMessage": "您即将使用您的账户在远程设备上登录。该设备将拥有对您密码库的完全访问权限。请仅在您信任该设备的情况下继续。", "successDescription": "远程设备已成功登录。", - "requestExpired": "此登录请求已过期,请生成新的二维码。" + "requestExpired": "此登录请求已过期,请生成新的二维码。", + "noPinOrBiometricError": "移动设备解锁需要 PIN 或生物识别认证。请在密码库解锁设置中启用 PIN 或面容 ID/触控 ID 以使用此功能。" } } }, @@ -473,6 +475,7 @@ "usernameCopied": "用户名已复制到剪贴板", "emailCopied": "邮箱已复制到剪贴板", "passwordCopied": "密码已复制到剪贴板", + "totpCodeCopied": "TOTP 验证码已复制到剪贴板", "urlCopied": "URL 已复制到剪贴板" }, "createNewAliasFor": "创建新别名用于", @@ -482,7 +485,8 @@ "delete": "删除", "copyUsername": "复制用户名", "copyEmail": "复制邮箱", - "copyPassword": "复制密码" + "copyPassword": "复制密码", + "copyTotpCode": "复制 TOTP 验证码" }, "urlContextMenu": { "title": "URL 选项", diff --git a/apps/mobile-app/ios/VaultUI/bg.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/bg.lproj/Localizable.strings index 8cdc36830..5b1ac986c 100644 --- a/apps/mobile-app/ios/VaultUI/bg.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/bg.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN locked after too many failed attempts"; "pin_incorrect_attempts_remaining" = "Incorrect PIN. %d attempts remaining"; +/* Password Unlock */ +"enter_password_to_unlock" = "Enter your master password"; +"unlock" = "Отключи"; +"incorrect_password" = "Incorrect password. Please try again."; + /* PIN Setup */ "pin_setup_title" = "Setup PIN"; "pin_setup_subtitle" = "Choose a PIN to unlock your vault"; diff --git a/apps/mobile-app/ios/VaultUI/ca.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/ca.lproj/Localizable.strings index dff197f6e..76e3151e8 100644 --- a/apps/mobile-app/ios/VaultUI/ca.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/ca.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN locked after too many failed attempts"; "pin_incorrect_attempts_remaining" = "Incorrect PIN. %d attempts remaining"; +/* Password Unlock */ +"enter_password_to_unlock" = "Enter your master password"; +"unlock" = "Unlock"; +"incorrect_password" = "Incorrect password. Please try again."; + /* PIN Setup */ "pin_setup_title" = "Setup PIN"; "pin_setup_subtitle" = "Choose a PIN to unlock your vault"; diff --git a/apps/mobile-app/ios/VaultUI/cs.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/cs.lproj/Localizable.strings index 5117d02ca..f2c3b2347 100644 --- a/apps/mobile-app/ios/VaultUI/cs.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/cs.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN locked after too many failed attempts"; "pin_incorrect_attempts_remaining" = "Incorrect PIN. %d attempts remaining"; +/* Password Unlock */ +"enter_password_to_unlock" = "Enter your master password"; +"unlock" = "Unlock"; +"incorrect_password" = "Incorrect password. Please try again."; + /* PIN Setup */ "pin_setup_title" = "Setup PIN"; "pin_setup_subtitle" = "Choose a PIN to unlock your vault"; diff --git a/apps/mobile-app/ios/VaultUI/da.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/da.lproj/Localizable.strings index 88d25bec7..5441ed8c4 100644 --- a/apps/mobile-app/ios/VaultUI/da.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/da.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN-kode låst efter for mange mislykkede forsøg"; "pin_incorrect_attempts_remaining" = "Forkert PIN-kode. %d forsøg tilbage"; +/* Password Unlock */ +"enter_password_to_unlock" = "Indtast din hovedadgangskode"; +"unlock" = "Lås op"; +"incorrect_password" = "Forkert adgangskode. Prøv venligst igen."; + /* PIN Setup */ "pin_setup_title" = "Konfigurer PIN-kode"; "pin_setup_subtitle" = "Vælg en PIN-kode til at låse din boks op"; diff --git a/apps/mobile-app/ios/VaultUI/de.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/de.lproj/Localizable.strings index 7b006969c..21b349339 100644 --- a/apps/mobile-app/ios/VaultUI/de.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/de.lproj/Localizable.strings @@ -32,7 +32,7 @@ "username_copied" = "Benutzername kopiert"; "password_copied" = "Passwort kopiert"; "email_copied" = "E-Mail-Adresse kopiert"; -"totp_code" = "TOTP Code"; +"totp_code" = "TOTP-Code"; /* Search bar */ "search_items" = "Einträge durchsuchen..."; @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "Deine PIN wurde nach zu vielen Fehlversuchen gesperrt"; "pin_incorrect_attempts_remaining" = "Ungültige PIN, %d verbleibende Versuche"; +/* Password Unlock */ +"enter_password_to_unlock" = "Gib Dein Master-Passwort ein"; +"unlock" = "Entsperren"; +"incorrect_password" = "Falsches Passwort. Bitte versuche es erneut."; + /* PIN Setup */ "pin_setup_title" = "PIN einrichten"; "pin_setup_subtitle" = "Wähle eine PIN zum Entsperren des Tresors"; diff --git a/apps/mobile-app/ios/VaultUI/es.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/es.lproj/Localizable.strings index 761f0e71e..47ea5d613 100644 --- a/apps/mobile-app/ios/VaultUI/es.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/es.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN bloqueado tras demasiados intentos fallidos"; "pin_incorrect_attempts_remaining" = "PIN incorrecto, %d intentos restantes"; +/* Password Unlock */ +"enter_password_to_unlock" = "Ingrese su contraseña maestra"; +"unlock" = "Desbloquear"; +"incorrect_password" = "Contraseña incorrecta. Por favor, inténtelo de nuevo."; + /* PIN Setup */ "pin_setup_title" = "Configurar PIN"; "pin_setup_subtitle" = "Elija un PIN para desbloquear su bóveda"; diff --git a/apps/mobile-app/ios/VaultUI/fa.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/fa.lproj/Localizable.strings index 5117d02ca..f2c3b2347 100644 --- a/apps/mobile-app/ios/VaultUI/fa.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/fa.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN locked after too many failed attempts"; "pin_incorrect_attempts_remaining" = "Incorrect PIN. %d attempts remaining"; +/* Password Unlock */ +"enter_password_to_unlock" = "Enter your master password"; +"unlock" = "Unlock"; +"incorrect_password" = "Incorrect password. Please try again."; + /* PIN Setup */ "pin_setup_title" = "Setup PIN"; "pin_setup_subtitle" = "Choose a PIN to unlock your vault"; diff --git a/apps/mobile-app/ios/VaultUI/fi.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/fi.lproj/Localizable.strings index efac5860c..9ebc6eb22 100644 --- a/apps/mobile-app/ios/VaultUI/fi.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/fi.lproj/Localizable.strings @@ -32,7 +32,7 @@ "username_copied" = "Käyttäjänimi kopioitu"; "password_copied" = "Salasana kopioitu"; "email_copied" = "Sähköposti kopioitu"; -"totp_code" = "TOTP Code"; +"totp_code" = "TOTP-koodi"; /* Search bar */ "search_items" = "Search items..."; @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN-koodi lukittu liian monen epäonnistuneen yrityksen jälkeen"; "pin_incorrect_attempts_remaining" = "Virheellinen PIN, %d yritystä jäljellä"; +/* Password Unlock */ +"enter_password_to_unlock" = "Syötä pääsalasanasi"; +"unlock" = "Avaa lukitus"; +"incorrect_password" = "Virheellinen salasana. Yritä uudelleen. "; + /* PIN Setup */ "pin_setup_title" = "Aseta PIN-koodi"; "pin_setup_subtitle" = "Syötä PIN-koodi avataksesi holvisi"; diff --git a/apps/mobile-app/ios/VaultUI/fr.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/fr.lproj/Localizable.strings index 702da00e8..30bf0534a 100644 --- a/apps/mobile-app/ios/VaultUI/fr.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/fr.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "Le code PIN est verrouillé après trop de tentatives échouées"; "pin_incorrect_attempts_remaining" = "Code PIN incorrect, %d tentatives restantes"; +/* Password Unlock */ +"enter_password_to_unlock" = "Entrez votre mot de passe maître"; +"unlock" = "Déverrouiller"; +"incorrect_password" = "Mot de passe incorrect. Veuillez réessayer."; + /* PIN Setup */ "pin_setup_title" = "Configurer le code PIN"; "pin_setup_subtitle" = "Choisissez un code PIN pour déverrouiller votre coffre"; diff --git a/apps/mobile-app/ios/VaultUI/he.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/he.lproj/Localizable.strings index 97ae95fb8..2223645c9 100644 --- a/apps/mobile-app/ios/VaultUI/he.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/he.lproj/Localizable.strings @@ -32,7 +32,7 @@ "username_copied" = "שם המשתמש הועתק"; "password_copied" = "הסיסמה הועתקה"; "email_copied" = "כתובת הדוא״ל הועתקה"; -"totp_code" = "TOTP Code"; +"totp_code" = "קוד חד־פעמי זמני"; /* Search bar */ "search_items" = "Search items..."; @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "הקוד האישי ננעל אחרי יותר מדי ניסיונות כושלים"; "pin_incorrect_attempts_remaining" = "קוד אישי שגוי, נותרו %d ניסיונות"; +/* Password Unlock */ +"enter_password_to_unlock" = "נא למלא את סיסמת העל שלך"; +"unlock" = "שחרור נעילה"; +"incorrect_password" = "סיסמה שגויה. נא לנסות שוב."; + /* PIN Setup */ "pin_setup_title" = "הגדרת קוד אישי"; "pin_setup_subtitle" = "נא לבחור קוד אישי לשחרור נעילת הכספת שלך"; diff --git a/apps/mobile-app/ios/VaultUI/it.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/it.lproj/Localizable.strings index 60ba78456..d5a198094 100644 --- a/apps/mobile-app/ios/VaultUI/it.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/it.lproj/Localizable.strings @@ -32,7 +32,7 @@ "username_copied" = "Nome Utente Copiato"; "password_copied" = "Password copiata"; "email_copied" = "Email copiata"; -"totp_code" = "TOTP Code"; +"totp_code" = "Codice TOTP"; /* Search bar */ "search_items" = "Ricerca elementi..."; @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN bloccato dopo troppi tentativi falliti"; "pin_incorrect_attempts_remaining" = "PIN errato, %d tentativi rimasti"; +/* Password Unlock */ +"enter_password_to_unlock" = "Inserisci la tua password principale"; +"unlock" = "Sblocca"; +"incorrect_password" = "Password errata. Riprovare."; + /* PIN Setup */ "pin_setup_title" = "Configurare PIN"; "pin_setup_subtitle" = "Scegli un PIN per sbloccare la cassaforte"; diff --git a/apps/mobile-app/ios/VaultUI/ko.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/ko.lproj/Localizable.strings index 5117d02ca..f2c3b2347 100644 --- a/apps/mobile-app/ios/VaultUI/ko.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/ko.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN locked after too many failed attempts"; "pin_incorrect_attempts_remaining" = "Incorrect PIN. %d attempts remaining"; +/* Password Unlock */ +"enter_password_to_unlock" = "Enter your master password"; +"unlock" = "Unlock"; +"incorrect_password" = "Incorrect password. Please try again."; + /* PIN Setup */ "pin_setup_title" = "Setup PIN"; "pin_setup_subtitle" = "Choose a PIN to unlock your vault"; diff --git a/apps/mobile-app/ios/VaultUI/nl.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/nl.lproj/Localizable.strings index 6a2f76ce0..48cb2e1f5 100644 --- a/apps/mobile-app/ios/VaultUI/nl.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/nl.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "Pincode vergrendeld na te veel mislukte pogingen"; "pin_incorrect_attempts_remaining" = "Onjuiste pincode, %d pogingen resterend"; +/* Password Unlock */ +"enter_password_to_unlock" = "Voer je hoofdwachtwoord in"; +"unlock" = "Ontgrendelen"; +"incorrect_password" = "Onjuist wachtwoord. Probeer het opnieuw."; + /* PIN Setup */ "pin_setup_title" = "Pincode instellen"; "pin_setup_subtitle" = "Kies een pincode om je vault te ontgrendelen"; diff --git a/apps/mobile-app/ios/VaultUI/pl.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/pl.lproj/Localizable.strings index 9d84d1dfa..bb48d38b9 100644 --- a/apps/mobile-app/ios/VaultUI/pl.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/pl.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN zablokowany po zbyt wielu nieudanych próbach"; "pin_incorrect_attempts_remaining" = "Nieprawidłowy kod PIN. Pozostało %d prób."; +/* Password Unlock */ +"enter_password_to_unlock" = "Wprowadź swoje hasło główne"; +"unlock" = "Odblokuj"; +"incorrect_password" = "Nieprawidłowe hasło. Spróbuj ponownie."; + /* PIN Setup */ "pin_setup_title" = "Ustaw kod PIN"; "pin_setup_subtitle" = "Wybierz kod PIN, aby odblokować sejf"; diff --git a/apps/mobile-app/ios/VaultUI/pt.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/pt.lproj/Localizable.strings index 30e775bc1..a1ebea840 100644 --- a/apps/mobile-app/ios/VaultUI/pt.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/pt.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN bloqueado após muitas tentativas incorretas"; "pin_incorrect_attempts_remaining" = "PIN incorreto. %d tentativas restantes"; +/* Password Unlock */ +"enter_password_to_unlock" = "Digite sua senha mestre"; +"unlock" = "Desbloquear"; +"incorrect_password" = "Senha incorreta. Por favor tente novamente."; + /* PIN Setup */ "pin_setup_title" = "Configurar PIN"; "pin_setup_subtitle" = "Escolha um PIN para desbloquear seu cofre"; diff --git a/apps/mobile-app/ios/VaultUI/ro.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/ro.lproj/Localizable.strings index 9cbdf907f..da5caf3bc 100644 --- a/apps/mobile-app/ios/VaultUI/ro.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/ro.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN blocat după prea multe încercări eșuate"; "pin_incorrect_attempts_remaining" = "PIN incorect. Mai ai %d încercări"; +/* Password Unlock */ +"enter_password_to_unlock" = "Introdu parola principală"; +"unlock" = "Deblochează"; +"incorrect_password" = "Parolă incorectă. Încearcă din nou."; + /* PIN Setup */ "pin_setup_title" = "Configurare PIN"; "pin_setup_subtitle" = "Alege un PIN pentru a-ți debloca seiful"; diff --git a/apps/mobile-app/ios/VaultUI/ru.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/ru.lproj/Localizable.strings index 9e8a165d4..7685ddb4d 100644 --- a/apps/mobile-app/ios/VaultUI/ru.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/ru.lproj/Localizable.strings @@ -32,7 +32,7 @@ "username_copied" = "Имя пользователя скопировано"; "password_copied" = "Пароль скопирован"; "email_copied" = "Email скопирован"; -"totp_code" = "TOTP Code"; +"totp_code" = "TOTP код"; /* Search bar */ "search_items" = "Поиск записей..."; @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "ПИН-код заблокирован: слишком много попыток ввода"; "pin_incorrect_attempts_remaining" = "Неверный PIN-код. Осталось попыток: %d"; +/* Password Unlock */ +"enter_password_to_unlock" = "Введите свой мастер-пароль"; +"unlock" = "Разблокировать"; +"incorrect_password" = "Неверный пароль. Повторите попытку."; + /* PIN Setup */ "pin_setup_title" = "Настройка PIN-кода"; "pin_setup_subtitle" = "Придумайте PIN-код для разблокировки хранилища"; diff --git a/apps/mobile-app/ios/VaultUI/sv.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/sv.lproj/Localizable.strings index c5d89b017..29d6c11f6 100644 --- a/apps/mobile-app/ios/VaultUI/sv.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/sv.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN-koden låst efter för många misslyckade försök"; "pin_incorrect_attempts_remaining" = "Felaktig PIN-kod. %d försök återstår"; +/* Password Unlock */ +"enter_password_to_unlock" = "Ange ditt huvudlösenord"; +"unlock" = "Lås upp"; +"incorrect_password" = "Felaktigt lösenord. Försök igen."; + /* PIN Setup */ "pin_setup_title" = "Ställ in PIN-kod"; "pin_setup_subtitle" = "Välj en PIN-kod att låsa upp ditt valv med"; diff --git a/apps/mobile-app/ios/VaultUI/tr.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/tr.lproj/Localizable.strings index fa8f9f717..8fa2e62b6 100644 --- a/apps/mobile-app/ios/VaultUI/tr.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/tr.lproj/Localizable.strings @@ -32,7 +32,7 @@ "username_copied" = "Username copied"; "password_copied" = "Password copied"; "email_copied" = "Email copied"; -"totp_code" = "TOTP Code"; +"totp_code" = "TOTP Kodu"; /* Search bar */ "search_items" = "Search items..."; @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN locked after too many failed attempts"; "pin_incorrect_attempts_remaining" = "Incorrect PIN. %d attempts remaining"; +/* Password Unlock */ +"enter_password_to_unlock" = "Enter your master password"; +"unlock" = "Kilidi aç"; +"incorrect_password" = "Parola yanlış. Lütfen yeniden deneyin."; + /* PIN Setup */ "pin_setup_title" = "Setup PIN"; "pin_setup_subtitle" = "Choose a PIN to unlock your vault"; diff --git a/apps/mobile-app/ios/VaultUI/uk.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/uk.lproj/Localizable.strings index 9dbe2c162..96f1bd089 100644 --- a/apps/mobile-app/ios/VaultUI/uk.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/uk.lproj/Localizable.strings @@ -32,7 +32,7 @@ "username_copied" = "Ім'я користувача скопійовано"; "password_copied" = "Пароль скопійовано"; "email_copied" = "Адреса ел. пошти скопійована"; -"totp_code" = "TOTP Code"; +"totp_code" = "Код TOTP"; /* Search bar */ "search_items" = "Search items..."; @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN locked after too many failed attempts"; "pin_incorrect_attempts_remaining" = "Incorrect PIN. %d attempts remaining"; +/* Password Unlock */ +"enter_password_to_unlock" = "Введіть ваш головний пароль"; +"unlock" = "Розблокувати"; +"incorrect_password" = "Невірний пароль. Будь ласка, спробуйте ще раз."; + /* PIN Setup */ "pin_setup_title" = "Налаштування PIN-коду"; "pin_setup_subtitle" = "Виберіть PIN-код, щоб розблокувати сховище"; diff --git a/apps/mobile-app/ios/VaultUI/ur.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/ur.lproj/Localizable.strings index 6cf0a1ed3..264f69481 100644 --- a/apps/mobile-app/ios/VaultUI/ur.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/ur.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN locked after too many failed attempts"; "pin_incorrect_attempts_remaining" = "Incorrect PIN. %d attempts remaining"; +/* Password Unlock */ +"enter_password_to_unlock" = "Enter your master password"; +"unlock" = "Unlock"; +"incorrect_password" = "Incorrect password. Please try again."; + /* PIN Setup */ "pin_setup_title" = "Setup PIN"; "pin_setup_subtitle" = "Choose a PIN to unlock your vault"; diff --git a/apps/mobile-app/ios/VaultUI/zh.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/zh.lproj/Localizable.strings index 1c3be63f4..70f7a2851 100644 --- a/apps/mobile-app/ios/VaultUI/zh.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/zh.lproj/Localizable.strings @@ -68,6 +68,11 @@ "pin_locked_max_attempts" = "PIN 因多次尝试失败而被锁定"; "pin_incorrect_attempts_remaining" = "PIN 错误,剩余 %d 次尝试"; +/* Password Unlock */ +"enter_password_to_unlock" = "输入您的主密码"; +"unlock" = "解锁"; +"incorrect_password" = "密码错误,请重试。"; + /* PIN Setup */ "pin_setup_title" = "设置 PIN"; "pin_setup_subtitle" = "选择一个 PIN 以解锁保险库"; diff --git a/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.sv.resx b/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.sv.resx index e64050410..9ed6204b5 100644 --- a/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.sv.resx +++ b/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.sv.resx @@ -263,7 +263,7 @@ - Importera lösenord från Microsoft Edge + Importera lösenord från Microsoft Edge Password Manager Description for Edge import service diff --git a/apps/server/AliasVault.Client/Resources/Components/Main/Settings/Security/QuickVaultUnlockSection.sv.resx b/apps/server/AliasVault.Client/Resources/Components/Main/Settings/Security/QuickVaultUnlockSection.sv.resx index ab85aa902..d0fb07dd4 100644 --- a/apps/server/AliasVault.Client/Resources/Components/Main/Settings/Security/QuickVaultUnlockSection.sv.resx +++ b/apps/server/AliasVault.Client/Resources/Components/Main/Settings/Security/QuickVaultUnlockSection.sv.resx @@ -71,8 +71,8 @@ Button to disable passkey unlock - Omladdning av AliasVault-sidan eller fliken kräver som standard att du anger ditt huvudlösenord igen. Med en passkey kan du låsa upp ditt valv omedelbart. När detta är aktiverat krypteras ditt huvudlösenord lokalt med passkeyns PRF-tillägg. -OBS: Passkeyn fungerar endast för den enhet och webbläsare där du aktiverar den. + Som standard måste du ange ditt huvudlösenord igen när du laddar om AliasVault-sidan eller fliken. Med en passkey kan du låsa upp valvet direkt. När funktionen är aktiverad krypteras ditt huvudlösenord lokalt med passkeyns PRF-tillägg. +Obs: Passkeyn fungerar endast på den enhet och i den webbläsare där du aktiverade den. Description when passkey unlock is disabled @@ -88,7 +88,7 @@ OBS: Passkeyn fungerar endast för den enhet och webbläsare där du aktiverar d Success message when passkey unlock is enabled - Passkeyupplåsning har inaktiverats + Passkeyupplåsning har inaktiverats. Success message when passkey unlock is disabled diff --git a/apps/server/AliasVault.Client/Resources/Components/Main/Settings/Security/TwoFactorAuthenticationSection.sv.resx b/apps/server/AliasVault.Client/Resources/Components/Main/Settings/Security/TwoFactorAuthenticationSection.sv.resx index 4228860cb..1de16d54a 100644 --- a/apps/server/AliasVault.Client/Resources/Components/Main/Settings/Security/TwoFactorAuthenticationSection.sv.resx +++ b/apps/server/AliasVault.Client/Resources/Components/Main/Settings/Security/TwoFactorAuthenticationSection.sv.resx @@ -67,7 +67,7 @@ Status message when 2FA is enabled - Tvåfaktorsautentisering är för närvarande inaktiverad. För att förbättra din kontosäkerhet rekommenderar vi att du aktiverar den. + Tvåfaktorsautentisering är för närvarande inaktiverad. För att förbättra din kontosäkerhet rekommenderar vi att du aktiverar funktionen. Status message when 2FA is disabled diff --git a/apps/server/AliasVault.Client/Resources/Components/Main/Widgets/CreateNewIdentityWidget.fr.resx b/apps/server/AliasVault.Client/Resources/Components/Main/Widgets/CreateNewIdentityWidget.fr.resx index be27c27d9..141b0b5f5 100644 --- a/apps/server/AliasVault.Client/Resources/Components/Main/Widgets/CreateNewIdentityWidget.fr.resx +++ b/apps/server/AliasVault.Client/Resources/Components/Main/Widgets/CreateNewIdentityWidget.fr.resx @@ -115,7 +115,7 @@ Title for creating a new note item - Se connecter + Identifiant Login item type label diff --git a/apps/server/AliasVault.Client/Resources/Components/Main/Widgets/SearchWidget.fr.resx b/apps/server/AliasVault.Client/Resources/Components/Main/Widgets/SearchWidget.fr.resx index da8c56031..e4d822741 100644 --- a/apps/server/AliasVault.Client/Resources/Components/Main/Widgets/SearchWidget.fr.resx +++ b/apps/server/AliasVault.Client/Resources/Components/Main/Widgets/SearchWidget.fr.resx @@ -20,7 +20,7 @@ - Rechercher dans le coffre... + Rechercher élément... Placeholder text for search input field diff --git a/apps/server/AliasVault.Client/Resources/Pages/Auth/Unlock.ro.resx b/apps/server/AliasVault.Client/Resources/Pages/Auth/Unlock.ro.resx index 0f8c1d21e..651b4a562 100644 --- a/apps/server/AliasVault.Client/Resources/Pages/Auth/Unlock.ro.resx +++ b/apps/server/AliasVault.Client/Resources/Pages/Auth/Unlock.ro.resx @@ -71,7 +71,7 @@ Button text for WebAuthn unlock - Deblochează prin parolă + Deblochează cu parolă Button text for password unlock diff --git a/apps/server/AliasVault.Client/Resources/Pages/Auth/Unlock.sv.resx b/apps/server/AliasVault.Client/Resources/Pages/Auth/Unlock.sv.resx index af85f966f..cafcceab7 100644 --- a/apps/server/AliasVault.Client/Resources/Pages/Auth/Unlock.sv.resx +++ b/apps/server/AliasVault.Client/Resources/Pages/Auth/Unlock.sv.resx @@ -67,7 +67,7 @@ Description explaining WebAuthn unlock options - Lås upp med passey + Lås upp med passkey Button text for WebAuthn unlock diff --git a/apps/server/AliasVault.Client/Resources/Pages/Main/Items/Home.ru.resx b/apps/server/AliasVault.Client/Resources/Pages/Main/Items/Home.ru.resx index 10e2b2633..f26cfb723 100644 --- a/apps/server/AliasVault.Client/Resources/Pages/Main/Items/Home.ru.resx +++ b/apps/server/AliasVault.Client/Resources/Pages/Main/Items/Home.ru.resx @@ -164,7 +164,7 @@ Filter option to show only items with attachments - 2FA Codes + 2FA-коды Filter option to show only items with TOTP codes diff --git a/apps/server/AliasVault.Client/Resources/Pages/Main/Settings/Apps.sv.resx b/apps/server/AliasVault.Client/Resources/Pages/Main/Settings/Apps.sv.resx index 17dea9d97..6f7b71c79 100644 --- a/apps/server/AliasVault.Client/Resources/Pages/Main/Settings/Apps.sv.resx +++ b/apps/server/AliasVault.Client/Resources/Pages/Main/Settings/Apps.sv.resx @@ -38,7 +38,7 @@ Title for browser extensions section - Med webbläsartillägget AliasVault kan du automatiskt fylla i befintliga uppgifter på alla webbplatser. Det gör också att du kan generera nya alias under registrering, komma åt mottagna e-postmeddelanden på alla dina alias och visa dina alias och identiteter. + Med webbläsartillägget AliasVault kan du automatiskt fylla i befintliga uppgifter på alla webbplatser. Det gör också att du kan generera nya alias under registrering, komma åt mottagna e-postmeddelanden på alla dina alias samt visa dina alias och identiteter. Description for browser extensions diff --git a/apps/server/AliasVault.Client/Resources/SharedResources.ru.resx b/apps/server/AliasVault.Client/Resources/SharedResources.ru.resx index fd25af17f..f30288a8c 100644 --- a/apps/server/AliasVault.Client/Resources/SharedResources.ru.resx +++ b/apps/server/AliasVault.Client/Resources/SharedResources.ru.resx @@ -139,11 +139,11 @@ Button text for editing an item - Show + Показать Button text for showing content - Hide + Скрыть Button text for hiding content diff --git a/apps/server/AliasVault.Client/wwwroot/locales/sv.json b/apps/server/AliasVault.Client/wwwroot/locales/sv.json index 52346e286..a79e64097 100644 --- a/apps/server/AliasVault.Client/wwwroot/locales/sv.json +++ b/apps/server/AliasVault.Client/wwwroot/locales/sv.json @@ -6,7 +6,7 @@ "refreshButtonText": "Uppdatera sida" }, "errors": { - "unhandledError": "Ett ohanterat fel har inträffat. Försök att ladda om sidan. Om problemet kvarstår, vänligen kontakta support.", + "unhandledError": "Ett oväntat fel har inträffat. Försök att ladda om sidan. Om problemet kvarstår, kontakta supporten.", "webAssemblyError": "AliasVault kräver WebAssembly, vilket denna webbläsare inte stöder. Prova att använda en modernare webbläsare som stöder WebAssembly.", "reloadPageText": "Ladda om sida" }, From 3a48e669d71f786e08610d361b245c43d279ce18 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 6 Mar 2026 12:29:38 +0100 Subject: [PATCH 12/26] Add release notes for 0.27.1 --- fastlane/metadata/android/en-US/changelogs/2701900.txt | 5 +++++ fastlane/metadata/android/nl-NL/changelogs/2701900.txt | 5 +++++ .../metadata/browser-extension/en-US/changelogs/0.27.1.txt | 4 ++++ fastlane/metadata/ios/en-US/changelogs/2701900.txt | 5 +++++ fastlane/metadata/ios/nl-NL/changelogs/2701900.txt | 5 +++++ 5 files changed, 24 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/2701900.txt create mode 100644 fastlane/metadata/android/nl-NL/changelogs/2701900.txt create mode 100644 fastlane/metadata/browser-extension/en-US/changelogs/0.27.1.txt create mode 100644 fastlane/metadata/ios/en-US/changelogs/2701900.txt create mode 100644 fastlane/metadata/ios/nl-NL/changelogs/2701900.txt diff --git a/fastlane/metadata/android/en-US/changelogs/2701900.txt b/fastlane/metadata/android/en-US/changelogs/2701900.txt new file mode 100644 index 000000000..8548d3c01 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/2701900.txt @@ -0,0 +1,5 @@ +- Add 2FA TOTP copy option to long-press item context menu in main screen +- Add haptic feedback to mobile app primary actions +- Add password visiblity toggle to password unlock screen +- Bugfixes +- Translation updates \ No newline at end of file diff --git a/fastlane/metadata/android/nl-NL/changelogs/2701900.txt b/fastlane/metadata/android/nl-NL/changelogs/2701900.txt new file mode 100644 index 000000000..268e132a6 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/2701900.txt @@ -0,0 +1,5 @@ +- Kopieer 2FA TOTP rechtstreeks vanuit het hoofdscherm (long-press menu) +- Meer haptische feedback toegevoegd +- Wachtwoord visiblity toggle toegevoegd aan ontgrendel met wachtwoord scherm +- Bugfixes +- Vertaling updates \ No newline at end of file diff --git a/fastlane/metadata/browser-extension/en-US/changelogs/0.27.1.txt b/fastlane/metadata/browser-extension/en-US/changelogs/0.27.1.txt new file mode 100644 index 000000000..09c5bccde --- /dev/null +++ b/fastlane/metadata/browser-extension/en-US/changelogs/0.27.1.txt @@ -0,0 +1,4 @@ +- Improve username/password and 2FA autofill field form detection +- Improve browser extension "save credentials" prompt reliability +- Passkey authentication flow improvements where "use browser" option now looks at full domain instead of base domain +- Translation updates \ No newline at end of file diff --git a/fastlane/metadata/ios/en-US/changelogs/2701900.txt b/fastlane/metadata/ios/en-US/changelogs/2701900.txt new file mode 100644 index 000000000..8548d3c01 --- /dev/null +++ b/fastlane/metadata/ios/en-US/changelogs/2701900.txt @@ -0,0 +1,5 @@ +- Add 2FA TOTP copy option to long-press item context menu in main screen +- Add haptic feedback to mobile app primary actions +- Add password visiblity toggle to password unlock screen +- Bugfixes +- Translation updates \ No newline at end of file diff --git a/fastlane/metadata/ios/nl-NL/changelogs/2701900.txt b/fastlane/metadata/ios/nl-NL/changelogs/2701900.txt new file mode 100644 index 000000000..268e132a6 --- /dev/null +++ b/fastlane/metadata/ios/nl-NL/changelogs/2701900.txt @@ -0,0 +1,5 @@ +- Kopieer 2FA TOTP rechtstreeks vanuit het hoofdscherm (long-press menu) +- Meer haptische feedback toegevoegd +- Wachtwoord visiblity toggle toegevoegd aan ontgrendel met wachtwoord scherm +- Bugfixes +- Vertaling updates \ No newline at end of file From 439a0bef4276e1633c475d215bae41b48bd8c729 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 6 Mar 2026 12:32:15 +0100 Subject: [PATCH 13/26] Bump version to 0.28.0-alpha --- apps/.version/minor.txt | 2 +- apps/.version/version.txt | 2 +- apps/browser-extension/package.json | 2 +- .../AliasVault.xcodeproj/project.pbxproj | 16 +++---- apps/browser-extension/src/utils/AppInfo.ts | 2 +- apps/browser-extension/wxt.config.ts | 2 +- apps/mobile-app/android/app/build.gradle | 4 +- apps/mobile-app/app.json | 2 +- .../ios/AliasVault.xcodeproj/project.pbxproj | 44 +++++++++---------- apps/mobile-app/utils/AppInfo.ts | 2 +- .../Shared/AliasVault.Shared.Core/AppInfo.cs | 2 +- core/rust/Cargo.lock | 2 +- core/rust/Cargo.toml | 2 +- 13 files changed, 42 insertions(+), 42 deletions(-) diff --git a/apps/.version/minor.txt b/apps/.version/minor.txt index f64f5d8d8..9902f1784 100644 --- a/apps/.version/minor.txt +++ b/apps/.version/minor.txt @@ -1 +1 @@ -27 +28 diff --git a/apps/.version/version.txt b/apps/.version/version.txt index f0eeb4312..42f89d1b1 100644 --- a/apps/.version/version.txt +++ b/apps/.version/version.txt @@ -1 +1 @@ -0.27.0-alpha +0.28.0-alpha diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index acd0219c4..18b706de5 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -2,7 +2,7 @@ "name": "aliasvault-browser-extension", "description": "AliasVault Browser Extension", "private": true, - "version": "0.27.0", + "version": "0.28.0", "type": "module", "scripts": { "build:rust": "cd ../../core/rust && ./build.sh --browser", diff --git a/apps/browser-extension/safari-xcode/AliasVault.xcodeproj/project.pbxproj b/apps/browser-extension/safari-xcode/AliasVault.xcodeproj/project.pbxproj index aeb26d61d..f8c4014e2 100644 --- a/apps/browser-extension/safari-xcode/AliasVault.xcodeproj/project.pbxproj +++ b/apps/browser-extension/safari-xcode/AliasVault.xcodeproj/project.pbxproj @@ -463,7 +463,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEVELOPMENT_TEAM = 8PHW4HN3F7; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -476,7 +476,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 0.27.0; + MARKETING_VERSION = 0.28.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -495,7 +495,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEVELOPMENT_TEAM = 8PHW4HN3F7; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -508,7 +508,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 0.27.0; + MARKETING_VERSION = 0.28.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -532,7 +532,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEVELOPMENT_TEAM = 8PHW4HN3F7; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -547,7 +547,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 0.27.0; + MARKETING_VERSION = 0.28.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -571,7 +571,7 @@ CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEVELOPMENT_TEAM = 8PHW4HN3F7; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -586,7 +586,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 0.27.0; + MARKETING_VERSION = 0.28.0; OTHER_LDFLAGS = ( "-framework", SafariServices, diff --git a/apps/browser-extension/src/utils/AppInfo.ts b/apps/browser-extension/src/utils/AppInfo.ts index df4db6ba4..bfbe99b5b 100644 --- a/apps/browser-extension/src/utils/AppInfo.ts +++ b/apps/browser-extension/src/utils/AppInfo.ts @@ -6,7 +6,7 @@ export class AppInfo { /** * The current extension version. This should be updated with each release of the extension. */ - public static readonly VERSION = '0.27.0-alpha'; + public static readonly VERSION = '0.28.0-alpha'; /** * The API version to send to the server (base semver without stage suffixes). diff --git a/apps/browser-extension/wxt.config.ts b/apps/browser-extension/wxt.config.ts index c93df8691..fc85baef2 100644 --- a/apps/browser-extension/wxt.config.ts +++ b/apps/browser-extension/wxt.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ return { name: "AliasVault", description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.", - version: "0.27.0", + version: "0.28.0", content_security_policy: { extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';" }, diff --git a/apps/mobile-app/android/app/build.gradle b/apps/mobile-app/android/app/build.gradle index 9d935ee1a..e0f27c8c5 100644 --- a/apps/mobile-app/android/app/build.gradle +++ b/apps/mobile-app/android/app/build.gradle @@ -93,8 +93,8 @@ android { applicationId 'net.aliasvault.app' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 2700102 - versionName "0.27.0-alpha" + versionCode 2800100 + versionName "0.28.0-alpha" // Instrumented test configuration testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/apps/mobile-app/app.json b/apps/mobile-app/app.json index 93c0bd0e8..2eb685cd0 100644 --- a/apps/mobile-app/app.json +++ b/apps/mobile-app/app.json @@ -2,7 +2,7 @@ "expo": { "name": "AliasVault", "slug": "AliasVault", - "version": "0.27.0-alpha", + "version": "0.28.0-alpha", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "aliasvault", diff --git a/apps/mobile-app/ios/AliasVault.xcodeproj/project.pbxproj b/apps/mobile-app/ios/AliasVault.xcodeproj/project.pbxproj index c126f364a..57fb5793a 100644 --- a/apps/mobile-app/ios/AliasVault.xcodeproj/project.pbxproj +++ b/apps/mobile-app/ios/AliasVault.xcodeproj/project.pbxproj @@ -1456,7 +1456,7 @@ ARCHS = arm64; CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -1513,7 +1513,7 @@ CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEVELOPMENT_TEAM = 8PHW4HN3F7; ENABLE_BITCODE = NO; EXCLUDED_ARCHS = x86_64; @@ -1529,7 +1529,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.27.0; + MARKETING_VERSION = 0.28.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1558,7 +1558,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEVELOPMENT_TEAM = 8PHW4HN3F7; EXCLUDED_ARCHS = x86_64; INFOPLIST_FILE = AliasVault/Info.plist; @@ -1569,7 +1569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.27.0; + MARKETING_VERSION = 0.28.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1710,7 +1710,7 @@ ARCHS = arm64; CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -1768,7 +1768,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 8PHW4HN3F7; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1804,7 +1804,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 8PHW4HN3F7; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1840,7 +1840,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 8PHW4HN3F7; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1900,7 +1900,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 8PHW4HN3F7; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1956,7 +1956,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 8PHW4HN3F7; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2016,7 +2016,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 8PHW4HN3F7; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2071,7 +2071,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 8PHW4HN3F7; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -2106,7 +2106,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 8PHW4HN3F7; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -2140,7 +2140,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 8PHW4HN3F7; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2200,7 +2200,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 8PHW4HN3F7; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2256,7 +2256,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 8PHW4HN3F7; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2311,7 +2311,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 8PHW4HN3F7; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2365,7 +2365,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 8PHW4HN3F7; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -2382,7 +2382,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 0.27.0; + MARKETING_VERSION = 0.28.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -2414,7 +2414,7 @@ CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2700102; + CURRENT_PROJECT_VERSION = 2800100; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 8PHW4HN3F7; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -2431,7 +2431,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 0.27.0; + MARKETING_VERSION = 0.28.0; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.app.autofill; diff --git a/apps/mobile-app/utils/AppInfo.ts b/apps/mobile-app/utils/AppInfo.ts index caaee2859..fedd1bf52 100644 --- a/apps/mobile-app/utils/AppInfo.ts +++ b/apps/mobile-app/utils/AppInfo.ts @@ -8,7 +8,7 @@ export class AppInfo { /** * The current mobile app version. This should be updated with each release of the mobile app. */ - public static readonly VERSION = '0.27.0-alpha'; + public static readonly VERSION = '0.28.0-alpha'; /** * The API version to send to the server (base semver without stage suffixes). diff --git a/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs b/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs index d79fc7b41..e6faf5bbf 100644 --- a/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs +++ b/apps/server/Shared/AliasVault.Shared.Core/AppInfo.cs @@ -25,7 +25,7 @@ public static class AppInfo /// /// Gets the minor version number. /// - public const int VersionMinor = 27; + public const int VersionMinor = 28; /// /// Gets the patch version number. diff --git a/core/rust/Cargo.lock b/core/rust/Cargo.lock index 69c16c80d..01cd6e3f0 100644 --- a/core/rust/Cargo.lock +++ b/core/rust/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "aliasvault-core" -version = "0.27.0" +version = "0.28.0" dependencies = [ "argon2", "chrono", diff --git a/core/rust/Cargo.toml b/core/rust/Cargo.toml index 56cce5c62..6e2046e55 100644 --- a/core/rust/Cargo.toml +++ b/core/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aliasvault-core" -version = "0.27.0" +version = "0.28.0" edition = "2021" description = "Cross-platform core library for AliasVault" license = "AGPL-3.0" From bf60aa6902d179ccad5acbc84ff0c5f3941dd248 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 6 Mar 2026 13:07:23 +0100 Subject: [PATCH 14/26] Update print-latest-changelogs.sh --- scripts/print-latest-changelogs.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/print-latest-changelogs.sh b/scripts/print-latest-changelogs.sh index 8a5e265c9..e56630d71 100755 --- a/scripts/print-latest-changelogs.sh +++ b/scripts/print-latest-changelogs.sh @@ -34,7 +34,20 @@ print_android() { local file="$locale_dir/changelogs/$latest_file" if [ -f "$file" ]; then echo "<$locale>" - cat "$file" + # Strip trailing whitespace from each line, then remove trailing blank lines + awk '{sub(/[[:space:]]+$/, ""); lines[NR] = $0} END { + # Find last non-empty line + for (i = NR; i >= 1; i--) { + if (lines[i] != "") { + last = i; + break; + } + } + # Print up to last non-empty line + for (i = 1; i <= last; i++) { + print lines[i]; + } + }' "$file" echo "" fi fi From 05d438eb97edd92a486681d12658c1dd4233289e Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 6 Mar 2026 21:48:15 +0100 Subject: [PATCH 15/26] Update FormDetector.ts --- .../src/utils/formDetector/FormDetector.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/browser-extension/src/utils/formDetector/FormDetector.ts b/apps/browser-extension/src/utils/formDetector/FormDetector.ts index ed3fc11c7..408cc6c99 100644 --- a/apps/browser-extension/src/utils/formDetector/FormDetector.ts +++ b/apps/browser-extension/src/utils/formDetector/FormDetector.ts @@ -399,28 +399,27 @@ export class FormDetector { } /* - * Check if element has zero or near-zero dimensions (effectively invisible) - * This catches various hiding techniques: - * - height:0, width:0, max-height:0, max-width:0 - * - position:absolute with clip/clip-path - * - Any combination that results in no visible pixels + * Check if element has zero dimensions using actual rendered size. + * Skip this check for positioned elements (fixed/absolute) since they + * can have 0x0 dimensions but still contain visible children. */ const height = parseFloat(style.height); const width = parseFloat(style.width); const maxHeight = parseFloat(style.maxHeight); const maxWidth = parseFloat(style.maxWidth); + const position = style.position; // Check if element has zero dimensions if (height === 0 || width === 0 || maxHeight === 0 || maxWidth === 0) { - // Cache and return false for this element and all its parents - let parent: HTMLElement | null = current; - while (parent) { - if (checkOpacity) { - this.visibilityCache.set(parent, false); + if (position !== 'fixed' && position !== 'absolute') { + const rect = current.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) { + if (checkOpacity) { + this.visibilityCache.set(current, false); + } + return false; } - parent = parent.parentElement; } - return false; } /* From 2d0b99d2d8024c44f46f0b7fbe52a7a66224815e Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 7 Mar 2026 13:22:51 +0100 Subject: [PATCH 16/26] Tweak unlock screen button sizing to work with longer translations (#1819) --- apps/mobile-app/app/unlock.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/mobile-app/app/unlock.tsx b/apps/mobile-app/app/unlock.tsx index a29de5028..1ef8a7c0d 100644 --- a/apps/mobile-app/app/unlock.tsx +++ b/apps/mobile-app/app/unlock.tsx @@ -391,15 +391,19 @@ export default function UnlockScreen() : React.ReactNode { alignItems: 'center', backgroundColor: colors.primary, borderRadius: 8, - height: 50, justifyContent: 'center', marginBottom: 16, + minHeight: 50, + paddingVertical: 8, width: '100%', }, buttonText: { color: colors.primarySurfaceText, fontSize: 16, fontWeight: '600', + paddingHorizontal: 16, + paddingVertical: 4, + textAlign: 'center', }, container: { flex: 1, @@ -418,14 +422,18 @@ export default function UnlockScreen() : React.ReactNode { }, faceIdButton: { alignItems: 'center', - height: 50, justifyContent: 'center', + minHeight: 50, + paddingVertical: 8, width: '100%', }, faceIdButtonText: { color: colors.primary, fontSize: 16, fontWeight: '600', + paddingHorizontal: 16, + paddingVertical: 4, + textAlign: 'center', }, gradientContainer: { height: Dimensions.get('window').height * 0.4, From 325be7016b04f507aa13918775f8e6ec63783770 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 7 Mar 2026 13:45:00 +0100 Subject: [PATCH 17/26] Tweak settings UI so both label and value will wrap text if necessary (#1819) --- apps/mobile-app/app/(tabs)/settings/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/mobile-app/app/(tabs)/settings/index.tsx b/apps/mobile-app/app/(tabs)/settings/index.tsx index afc5e5f24..1067852aa 100644 --- a/apps/mobile-app/app/(tabs)/settings/index.tsx +++ b/apps/mobile-app/app/(tabs)/settings/index.tsx @@ -283,8 +283,11 @@ export default function SettingsScreen() : React.ReactNode { }, settingItemValue: { color: colors.textMuted, + flexShrink: 1, fontSize: 16, marginRight: 8, + maxWidth: '50%', + textAlign: 'right', }, skeletonLoader: { marginRight: 8, From dbd86b27352b81b13a4450ff8e5057ecd4c3d872 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 7 Mar 2026 14:38:08 +0100 Subject: [PATCH 18/26] Update Android boot splash icon to be transparent (#1819) --- .../res/drawable-hdpi/splashscreen_logo.png | Bin 11523 -> 0 bytes .../res/drawable-mdpi/splashscreen_logo.png | Bin 7211 -> 0 bytes .../res/drawable-xhdpi/splashscreen_logo.png | Bin 16568 -> 0 bytes .../res/drawable-xxhdpi/splashscreen_logo.png | Bin 27473 -> 0 bytes .../drawable-xxxhdpi/splashscreen_logo.png | Bin 40907 -> 0 bytes .../main/res/drawable/splashscreen_logo.xml | 25 ++++++++++++++++++ .../res/layout/autofill_dataset_item_logo.xml | 2 +- .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 2325 -> 0 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 5569 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2979 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 1590 -> 0 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 3652 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 2014 -> 0 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 3084 -> 0 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 7603 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3984 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 4639 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 12239 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 6029 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 6285 -> 0 bytes .../ic_launcher_foreground.webp | Bin 17384 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 8155 -> 0 bytes .../app/src/main/res/values-night/colors.xml | 7 +---- .../app/src/main/res/values-night/styles.xml | 9 +++++++ .../app/src/main/res/values/colors.xml | 14 +++------- .../app/src/main/res/values/strings.xml | 18 ------------- .../app/src/main/res/values/styles.xml | 21 +++++++-------- apps/mobile-app/android/gradle.properties | 3 +++ apps/mobile-app/app.json | 21 ++++++++------- 29 files changed, 62 insertions(+), 58 deletions(-) delete mode 100644 apps/mobile-app/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png delete mode 100644 apps/mobile-app/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png delete mode 100644 apps/mobile-app/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png delete mode 100644 apps/mobile-app/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png delete mode 100644 apps/mobile-app/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png create mode 100644 apps/mobile-app/android/app/src/main/res/drawable/splashscreen_logo.xml delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp delete mode 100644 apps/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/apps/mobile-app/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/apps/mobile-app/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png deleted file mode 100644 index 463986f5e568ee9bd4c4017482902da884a00c9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11523 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 T_jB)|9)gUdf<*Z{!@&O!-w`|r diff --git a/apps/mobile-app/android/app/src/main/res/drawable/splashscreen_logo.xml b/apps/mobile-app/android/app/src/main/res/drawable/splashscreen_logo.xml new file mode 100644 index 000000000..e5bcf6e65 --- /dev/null +++ b/apps/mobile-app/android/app/src/main/res/drawable/splashscreen_logo.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/apps/mobile-app/android/app/src/main/res/layout/autofill_dataset_item_logo.xml b/apps/mobile-app/android/app/src/main/res/layout/autofill_dataset_item_logo.xml index 2b2747a3a..577e7a1ad 100644 --- a/apps/mobile-app/android/app/src/main/res/layout/autofill_dataset_item_logo.xml +++ b/apps/mobile-app/android/app/src/main/res/layout/autofill_dataset_item_logo.xml @@ -12,7 +12,7 @@ android:layout_width="24dp" android:layout_height="24dp" android:layout_marginEnd="8dp" - android:src="@mipmap/ic_launcher_foreground" + android:src="@drawable/ic_launcher_foreground" android:contentDescription="@string/aliasvault_icon" /> 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 diff --git a/apps/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/apps/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index 9678d66e06d23dbf2f3ea9b6f47e6a4654e49d36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2979 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 Z{{o_|$w+l%B$faG002ovPDHLkV1jYwg8%>k diff --git a/apps/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/apps/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 31c9ed39af0102873d452ab898d536cef205e504..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1590 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 Gg#Q47LB4kY diff --git a/apps/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/apps/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index f94357b6d15adc9246575b6934f74606bf57bc68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2014 zcmV<42O;>0P)!YHmC}KsB*FAqba!2_0%TGKst~BL;(iC4%7lyfYU%ZZ~!Q!hXg&71>^&HKrG!?4U_;yKq+@YaCdTmr9d|K z&K_V5u$yiWbWbX<3YbOjDF&7SXXye#*A)Q21q}2)JMbc~kxr;|&0641K&5Y}z;wV0 zd`1u|o!bgL$Ne!5NC%1uKqa`Xz&yGjEjdn(O6x~*LL}D6aB=Il4X4w^J&*yU1I2Ve zM__A#c?6L(AetBFXYu5OL>V2f6KJ&9C0`u9&U;%9ark%zJv0xf0hZuL(6Itw13@gx zpUA7vW)h-T2u4cS{P{&*S+i5xYz|>I8F@G(i2*T2TyD5{t$`z7UZBNhrwg73HliW8 zQz~!*Fc84IujKMf&S1Ku^h6C6S6j%Nlz`Eopw-ag=HtB=c3R3j7&#g72h8G53k#96t4pxO}aa08}6z_%l8# z-4^mVZdSFEFKEqwv-^2E*LBIs-QQS zjXFMj?J;yZRT9`zd=%Pc0oeo)W(*7oX!2ax7H(O+; zMsYt(9Gxh4AFLF$^^FAJQ1D8?k2TUv$?6#-#+kTZVxmIC$8SGLRD_8D5){0m@bmUd z)8wEy6TMAJNifOrW`48uQAx+5ba)xi$Q+i)%t?a;e$HIF&AM&JDJ`$0si|4IB}gfS zbh~g%kP_T(i9{kLxZM&7NViK$2@;S}A|*mfNh>KOilPu7Yh~&qgIV;05j+@gLTl!v zLE?AA5;=CV5mDI3p z*AdpgmLq3ON)q@8Ay|+*oMR^|@ncu;szoDvY!X^!r*6oC6`x?UI|RL~#b)REU+mz> zS2raZ6GsoE1GNfXSMZ4pH(HQuEm7ytd_{&Hi zKzytft2!&oe|SKkkrW$-#T}SR1EcYXi3&joL7VDYyZm=~ zg+L=MIhNh;&Bkic^VP+A@mS$j8X8-em-`UwULKFC2uk)<%IDrF5qPX${uFbkCF5|p zdEu@7Z2!C*i`mHD4f&)bn5b)TkhA13oH|!Un}bKsQ+wMm}TJ*FUGANuW4=2-V;mizFMbK*Iq=BMel!b6h7<00dXr)&b z3T9?VJaQ(ak&}hEdKb6&uXf-su@l&qgbpW4>RkuX5L((SP94hd0=3=eD z$L)smRrRzwUsWgZskvpB^$pFkvB}1bI=jT@+~r#Wk296GaJeOI&Q;zL_z5%WX=8Ip zuGZAy=YWdWC7=*6ODTEuyJ>Pj|1g0^q*>1&cbt?>%{GD0f6rA@+oW)#zJ=9A6^)?i<)&R{ z#HPO#iVl$&?_N6@YsSR!b*9w@|PNq!b@6pW?KJ6p)(C-5 zeUp=|yT8Ow5gsb-E&&sO2b){$I9w`I#|)sAst9IGPNBse!tpaz(&cgsdVw)CgvARc zvwHD3bgH1u%0HB`zwA63dw`XA2)-=`*oB7>f-S$E%@dC#pjFrC;$J6gxpeg=E~kSw zQWBI9bUFU~{;6ciSnJ ztoj6--GPQ3m<8O#N99g8fofnn9#TsFS@t#C_nrrxg}D9}!ow7LLv^h~ZvOOZv2f+5 z?Amt%DJ6aifxn|62y88|m=457N0OQtfldKMLDEA~LdCUaapBT+f>;ME!H*!gt-w6) zzmI_VbU-DzVjvyJ;Qsjtm`?yIoht^cz&QHeI-r0cRJ!IfpbB^#Q0aYkpb%I|Cj{M- z3akQV(R+%4Wx!dwKyY_*fTch-_s$+*4X~ST5cEtIkPqYmv2z>%07*qoM6N<$g0HNg`v3p{ diff --git a/apps/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/apps/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 216c2012aa978ce0c1ccfb08966244a46089f7d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3084 zcmb7`_ct317spX6MkTR%h-!;GR#eO=Vy~Jt(xz(mX=|2RDN!j(td!OYYP3}~YCR}g zTbrs8vqTq;aL>KpbM86!+)uK-tvNrB1P==f3%{j>i6b+6 z{OjDD%(#>rq0Pd=J7#Huyc)^6*~K3{{xha?XLvgzr|m+{DuGJ|cqY!nC|-v3OcEGY zU2LRikBpgkW}8>bDjBvvG0i!<)PtU~z1ieg9o8x@j^}>K7-7rif&jUJ7VPH)2n2#5 zcsH0tJw>!4Z{`iqP5==U?XBM~4}oAI4Nz}MX%aHCJxxg!fo}8;#vG9GY8~nmC-E~|67ypbUB{kJ!NGj?}UbZPR zyej1Dj>*nE&sFC~U5MD%D?ig7=wPbwetB&1EWsvbwJUM6QH&xR%cLfSqb}IOqu;BC zWI4E9QRH!AWcYOT3x4h%QFI`K_RXp&5}T!6ScvyaVvDad@f%$CZJ?2- zzgt10w?;@V(#LaVf4&(Wq2_LQ+DOHoDN}c{v`&}((`ZrCL}IwP0*?{B=v#hB9~(X_ zElsSv!rJZZ(U3qIFeoU$Ma@h#m?(@O6?9A&zwwheY>0A-ynRbV94gWdiW_~SauPLm z&V8N%+T4^v^YZ|Yzj)Qr*Ru}PLcPGQ#^$2;TV>>M*b1H1?1_%!tMBWgv`%iZ;&&R# zUFQb1c*a_;Rf*I-CI^v0*QgpgX_DsrSqBji*TNe&7%Bb3TkqAMn>Ji(5urK}sx&Wm z5MIQKb=kqOPoIh@4l5}eLi_$+f92^=bxP6`7ToNFz_I((p!ruzW$>w1vN*Y&@o1Xy zsNZgsQ?9P|wpiDa;vq>XUkgQ7OM4z28ZND=h@|ZVQL8e+%j}R*28r@LkDZunF<8$f z2aUE^<|<1Rkh?r>2E)*YY`jXP82EmHKyh^wZbvdGUuP_IM>q}QBVgyqUiWtRQ@1M| zOAy)y;@ptodUa}K2CiSAN~BUYDFL;Rt!7N;NQ2zhM#RfRj3o(0BbT{0ZJT93@!KYM ziNdiXG%&94e779qp)jQ0O-ac;5)sNf1_&3O(G}MB=rcF-;mdv=8zi@ErY$rT=k1)v zzzIWg5qlyh_omrt+*4@PSLu?UQ%*fWIRn)#Y$Ck^_?`**Iit%=*g;4WAS;MzH5MW0T#Oe<3a?*3$~ApH@lr=HcZ|b>Ib~vap+nm* zS9|9%nW3}g;7%C3F*~R?7=aYRd59^n@)-jS*_lBF36z&U{DEysBFV;m7Ya$Fn0FDaz+Th0RFJg)nH1gwm%svspt8^6t@;l z{a7k|3*OuswR_Vl%9nap7tz*YvJ(qEvL(mz2%uF&KiW1`ddvGo{%D5OeFk+cB_t?4 zCnldqFQ!e_?tdVn+FRKglnmA6di%`wDBfeW*XZjbB+V_9`C!aiGc_t4iHcK1T2MyD!X>l@cHN|)db z+3iE+zN_r2-95#(N4dX@=EA*~U|lJ1F!H=E0mbiLrg@A}M*DFcG1kr#VGD!R%cE-6 z-lOs43cPz#=S}^Fw4=m5yv#&!O?n||E`YQQIfC#1`%1#-fuWwX4 zd*T4y-mWg!$~&|9byKs8T~Urg&}6Azn^2vMNknDsHRKVptcO8FXo6_^r(pNPo#HUbKQ!45xwIB|^&7D0rcSa&TWMQep2PaP z?V*(cY|~v4rsjRo)mnPxww<@zkzwlq1c8J6j#6>qJso&a;5MnG*iEddQE?8b%9|6D z%`H(wlH1nRl_R~IXMM1qx|i#&Eu5g<-9PB8Li@X5;L57W<16ecDH$o>Dg_NWCIpd_9eDA@k`^_@~JsX}hK+cw&>vitqtH+qS^EegH@J!Op-xTyS= z$-RQr=aesI4`hN%`$~5d!VHC|v~-KEEIQ00HN!EIybfO*tT@t407wX)&7NA&q${ZO z$7%gFq)8sfo6Fm3jot7Ks9cDfedrW6h<2G8DMbhB=|;6CZ&_tb@k%!~^8Vx>#EG75 zoZ0u_h%m`)u+kErW5o*KREyn=c5_!LQeJj@->qXO}PBA^5S=rH<$#ijv ztY6P20MZn=9%ma>E^_BUHCE>Bd)Jh6$D~6Stqi^QS@p{k_aioXp+6T6=F5K-TyHTk ziS%1Vp?3BW(bM>Ht%`ZU`^p{-zPfh{rxMZ zy>2$((yd6Pq77ApN|+@p4lrNj+No|WzeIXuz>p#~KlAACd}Tj`X}z)k3L{Y;r%v=ZZ|M#>S<96QU&i>jOaH5A#$4ImA-IA~ zur6X^Qi==qgH9IwLmz`n5pr*A`nw_E0C{8t?HdhR0Tizxb0jrm-D+T#-j1{i-Eir< zy;;z*<#&~^43B2|R&=1Wco}^m;=vE*5_*-cK-yaZA@2`Kc<3dc5f+^D(5G`%QH>Ah z+FP)Yn3C_0=KT#mG4CA5b;e8SF16txGrz#FR-CAws&%^P2bLH&Q{YqVGD~I9j+4^{QC(U z3aoz)B~}`V;!!s z;N?MMQl3e^M$BJCmrJOMU5c0x2`jBn{OpT5zTin9@h_|h;T^^>G2*TAbUX~Jj#ul*Nc?Wz07>Id5jsJ!n!XVX_OwNb+; z5xu7L^?XdtQx1i1JYm`h)M+3v zM;6si5C5Ry*YcwOx)8FswX1K2pVr%MVfVWkeGtWC0 wu6soB`+lqpZlf}YZ1IGLP+jrKzn+jgbfLKg+<&{{R30 diff --git a/apps/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/apps/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp deleted file mode 100644 index 288a6c0bafa039a7ba9b48e166d2e91a7545636d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7603 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 XFUjDsx=7zWBW(cQz)b&zE+OWBZSy`h diff --git a/apps/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/apps/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 2b4ae85ea2557bcb41701f7df487215c3b5f9732..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4639 zcmb_=WmFVS)IQy?bcetqu_72qNP{dP4J*>HG}00hi!{5SAnXFt9ZN__NC^ll-6bK7 zk_$-JyT330PycW4d#28vXXeg5Gw0m<%!TXefT=0iDe&;{s5R784RB-Pe~pX;*Ket< zHsRsXZEC1083o|)b&>|7PMW_T{ytWC5<&IzJ(2Qz+C*_xB7HqOA&OE)Ev~P_reIeO zx34a45?_aZ{gOd^-NVHF3iR?I(l4c8e4%uY!iL`_K~ecs`oL#u#I3yf{Z{KiU-fN= z6+YvDcc1lZZq^3_gc0`dtg9aYs8Tkyn+ z8vo-z*+YWJO+I~_69{-V1 z@N$i(h?{wO)YWQyxv+a?r@H~G=w?v7Kpb7&1YT-%uh)9eZ2qytS5;5DydUs)(SA#4V*2)xN`W!JE?%CoQUl|O+@Mp4hVcnLCuXHw6R#+Xl7ujDtgS!$%Jr@9i^>PW zzso$tEHPaIIHJnh%SjxlVRw{;D%u@tB%Zy(8KJ< zX$+bMWEWmVXl4hoGTTd^xN}Sp&)5=qZoT1OXRd(DY#(3s17IGP3!=*-=hx5EtgEKC z-U^x&;Xs5)Ye$3SLFR4MU{*)uDb9Es>$gI98|Ah0zJHt@{l-4tIGQp&A^Vz~P;_OQDM< z;kb8i!Xn)le246Q5vn{tia6FuTy4EKo!Vk&i?8s_N!D#>RL9P@f0PGSt^nC}KOd%x zmv_JC);FqS{_d1)JgIt>7TXyzld0&S#_rZLN>f+}^tp}Xt2p2AKk_+72~I$$_P+mx zl8!wxCK^EbV0`WMI#>(q>=v9a$ey{*5m z!a+|L_*SDwGqbPYYCN6FO^BAv$&?;KkR>jZTv#Yx*)tkVfZuT*f}HQt?`2B))~ z-IFtBdbtEeEmsRTvy%L{+KLAmFzF~lOs|%pSfNdqEeuVk6@0jNVXl971v8BU2o*&Y zl@uNQBv4xf^V4>SC@*B|8S zGG|nioU)F?c|(Hyw|as~4AR!RT`XWZ;x?+fOP1}g8uw=T>uFUe{~AtaIfnW~`}NUQ zG<-@SO8D(Z(H7)qXd0VGyzW1|+ShV_OuCy4lF_g8h*QtZQvH; zUJBE97oNcN=rq`lopN4ol65pu%Z^|&>m>i{>R%}6Ww93~VT6``Fi|lX16W(AxiHzA zAYMC`zA4NDCh;mgelp&4eAR7CRGvj2W@Y7D;WzkFP@jt2`=$u`Q$sR53Z0KG6+t7= z`4s5f|LRA=|5sGv`ZXlI*X*Ghj0)tVbwS)LnFi|ls`=G#42@VSXO%84MQejRm_Fzm z3!=LOIgRsaa`cV!wZ8`Udnuwuq|n-lnM7GrOw0UaBfWYC>ifnjzV}Y&xnZYve*+)z`xIpv$bv|*cZydk+Won4D>2e zvC+|Lr@M<71ZpQBaFb7d>Pype7G1fpI%bIS@v>eMVO&Tky`FKBlQ@|g7$X&9V{B{8gTdlP#7GOz!0-eM?>a* z9#Vbvegzj%uwBbzpP_M{uE$j*_pfK_25+yfOb*}KOcUs|ZNHXZnu|3v>}6a-bw+R# zTL=l0t@KW}X}@g0eBD3e-s@cwyyr07bjkMY!2QREq;Oh`7Q3~m&WK0dag$_T<<9Rt z?t+;)o*jHYaxm8GKSYMyD=#?|i3I~G6hf`5 zY`?}@$x8via4j#G`ZcU$HL0_xt7?uY_=o*wOOWr$x1sPUzLYNu-!njHIM_7FATfu( zX&qB!xmX)J^N(eTMfOi-M$c=87_XOi!$gG2bLoqh{qwGNhvNyc#-=ew#o02dF{pKQ zO|phvxcFHc#pdvxVn%v;B`Tt6S3Zx!Yujo+NmI^XS2NG*Yijgk!$!8C zU1_}WJWC&17hbw>AOZdU*ue^cH|GH$_nVUq`*?1tBop}eW!Ocn3FK3O{!c zAorLh^_aXfDz5nBN@^+%P~t~NJlm{NRr+{D>JnSALhFfe8M2IpEJIDdkkbVVclftax_Sj@n)ULmNa?Tv1go;Qn}?R zvt{?_ra_nrq_cT^pK~!RC)4=!-u(|u-zHBg7;_##qnJOt$hk)@FI zf5IZmdK5=K?+%xQsUA@Ih_@XR$ev|Puo;cVsgTF03^oOcbR&|b(-L`LewL3EzMUH_ zNO|T)*M9KD`Q#=$RX8SQ=WJOmb>V@g7#pzH(}e%hcRBw_3?=Ex+0{vQIhc;d)5KW= zTYix9GjG9rsxLj+A+lRtD?I26SInd6i011|238Z+)dBM$9iAf@#}&1?Z;Q&D`3To& zh(S#OyX3HJStOG7k#y*|BF*M`UPzIUnEC@X>TUI{%m?WM>blVDv8+f|Dj}*TxgNYj zY|tjpRk|O1!eox|!kr(q%|LLl8(|fjIm4>Hu|)*OO`HrvxupdGE_O~VEc1V(Eq@QV zWZ4oP`nR23kcan(j*jHtaB=gJ+_CV_XFpu@a)z5~8o$8O)cAr zF}x0cxeORtrh{9-v18eU`NmTcPhZv4MF+fD(yx44=k|&Tty%HxO=V&86i0(IG&0R? z_8X66Qn$Q-nm*|>kNF5Ql2nm~ecRAB+Vbm6ns7?7w&v(km6r|=XN5kljV@$Hl{-7} z-RJOsTC-a~{j|~f*l*gy1IXNhHf9mE5PVwa>R;5_>Xb@P2wo|YoFRMoc36|i)@XtZ zbp=>X=`3=3+K<+j_<{CF_}Fx4bD`!ixY(aMXH{^0TNA}OIK9`Bc6u$dp{2@o-lK9r z3s;SFz_G*Azgl`5T8k$?4P0Dzq|!3>L$wXuesB~;lldP?eJm4K?TtAlRDCItHOHCUB$qKL}MjU zjq4Ml4Ldmhy!mLF)07cPiL;Fqm!?qnT96xot&ClQxj*MoJ0c4DqVBi+qoONR>VK9IYFTX z%VP(yXDG_vErz)0`+v@cV#*d?MVXNR{0`;>55AP~LL3u7KsiySFGeVJAYXu+H1u_BbqYT~IB?KXgp@sY4rPe=V>U?*s!^lb!@Lvp<%hJCzp(n;NcDw^ zd+&?P_xKq}x^hp&fIr0=K!K_nK&v~p%QZ@nQoRcBtsjvl+7Y`hzYA`%Dwv?fMyC?OfWe-~@ z1_fy;WzhRSG56i?DRdP$fSGrkm*v*F7 z^0|IQI9J2-%y?JPxaL-rmh6wSAa%0Bf|Q2X^GZc!{MFOTq_&0N-vhuTJWT@M!I#ke zoRN}y^9g1gej;LG6{GJCDw6GaDoebrn5sioe~}R z9RLHv2@ML$J|$k^b$&)gVlrZ*qzG%G5%zTJ?Ga1I+ZKCZI-VwFnv@?a;CBEgJ`aS$ zn-S(YV(ZY_^gd-7$BFS>pS`i!X~z=;40N+rJX@B+9>)xPzMV8TMf^v&D}?UShOWXh zpVm{#M@%93^qcncm@c<4Rt%@8ct%S;yP-N77xNFYy1(IOs+spljc%s2;%H*0i3U^^}UZDd81Svq4#( zd*@mPZ`M5i78;w&vm?&VC+_H6dHc64>hWFH<6pR2d;V!r*ii6;oTeH;Y`P$>YrRN|61Fr`|RxOk$&oUJ+IN~ S364wr;AyrLHW5J)oPu9l|JGm@Q8;GA6JT=?$=rFD@8)ZR<{pX&%JH91WUatBXqww4HA zYfJwHUval$2POo>hJyT4^y}Ey5u~E`UgqI&_x?Hr<~I>t)^q&T+xPm0o0)7=n@(=~ z?_Ql#z_3sR)&FZBf>?IsW5e+7wojSmIrBH&KIF4zk<*nlz55W`(Cqi)RqtSSlu|Dp z9pWB0nF!6jz**jTi;ChF4Yz;@lwUqm5kp?(?Ed4Cdp2W@cC!Qf&db*YZrjVs+M9nc z3>uiB2x_#Pb2A$1kH9f?Yn0sB5TZ-x!ellrGDoaj8jHfcQU~b8CpD{C&9VvLUKac4 z?#h0c)#%4BmCjcJ$@1zqL}&asMs?V*SZ*@B!bM!$gr?nTQ^L1Bsa|Y*V zDLd3Yk{D8l*?5kg8*9;SB^ukB-i0zT(H52lD}0cGA*j{5t(!IAJNm)IX4Ed91b8nS)A?#hY&h|xrb zOXEgTF{xjCrtV_)WHZm1TZSVugi6#01&+=IcAVy&ze!xaVRRdcz>(VWy;(`z&00)s z^=eXY-7pkyWACSbq~mz5({fZV`Ms**Iv7ULse#a5)4An-ntp$7{-$zTrmJ?7eODc4 z^4S};Lvd?)F|!yAF0lU^dZz#amHEt$G@#hXGb>&H;yXX` zl{YFwh_J=9Kbcu@IDctSf%#&xl9GBolKRO;l5yGg!@_|-aL<;HCSW}0j#I@4pXIRL ze2B`|MCVleK4dlFLF|8r?_xjF)*+upKxF$8-;zSqC=KQ236r1ortB9O48f)(Hhk{v zvSQH%b;DO_6O9KU7-MG?37$(DxLM<&U;TUmKc8&~nYcRn2StCi@;pO{hhr7O87Z=V*^n)tPt0 zuFAU-b$9Tx$W{^9{^Cl1 zO(#qJy?;_aeaHaU%x}ikVzY;&S??a9i3iqMzF#q`en*sbXiZS6Kds=?H4LV?TVVgRGONG&x#Tvg67Khm4rc!%qDIT59H54s#JP|l{t zVcRQ+oR`A2rebs&)sml2Y&<{ka=xK)kOWcrobU@v@AUx~>nuukICtUYuYBE^Aq#tx z;c2>5Hd;aV**BSZl>b%C_~D8#a1$`ySdRD7EoVJkJ0Hh8U$b}>`Kg6eawUFJhkF!X zcK^7%=eF&XGwv=k-=nn`yh=NT^FQxfQ_6v1Q6(2|)!Wbcd$kVA<+l_n zo7T2C9Sq3{X&!&`9`E{G?PG)`=@IeTVzS8Vs+lf47ER5|(;uc5DmCY&NW6L?1~k5H zToT(v;jQdgmRweu@gP>R@98{Sc~(KhLCzJrMLyjDu4jwZkv-w#n#lj0#akC2Skvh= zJfGLUzQX5D1ynFCF z2op*wh|*5!tK41nkcC&@e}KG@+$-*+gccKYYhR)f)U~c2@T&Ct_ zL}aV&r(cu)ZX2I9337|cdw;XwwNFI|`JOm<&wama&85yD4T4-j@uO9`wm%fB#Y$9) z1g-*EGTkpvt_=7bviJM^td^3WsBw}5m)~bTzhX3_fyxXzZ+vbW6~B^b6V$~o^VQrx zQ<9KvV++T*h$6Y4=}NUb!TPzu0hX8Flcq&g>RGU;M`p?sz<&&d1D?348?rq0hnpT-!(Zl4OKshCIaekpbecHnI+$(#jL)aU2cW#E`WS|a#k}ol4 zuTh@3j{7Cb=IOGxX|QSGeA5B0Qu~{vX(IFo{$FMZ24+p3P(ykn8}OGQ|HA+63~q89 zF$=@VFrv1fmmU~W_CvvEEe?J*6jUMtGVuBy-XHHt9b+T8Q~!7)?lF*=evJA>4WgRhA8%gQJ34+k9mz;i5ErOIpuff_~|`TbN3ePSyA zZ=+oAHTH2}KM~1^%GtQj!UEhYBUy|fR0PhesJJ+1)ZRcxkpPnxVqwqZD?(8KGymbg z4I6yRvJ$~i!|I4x!;J(kVP@bkBWM_e0h=bWnlN~*+hF_}<$=3nhqjPGKLd{3=qy`I zX*NrP(0d_K-C-29S9GJGI?7XN8+O=L!-fLw4H(EK;C29HB zweeCKmjgW0*Xerwf@SKqb|ts?%oz|=owS*{T$Bbvw9wP#mgFxy&sMlJ%MWRR*@U=E zc!5lnuH$v=(%^WB={oqEf8Oxtc+a}pv6>T$>*x~rU|CQg9w%e_8Wmu#vWE5xzB&e; zAPFj1&0zJkLyx&CP^+U38r%f0KU>wO#q>nA@j7a~PB>)_THgH`Ew8ut z62944Mgm*eGqhUzp5)fP-!*g8z@x`3>Mke2lNRSyv?U_Zz|8ZNb$1@uoog>`uQIlY zgnIQP^iL1>vbL@B7Z&((kK;P!1n261M=J!+ZvE9eZS3C(Sh@%FET~LznryQM8oXB7{x@28$|AHb*+t8SGiA4N!$&c>f`q^1}N0(o0%H9->W#gqe_Hain7w=)d!|fjdX$Q}ch?EQLKuZw&D~TO=(#Y)}uQe*#5N>x(f8GUP0H z?!bAiTP{CZrAdV|tE z916EysL2_n-~+l8eU37oOKGh*7EfbkhcQv&DG0a^Lhca=_+#f=B zRX#rd&=S%TI4DnjqgMmsb$qfs#K+Ql|79ri3#fP_d)62 z#*tZbIv+NT7Fe_zPQGwO_|!x(bsCVihO;d&3LfW@A&_4nso?qTB+PTRdmvo`MOmI} z=##!{|6t9_F1gPC|BI#b|9Y`MvO$GQQ}nX@Uw_=lqUzKF36V-MhQ)zUgDts*)4JAEUg6o~VRf$a7El@$7P{`XhTn9eFuHk2?oHFLaq zx`Y+Dp`1smWM4V!@kWGaB0LQ8NCtBc6_Hx~#;ed1zgxbV@>dAIk>8C~vqpoH4R=qO zc7)76P#1)ug*BDots?-aVFpdYde{*ITtOdTE$j1i{T%s;OkG-nB1=`~TB`$nwmo?b zuB^LZ99#;^RUA;6yE1TBG5>-GtI_&@UgoY-#X0%Iq^|%l-P3HIY z3;fWTs3S1B9f>2|7dnUis605(ex9FRw#y7f@Xyz($($4nbL@ZY(<)K`!C;G}K}(-bcNv=0 zlC7&-jbstX9;a5OS0~9g@xCuqu?gW&Y8kasg3qug6&I$_WZ84!Bd3Yae@9o@=)>|P?~`OSJJDn|TNlE$KN9M$?yFK4dvIJhw~ z6%-Jo0C?T_tOL#^$-1-1K z&50!)*C85oBuIwrX`^?R=J<-p@gHbD_OP~H1p~iH4XA{XRGBb&+HJlNk6WZ-yry^y zd16T#_U2cXf*Tu=fKk4T)Y1MyMI~a3Ha^GA1&@ITnu`~}S%`gU{6+v65iOu~;oww< zI?0J%8b{Bi?wQLDofggcSa4!dWPaoQ^=gMXq0(HY!C)IsZPudZVUIqmD(zSgEK;iW z=z%g};x!>*^GyANx!z^k+&fh;qJ*&m-Olw?WlR0vg~(nTXekrq-mZkdgz|I43TERD zNd^tHbOs~(?7SC_r7Jv($q*&5!oX@Ld7i5pow>1?hbXf+zHxt61Xe)s!bjg?Em}{P z5m0?T{;F(fU3i<9NdX(rnhS1a&h@>F31~fXWJ&R2c0$5MpiJDbH)GFfKB(Mg*6tcO zMf%Kt=9P0}V*rq&jmhx!7emS?hG-MoH%OqF2_jGA_yrXah&3I(G11u9?CcDcCCsoz(mS_IjDLOl3;gCq~l#2Q&Q~N|f52J2Mh3JN1$r*PfD4iQLiAHizN^hBBLs z=U(vRhJI0F$#toOB_EgO>`x2J^%l?7ZhBROJm#emu?HqMrefo?f~4f3%?yXyy)~T& zSdcCiIdjmu%Z5>5XF@!FIP2*C9tDit@UNlNAxCjoLrgoYzHqo6ZkGeCc)s#G0MF&sN$->*Q}ZSqR3$@l3MkjxB1nNTv4@g! zYnP-ub>%wWA&L9%umX6N`ZTD{xcm;|)a`YAxG9Q=vt9tbh#$~Magj8rYIP8!CcMqN z5@2-mVw4$s@LguSfxtWGC)%0mA~}b>>MA^HX)->$$>=`aTd_t9vAxfN^I}Fd#kt~n zEEe^*t;%X6ckZ1ju%%SS4*!yKWoxzT`SGzkwj|29%s@lNLrdzTy$Md$gSFnOjOasi zNdvTx*QAv zDn@nBcX}nt3z_qQKl&W}ehND~{&aT{s;qpQbBH?r$-DcGEBP`pR^Bm1xBr2Mx~zu} z02K8BEGjW+)eIS-y~-yAcxvZPsEKDw`NCVvgIJZ*G5v&1is4vf)eF_sXOGWOsr^YKJh%e4Sk=nqc_a#&pF|}ry%N-3`;h`Im*m+bZ(q2cbo~B&zH6O z$iC*Viu`d>xs=izIZb4kbCa)ZjN}+4@=q2qlHM{rkqxt?c8(?;u_~D03bqvqb288( z)jlrPw`kNVX;CMyqGbvD*I+8nur>abEqC7#9MBJg&#V{rJ936%c8{YKtjdW-G_RGc zxFRz$ymZ#V%t4nTK(#036BYh7ZTmwKE8!}&RYP3C`>v&jQ*dxuwK2|$0u(*f3jkk- z<`czGpL4K7?coDctANjW6I+HEB${cY&%vV@bH3Mv20 z8+t?<8a%9Sxj%8mZPn&Stl-(g7-iY*e#d;}o&STMZMVMULvE|zzh6TN>V6~5v*H@L z58m4V<$2heFsLx_(}}>-(kl|}wdB1xMY3V*Wgp^YAfOsZE1`nE9NX9pw^TPD^lxu| zm}>v_epe__WGV59=l4%y|ekD(D6tU}t}t9k_ut9lN96(s`>hGt#0`powZ-ws4j*NBLj*Y!or@!4izZ1IqRTGV~r+#+l6|3Vh zaZ?k&tHh|QabI%x&u!mCmERZFWZ?WBSRss3?i{9$;aDqul`J`DI`hDy!nNeC)5zl0xX zfv0Eq-k;bj(#lK+c;Qq}(JOHSbk|oCpQwSg0U`P~-ttFOgiW^R58V{cG{);Lzcd-Y zIIIV~PS?l-G$hC7b0(A_ZoB-X&)*WOIu_FWMc*$s@@{Gz@#foEg6c|&dixgBYB3!u zVjxl4->1)UWDL>nEbagt#{z*D#7p}@qdFTM{ES((dku6NE#)Kl_j~<}zRZq@5M%-3 zc9THQnMDq0NuoPWn<-ABD$gW@?v@12lZU~Q9WV}>IAulVm#@H}p93`h73L(J|+ z-yOjODjhta@WX9eMc<`j%Ke(IBF!97(&|u=(ZnIlgfU4hfU=(i)ZdQdUlx`b#3Eo) z&f;ne+Wg*k2F)lG4p3md^a1)w8z!JI81*OpdXbwPp9F=~N0VB|Ezsvbq#!aX@2JhZ zSMVkwTIj##27&{zB?eTpK3YYv|DCes3_JG|O&`!F$8IXjbZvz{3{8Mn(XJ--0ktZp z3xmXoS@ediLYiOrB1ol{WJRTv>K;O~-PF0XQ>1K_g+Un8llcD*`Qj4_3EOMNDovJi zF~Q^6F5=?ASo2GC${i$1f7bfeEhyD1NPUNl`{gG^hKC?gWIz^6$wMeA5)#KhDol%> z@|6+WeN9k|PxxUBVSGB92$ZkICHhcsw*0!?tVfDdg85@cH+-io3IZK{+- zj_J`!@kiaUXMg$Hcq9s8qxy3+TAJmB53md6a~zj+;=e(|^s+R*C!=`XW+Wbu#qZw+ z;GDoPU;qwc@tuMm0<~lmoq&ghHD0x2dRDXxI!N5_e4Aaz{L*If)-%=wFhLe@OE}=o z4+TX%L=8Z*)f+lg0ID-ka-*TvXj-lkW~#V+E0`>&8l$?r|8f0U(NMQwZ2a+hCpKA? z`Pz=YU22x`2abT=o$Tv{uDjP!qzzji0q;m}g(F$+iHs{-@*8A*<`WGPsft==SSKST zs+C$Ocm9#u3)=J>dUs)%wyj_PSjbgbdtKuZ@6xr*f$@cDpQWW(&=$}(k$3X&_ z5I>~l=$n{!k)CZTzzh0a%>w9{O(+9wCU~F67&kbu(MLY6rsI&2}~7dgD3kcuP;3QeKRpPo>`InC(9Mbzga_+_>-FM3@q0@zBj z9yk2CUa`>_K?Y~q1mT8#eXsQ|3UTW7A{MwuXjg4JDSwG8}SJ$Vx==TmpU^pqfLJ9T}#_rYv)q zQxospmMq{fCHeL7gTAY}$1Q&(!d$pXFjGE#tnFL*mA%)vq$#wD=3flm-jkh77{`#> zQ^zE_)a|~_*Il>xbG^_D&7>_<7wJS|Ly#1=6CdUiBu6a8*Mvx~C+}QPrLpN=m&WK! z&i|g<^S3zkjpH{9>h>RZ!(3dFJq)o}qclZ`k-hvB0J$RH;9-l&P$8zcqHKerY;8i^*kF~6DI+48h3emD2ladYo(|IqHLaz^~- z?Q6=hHilU;uB?|5AAj=f)o*5CKC^5#{taw8c2f^bz`vIOb|ULfwqwd-%#9rgK3pBe zuLqC5eR98$a1r;M5kN7@0J2NbMSXXvIP3X3HSci_P$oV>lbW_kNUxvO+twcLl@xj} zUx3r**I0)BfW}wa&n50QyDwZh_xUbd$}e|~XnuSwrcUXcG}V=5hYtQ$slFMG{DJ3f zc%|Rk{nptR2mU8N{@QfE_=5qW3C1nV-f8(5^7>Ag^k7v#EU0&5A)!A1dD+Cf;IEq# zZc-DWt4YI=)CC}R`Y4FLQN4ToP|L4zzj26Z)`YiC3b;aL zaz^Nvt{XQ54VOHh~0{L9FQ!`=)MA`FmduLzpP$R(emaI6YO{T)!}YB zfaoMaW_-z7l>y#G#DN-ri#975IXgnI?tH#t_Hw;iD-RHKT2$MA=ehwDj-%qB&&sko za-g42r4iWK$n}qRl`mv2B&Tb%z7oANSZR#C{h=B^R)2CJm8*{3cCj@ z6!A_ZhyWcxyVbGfZO*xQUEq*_&gy_I3od`dEhBEo+o62x7M{zjfap^=X(nas*l0Z7 zT}USx|KIrDU~@5-cF~Y-IMOKxSOM>#W0DyRbNk#R-=of^1D$VVN!6ixjydFME**>p zL%QBNb7>HY{O5hE;Pv}71lhWfv+Vv=-WF2WCn!I_pPbNu)C^F0obErZsrmO@pZSEh zCGM_4c(*Y|xS+KjB2*u%pv-T!4kLH?NwuSwH4-l^z~A(EL)O+6i_N$__0R_8+)I~; zOT~xBfBX2OLB#2a!a*EFU14uY7Km5ej=@Pm`E`!E24^d|jjRgj0t96vzZx~4`+!10 zcovKnOV}+0lARHv!C#amb?sglym^NoFmiPAJS$4Z-{LzsU|o#-I*#TDqoF{ifKoiX zmiS22gM$hd`eqi=%@wjbE(|i5zIB?j+5kI;+^SZLHQ{N;|6oU~!mfgA zW?L+vG;uIi>2rR#9}-3S8$fQ`$xl?6vCz{&nf^7%oMA$`T=hDA{DH#fEZ(g zf-M-?w2(amb6k5!rnwmeod6##a>*OR@wY~ssH4JbOC?#~XNYBtwU0%DK(fk5fb=i}pksx0^@dX=ab}Lex3Wzq66Z3c zeRGIe(B6ia~3YlFF}X@?dyb5P-Xy@jkc}{T6$(FlWrEiFYg*If zcfYUsl^m6b=PqILM$_|-M(dJhlQ*xkYG3JJ+$%a-ae{hr-{`EgJm9%rwGc%LutgPc zsQ(LKLe}en{M0eulRcrXDUh3 zX&`I{B%BVpcM=+^0CTBt%HsS1;9Rl;VTb#*LA&Spdir_CMG+A9XaICf78J+CVg5UF)8`YFmKUTAqSvS(aeSVg zREBd-fdVzbHvrv%d4&7(8(`Ll-d4C;;xa(crm+Jtl06Rm2D@UO71@SyeWZjv zx<#P%SiQkYvD~t-n)D=~V<_hwzpv#n4t~EW-945w zL4yWQ%(8(v9ml^n>N}j%*gNm5@6|q|)#$OVfMJJz1)o!oxgZsu{F)+d*ux=jQ$*wWfq6n+!=3`%@B5-+kLa|H0=0&&rb>Cjm3G0RIg8#MldszaJ;Z!c4fYw8hj5h(>gcN_U!=WsUtj z@IE^0F6!oB=V;v0`;5%Ox?%NUjG0nA)8~gv7w;{_r=K%i7@)zK1x8Xr9B>{Fv9Afp z2#88|BOZ{FsiR$bqhbt8NUDk{fPmmg_MQ7QX5LlB5b+095mafs6UtI=(ny5K%pg@C zKxtu<+~XiwG0J6{1>fq4Mmw&1?A>xd={suqTh`nA>H&ZND|*+6e<2yNaKmr>JwsuS zCm`7`OPll84V{4ibdAMz*{a44sjkbS2Mcti=sZx&ep@ab$$(Yy(x!_HRs_nb%}z3R z`zGF=YwnroqG6N$-`so3imAE!bZPrrNFG(T5x{I!we8RI{QF?^%)O&4RzVcSOgfJ2 zo7sBzck_sg11_Ukem)q+%7;aX_oe{>y@mpBiPHk9Ws(xcaF-JMxd(wq6Y$nfES?US z6eMPVmn_eo2>{2A>wtdG7G=3%4;aqT%GFJGYpndoX#k1{xR66pKoAF}8@=C&QS-uO$L>%adY0xBZ{Wbl%Ph%pT2iDJBK(N)rk(rFO&=_cWp_4O0L9opx^w!-*t z+duO8q;lJ4;@0o|uUYUPfxw4@RblCgT;H|4z ziz#>aj155HlY$x^XDO7g-?klATwd5Nv2Q4(g`}0qZ@KT71Aeo5w^smnIqD!)R)g@TM7z?lW%}LHi{k{@>(~5>$w|o0#H?!T zMtNsX^Z%`^n|$Uph!VEJootDg)fKL)HRe5*(db(QYbo5+%1s6dpL6Te1ohgDIY6e2 zz>Spz6n*@PdF=b@amxyQq3f{}VZnwkrs#s3M(9}L4T5Xv8e(e~Hn zmc${rsm~fhpNHxCR(smVBlhPq|1Q^9y^VY#P6dGgD5mmnVW=&TV-0`n{KKXvTNMXS z20IWABj z{zI#EBLIkoLf^0L6uA0D(3nS}I4BLF_X2Mrw=0ao$0;R!>kpXv05RBDM#Y0AO^$?8 zBm307elkJR`vT5sGji{zybEruS4;3lL1K_rE71{c{KSsGcCOOuUB0$kbd>RG`JUVt`vdlIntAJ{98}8-{2shuWFR;8Ke})jw8KcWTI$wlx_Z nmc}?k|KCfV18I5W^m)gDf5X>})n!*8s1LfUt*2Fuu!;CTw{PXi diff --git a/apps/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/apps/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 02dacb90e9749ecfef07aaffae836658ce10b7ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6029 zcmb7IS5y;9v`s=1Lg+noLNGw21f>@Vp$F+*KtfSKL8>%qp-S&f;fhF?rf?MmG)Ped z5kv)~g(g^#&_jRR_w&Brnl&?P*4pcQ%$#%fo}FTDYQPNVhXVirW+Ovg%kw?^zX^k$ zudDit?EnC;wUMs2br^8Hhkb1~T|V6foV-6+1^kB| z9t)C1#qBj~VG#(J6N3kc<@g4$4FINPFHV;ykHvRcWcB2tm}6R__D6H#eYXh@slK)m9c zUcO!e7zRR#-ad?2UVDN7(n~#;@6 zhXIXmzlVxbRii5wUE}~}L~j!m$)BhgUB0N*3}kinW=sOglMQIasWDy9V=iOITu2{4 zWI+e>dLKk(D|{iz2&1=MLSbGj(uaYJ9hI1}$*Z*3)Qw@lb#kB-f`N-pHEb}+&x6iy zR6ijBJg%jgqJ3h?7pccJs~%8TDgV;I+}}Dc{D`1?y~nj9;2qGyAv|fQPXNc>A}9yz zDuOo9S9t5yX>zVqj{S5S&gLqaa^^M_Nb1mt`*_bu`oQPqG0`SdQbyfG-44n`7vi2v zzIO&l8io5PJuLN)Ds10yIlSCbBZQ=ue2=hyQI|V$w#C=cxi_Ba3rKRyC97|fj$l|M zIB$UY#-1kK+E=~)M;Z^7pU8X-{M%LVXEw%u^ok7NCNVBGd><6V(2~ClKA1N%<7T~T zTJ7E*_eKhz{eZ%z)m4|(k{oh#d?f1g(inJs2v-i9OwtAgS(1csY`~o*k#yI;-9Hnc z4A+MCPQGrrtTqw-TytjJkchki#X%vgrq`pJd)z;@#)nIh>R`?W5o-o%!4uKGa zD0US^)BrB}Pp%lZ`f}$QrEO_KwEmh`Lv9jjMxAWZ1rbRcy;GATytb{i{t01sm$>B4VO{(5Jso zFPEv?C!^lX54y}b_9}DfEgZ7j$@>Pz=vO8$wz(4Y6dS%@>#1N>qGpvgdqY@(&E
  • 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 U4>zF1-wY(`$~sCF3btYY2dYchDF6Tf diff --git a/apps/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/apps/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp deleted file mode 100644 index 3e49a831a633615cbaedf1893e5b501c08a44ef7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17384 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 MnHsVZVIJ~-00KCZE&u=k diff --git a/apps/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/apps/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 23d90e57d4fcaf76216eb07807376c93ceeb55c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8155 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 S3PG(00F3p`_3CxtvHu6ADIh}t diff --git a/apps/mobile-app/android/app/src/main/res/values-night/colors.xml b/apps/mobile-app/android/app/src/main/res/values-night/colors.xml index c52cdc713..86d8666a4 100644 --- a/apps/mobile-app/android/app/src/main/res/values-night/colors.xml +++ b/apps/mobile-app/android/app/src/main/res/values-night/colors.xml @@ -1,8 +1,5 @@ - #202020 - - #ECEDEE #9BA1A6 #000000 @@ -14,7 +11,5 @@ #eabf69 #9BA1A6 #ef4444 - - #CC000000 - + \ No newline at end of file diff --git a/apps/mobile-app/android/app/src/main/res/values-night/styles.xml b/apps/mobile-app/android/app/src/main/res/values-night/styles.xml index 4b5e161dd..f7b01f604 100644 --- a/apps/mobile-app/android/app/src/main/res/values-night/styles.xml +++ b/apps/mobile-app/android/app/src/main/res/values-night/styles.xml @@ -23,4 +23,13 @@ false false + + + diff --git a/apps/mobile-app/android/app/src/main/res/values/colors.xml b/apps/mobile-app/android/app/src/main/res/values/colors.xml index 2dad46ed3..fca018d0a 100644 --- a/apps/mobile-app/android/app/src/main/res/values/colors.xml +++ b/apps/mobile-app/android/app/src/main/res/values/colors.xml @@ -1,11 +1,7 @@ #ffffff #FFFFFF - - #023c69 - - #11181C #4b5563 #f3f4f6 @@ -16,8 +12,6 @@ #eabf69 #687076 #D32F2F - - #11181C #4b5563 #f3f4f6 @@ -29,10 +23,8 @@ #eabf69 #687076 #D32F2F - - #CCFFFFFF - - @color/av_primary - + #000000 + #ffffff + \ No newline at end of file diff --git a/apps/mobile-app/android/app/src/main/res/values/strings.xml b/apps/mobile-app/android/app/src/main/res/values/strings.xml index 054a47ab9..08d409ca2 100644 --- a/apps/mobile-app/android/app/src/main/res/values/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values/strings.xml @@ -5,27 +5,19 @@ false AliasVault AutoFill AliasVault icon - - Close Next Cancel Back An unknown error occurred - - Failed to retrieve, open app No match found, create new? Open app Vault locked - - Store Encryption Key Authenticate to securely store your encryption key in the Android Keystore. This enables secure access to your vault. Unlock Vault Authenticate to access your vault - - Create Passkey Create New Passkey Register a new passkey for this website. It will be securely stored in your vault and automatically synced across your devices with AliasVault. @@ -56,8 +48,6 @@ Retrieving passkey… Verifying… Authenticating… - - Connection Error No connection to the server can be made. Please check your internet connection and try creating the passkey again. Session Expired @@ -72,28 +62,20 @@ A network error occurred. Please check your connection and try again. Server Update Required The server version is outdated. Please contact your administrator to update the server. - - Please enable biometric or PIN authentication in the main AliasVault app in order to continue Please unlock vault in AliasVault app first Failed to decrypt vault Biometric authentication cancelled Failed to retrieve encryption key - - Unlock Vault Enter your PIN to unlock your vault PIN locked after too many failed attempts Incorrect PIN. %d attempts remaining - - Setup PIN Choose a PIN to unlock your vault Confirm PIN Re-enter your PIN to confirm PINs do not match. Please try again. - - Unlock Vault Enter your master password Password diff --git a/apps/mobile-app/android/app/src/main/res/values/styles.xml b/apps/mobile-app/android/app/src/main/res/values/styles.xml index a3d383afd..36fdc5cce 100644 --- a/apps/mobile-app/android/app/src/main/res/values/styles.xml +++ b/apps/mobile-app/android/app/src/main/res/values/styles.xml @@ -1,25 +1,15 @@ - - + \ No newline at end of file diff --git a/apps/mobile-app/android/gradle.properties b/apps/mobile-app/android/gradle.properties index 889455a91..f6abd7fa0 100644 --- a/apps/mobile-app/android/gradle.properties +++ b/apps/mobile-app/android/gradle.properties @@ -60,3 +60,6 @@ expo.useLegacyPackaging=false # Workaround for Expo modules compatibility with Android Gradle Plugin 8.x android.nonTransitiveRClass=false + +# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin +expo.edgeToEdgeEnabled=true \ No newline at end of file diff --git a/apps/mobile-app/app.json b/apps/mobile-app/app.json index 2eb685cd0..ea6da0575 100644 --- a/apps/mobile-app/app.json +++ b/apps/mobile-app/app.json @@ -28,7 +28,17 @@ "foregroundImage": "./assets/images/adaptive-icon.png", "backgroundColor": "#000000" }, - "package": "net.aliasvault.app" + "package": "net.aliasvault.app", + "splash": { + "image": "./assets/images/adaptive-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff", + "dark": { + "image": "./assets/images/adaptive-icon.png", + "resizeMode": "contain", + "backgroundColor": "#202020" + } + } }, "web": { "bundler": "metro", @@ -37,15 +47,6 @@ }, "plugins": [ "expo-router", - [ - "expo-splash-screen", - { - "image": "./assets/images/adaptive-icon.png", - "imageWidth": 200, - "resizeMode": "contain", - "backgroundColor": "#ffffff" - } - ], [ "react-native-edge-to-edge", { From eb1f4e492bc1002e0d81cefe04e74c147e724335 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 7 Mar 2026 16:24:40 +0100 Subject: [PATCH 19/26] Update FormDetector.ts (#1821) --- .../src/utils/formDetector/FormDetector.ts | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/apps/browser-extension/src/utils/formDetector/FormDetector.ts b/apps/browser-extension/src/utils/formDetector/FormDetector.ts index 408cc6c99..91e69cf7b 100644 --- a/apps/browser-extension/src/utils/formDetector/FormDetector.ts +++ b/apps/browser-extension/src/utils/formDetector/FormDetector.ts @@ -53,8 +53,14 @@ export class FormDetector { let formWrapper = this.getFormWrapper(); if (formWrapper?.getAttribute('role') === 'dialog') { - // If we hit a dialog, search for form only within the dialog - formWrapper = formWrapper.querySelector('form') as HTMLElement | null ?? formWrapper; + /* + * If we hit a dialog, try to find a more specific container within it. + * Try in order:
    , custom form elements (like faceplate-form), or keep the dialog. + */ + const standardForm = formWrapper.querySelector('form') as HTMLElement | null; + const customFormElement = formWrapper.querySelector('[id*="login"], [id*="register"], [class*="auth"], [class*="login"], [class*="register"]') as HTMLElement | null; + + formWrapper = standardForm ?? customFormElement ?? formWrapper; } if (!formWrapper) { @@ -400,25 +406,28 @@ export class FormDetector { /* * Check if element has zero dimensions using actual rendered size. - * Skip this check for positioned elements (fixed/absolute) since they - * can have 0x0 dimensions but still contain visible children. + * Only check this for input elements themselves, not their parent containers. + * Container elements (divs, fieldsets, etc.) may have zero dimensions but contain visible children. + * This check is primarily to catch fake/honeypot input fields. */ - const height = parseFloat(style.height); - const width = parseFloat(style.width); - const maxHeight = parseFloat(style.maxHeight); - const maxWidth = parseFloat(style.maxWidth); - const position = style.position; + const isInputElement = current.tagName.toLowerCase() === 'input' || + current.tagName.toLowerCase() === 'textarea' || + current.tagName.toLowerCase() === 'select'; - // Check if element has zero dimensions - if (height === 0 || width === 0 || maxHeight === 0 || maxWidth === 0) { - if (position !== 'fixed' && position !== 'absolute') { - const rect = current.getBoundingClientRect(); - if (rect.width === 0 && rect.height === 0) { - if (checkOpacity) { - this.visibilityCache.set(current, false); - } - return false; + if (isInputElement) { + const rect = current.getBoundingClientRect(); + const height = parseFloat(style.height); + const width = parseFloat(style.width); + const maxHeight = parseFloat(style.maxHeight); + const maxWidth = parseFloat(style.maxWidth); + + // Only reject if both bounding rect is 0x0 AND has explicit zero-sizing styles + if (rect.width === 0 && rect.height === 0 && + (height === 0 || width === 0 || maxHeight === 0 || maxWidth === 0)) { + if (checkOpacity) { + this.visibilityCache.set(current, false); } + return false; } } From 049cb41319fb76f3f876438ed154d85f322c3a03 Mon Sep 17 00:00:00 2001 From: Leendert de Borst <6917405+lanedirt@users.noreply.github.com> Date: Sat, 7 Mar 2026 00:05:19 +0100 Subject: [PATCH 20/26] New translations en.json (Danish) Update translations from Crowdin [ci skip] --- apps/mobile-app/i18n/locales/da.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile-app/i18n/locales/da.json b/apps/mobile-app/i18n/locales/da.json index f66730d3b..5bce53f0f 100644 --- a/apps/mobile-app/i18n/locales/da.json +++ b/apps/mobile-app/i18n/locales/da.json @@ -475,7 +475,7 @@ "usernameCopied": "Brugernavn kopieret til udklipsholder", "emailCopied": "E-mail kopieret til udklipsholder", "passwordCopied": "Adgangskode kopieret til udklipsholder", - "totpCodeCopied": "TOTP code copied to clipboard", + "totpCodeCopied": "TOTP kode kopieret til udklipsholder", "urlCopied": "URL kopieret til udklipsholder" }, "createNewAliasFor": "Opret nyt alias for", @@ -486,7 +486,7 @@ "copyUsername": "Kopier brugernavn", "copyEmail": "Kopier e-mail", "copyPassword": "Kopier adgangskode", - "copyTotpCode": "Copy TOTP Code" + "copyTotpCode": "Kopier TOTP Kode" }, "urlContextMenu": { "title": "Url Indstillinger", From 65f5655f39b2e272197fdaf6b8b6f1ef92dffde8 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 7 Mar 2026 17:31:56 +0100 Subject: [PATCH 21/26] Add release notes for 0.27.2 --- fastlane/metadata/android/en-US/changelogs/2702900.txt | 1 + fastlane/metadata/android/nl-NL/changelogs/2702900.txt | 1 + fastlane/metadata/browser-extension/en-US/changelogs/0.27.2.txt | 1 + fastlane/metadata/ios/en-US/changelogs/2702900.txt | 1 + fastlane/metadata/ios/nl-NL/changelogs/2702900.txt | 1 + 5 files changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/2702900.txt create mode 100644 fastlane/metadata/android/nl-NL/changelogs/2702900.txt create mode 100644 fastlane/metadata/browser-extension/en-US/changelogs/0.27.2.txt create mode 100644 fastlane/metadata/ios/en-US/changelogs/2702900.txt create mode 100644 fastlane/metadata/ios/nl-NL/changelogs/2702900.txt diff --git a/fastlane/metadata/android/en-US/changelogs/2702900.txt b/fastlane/metadata/android/en-US/changelogs/2702900.txt new file mode 100644 index 000000000..4540b9a3f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/2702900.txt @@ -0,0 +1 @@ +- UI improvements \ No newline at end of file diff --git a/fastlane/metadata/android/nl-NL/changelogs/2702900.txt b/fastlane/metadata/android/nl-NL/changelogs/2702900.txt new file mode 100644 index 000000000..6a3f87fe3 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/2702900.txt @@ -0,0 +1 @@ +- UI verbeteringen \ No newline at end of file diff --git a/fastlane/metadata/browser-extension/en-US/changelogs/0.27.2.txt b/fastlane/metadata/browser-extension/en-US/changelogs/0.27.2.txt new file mode 100644 index 000000000..2ef6cde18 --- /dev/null +++ b/fastlane/metadata/browser-extension/en-US/changelogs/0.27.2.txt @@ -0,0 +1 @@ +- Autofill improvements \ No newline at end of file diff --git a/fastlane/metadata/ios/en-US/changelogs/2702900.txt b/fastlane/metadata/ios/en-US/changelogs/2702900.txt new file mode 100644 index 000000000..4540b9a3f --- /dev/null +++ b/fastlane/metadata/ios/en-US/changelogs/2702900.txt @@ -0,0 +1 @@ +- UI improvements \ No newline at end of file diff --git a/fastlane/metadata/ios/nl-NL/changelogs/2702900.txt b/fastlane/metadata/ios/nl-NL/changelogs/2702900.txt new file mode 100644 index 000000000..6a3f87fe3 --- /dev/null +++ b/fastlane/metadata/ios/nl-NL/changelogs/2702900.txt @@ -0,0 +1 @@ +- UI verbeteringen \ No newline at end of file From 8fc3708173e09b55546cfe54b364aa5723849e1f Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sun, 8 Mar 2026 12:26:37 +0100 Subject: [PATCH 22/26] Fix password unlock error disappearing too quickly on Android (#1824) --- .../passwordunlock/PasswordUnlockActivity.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/passwordunlock/PasswordUnlockActivity.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/passwordunlock/PasswordUnlockActivity.kt index 687b4e358..77cc063e7 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/passwordunlock/PasswordUnlockActivity.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/passwordunlock/PasswordUnlockActivity.kt @@ -148,8 +148,8 @@ class PasswordUnlockActivity : AppCompatActivity() { // Not used } override fun afterTextChanged(s: Editable?) { - // Only hide error if user is typing (not when we programmatically clear for error display) - if (!isShowingError) { + // Hide error when user starts typing a new password + if (!isShowingError && !s.isNullOrEmpty()) { hideError() } unlockButton.isEnabled = !s.isNullOrEmpty() && !isProcessing @@ -278,9 +278,11 @@ class PasswordUnlockActivity : AppCompatActivity() { } private fun showError(message: String) { - errorTextView.text = message + errorContainer.animate().cancel() isShowingError = true + errorTextView.text = message + // Animate error in with slide from top errorContainer.visibility = View.VISIBLE errorContainer.alpha = 0f @@ -290,6 +292,7 @@ class PasswordUnlockActivity : AppCompatActivity() { .translationY(0f) .setDuration(300) .setInterpolator(AccelerateDecelerateInterpolator()) + .setListener(null) // Clear any previous listeners .start() // Shake password field to indicate error @@ -298,18 +301,15 @@ class PasswordUnlockActivity : AppCompatActivity() { shake.start() passwordEditText.text?.clear() - - // Reset flag after clearing is done - passwordEditText.post { + passwordEditText.postDelayed({ isShowingError = false - } + }, 100) passwordEditText.requestFocus() } private fun hideError() { - if (errorContainer.visibility == View.VISIBLE) { - isShowingError = false + if (errorContainer.visibility == View.VISIBLE && !isShowingError) { errorContainer.animate() .alpha(0f) .translationY(-20f) From 4792c71c25f0a514907b795f8302ae97c2530791 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sun, 8 Mar 2026 13:22:32 +0100 Subject: [PATCH 23/26] Add local password unlock rate limit to iOS and Android apps (#1824) --- .../java/net/aliasvault/app/MainActivity.kt | 5 ++ .../nativevaultmanager/NativeVaultManager.kt | 5 ++ .../passwordunlock/PasswordUnlockActivity.kt | 84 ++++++++++++++++++- .../app/src/main/res/values/strings.xml | 2 + apps/mobile-app/app/unlock.tsx | 14 ++++ .../ios/NativeVaultManager/VaultManager.swift | 17 ++++ .../Auth/PasswordUnlockViewModel.swift | 71 +++++++++++++++- .../ios/VaultUI/en.lproj/Localizable.strings | 2 + 8 files changed, 193 insertions(+), 7 deletions(-) diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt index 78a0af9fa..cd199b510 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt @@ -291,6 +291,11 @@ class MainActivity : ReactActivity() { // For authenticateUser(), resolve with false authPromise?.resolve(false) } + net.aliasvault.app.passwordunlock.PasswordUnlockActivity.RESULT_MAX_ATTEMPTS_REACHED -> { + // Max attempts reached - vault has been cleared, reject to trigger logout in React Native + passwordPromise?.reject("MAX_ATTEMPTS_REACHED", "Too many failed unlock attempts", null) + authPromise?.reject("MAX_ATTEMPTS_REACHED", "Too many failed unlock attempts", null) + } else -> { // For showPasswordUnlock(), resolve with null passwordPromise?.resolve(null) diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt index f906df7ab..e9851ab2f 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt @@ -130,6 +130,11 @@ class NativeVaultManager(reactContext: ReactApplicationContext) : override fun clearSession(promise: Promise) { try { vaultStore.clearSession() + + // Reset password unlock failed attempts counter on logout + val sharedPreferences = reactApplicationContext.getSharedPreferences("aliasvault", android.content.Context.MODE_PRIVATE) + sharedPreferences.edit().remove("password_unlock_failed_attempts").apply() + promise.resolve(null) } catch (e: Exception) { Log.e(TAG, "Error clearing session", e) diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/passwordunlock/PasswordUnlockActivity.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/passwordunlock/PasswordUnlockActivity.kt index 77cc063e7..f53a2d44e 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/passwordunlock/PasswordUnlockActivity.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/passwordunlock/PasswordUnlockActivity.kt @@ -4,6 +4,7 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.app.Activity +import android.content.Context import android.content.Intent import android.os.Bundle import android.text.Editable @@ -19,6 +20,7 @@ import android.widget.ImageButton import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -45,6 +47,9 @@ class PasswordUnlockActivity : AppCompatActivity() { /** Result code for cancelled password unlock. */ const val RESULT_CANCELLED = Activity.RESULT_CANCELED + /** Result code for max attempts reached - user has been logged out. */ + const val RESULT_MAX_ATTEMPTS_REACHED = Activity.RESULT_FIRST_USER + 1 + /** Intent extra key for the encryption key (returned on success). */ const val EXTRA_ENCRYPTION_KEY = "encryption_key" @@ -56,6 +61,15 @@ class PasswordUnlockActivity : AppCompatActivity() { /** Intent extra key for custom button text (optional). */ const val EXTRA_CUSTOM_BUTTON_TEXT = "custom_button_text" + + /** Maximum number of failed password attempts before logout. */ + private const val MAX_FAILED_ATTEMPTS = 10 + + /** Warning threshold for failed attempts. */ + private const val WARNING_THRESHOLD = 5 + + /** SharedPreferences key for failed password attempts counter. */ + private const val PREF_FAILED_ATTEMPTS = "password_unlock_failed_attempts" } private lateinit var vaultStore: VaultStore @@ -74,6 +88,7 @@ class PasswordUnlockActivity : AppCompatActivity() { // State private var isProcessing: Boolean = false private var isShowingError: Boolean = false + private var failedAttempts: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -94,6 +109,10 @@ class PasswordUnlockActivity : AppCompatActivity() { AndroidStorageProvider(this), ) + // Load failed attempts counter + val sharedPreferences = getSharedPreferences("aliasvault", Context.MODE_PRIVATE) + failedAttempts = sharedPreferences.getInt(PREF_FAILED_ATTEMPTS, 0) + // Get custom title/subtitle/buttonText from intent val customTitle = intent.getStringExtra(EXTRA_CUSTOM_TITLE) val customSubtitle = intent.getStringExtra(EXTRA_CUSTOM_SUBTITLE) @@ -253,15 +272,16 @@ class PasswordUnlockActivity : AppCompatActivity() { } if (encryptionKey != null) { - // Success - return encryption key + // Success - reset failed attempts counter and return encryption key + resetFailedAttempts() val resultIntent = Intent().apply { putExtra(EXTRA_ENCRYPTION_KEY, encryptionKey) } setResult(RESULT_SUCCESS, resultIntent) finish() } else { - // Incorrect password - showError(getString(R.string.password_unlock_incorrect)) + // Incorrect password - increment failed attempts + handleFailedAttempt() } } catch (e: Exception) { // Error during verification @@ -324,6 +344,64 @@ class PasswordUnlockActivity : AppCompatActivity() { } } + private fun handleFailedAttempt() { + failedAttempts++ + saveFailedAttempts() + + val remainingAttempts = MAX_FAILED_ATTEMPTS - failedAttempts + + if (failedAttempts >= MAX_FAILED_ATTEMPTS) { + // Max attempts reached - logout user + logoutUser() + } else if (failedAttempts >= WARNING_THRESHOLD) { + // Show warning about remaining attempts + val warningMessage = getString(R.string.password_unlock_attempts_warning, remainingAttempts) + showError(warningMessage) + } else { + // Show standard incorrect password error + showError(getString(R.string.password_unlock_incorrect)) + } + } + + private fun saveFailedAttempts() { + val sharedPreferences = getSharedPreferences("aliasvault", Context.MODE_PRIVATE) + sharedPreferences.edit { + putInt(PREF_FAILED_ATTEMPTS, failedAttempts) + } + } + + private fun resetFailedAttempts() { + failedAttempts = 0 + val sharedPreferences = getSharedPreferences("aliasvault", Context.MODE_PRIVATE) + sharedPreferences.edit { + remove(PREF_FAILED_ATTEMPTS) + } + } + + private fun logoutUser() { + CoroutineScope(Dispatchers.Main).launch { + try { + // Clear vault and all session data + withContext(Dispatchers.IO) { + vaultStore.clearVault() + } + + // Show logout message and close activity + showError(getString(R.string.password_unlock_max_attempts_reached)) + + // Delay to let user read the message, then return max attempts result + passwordEditText.postDelayed({ + setResult(RESULT_MAX_ATTEMPTS_REACHED) + finish() + }, 2000) + } catch (e: Exception) { + android.util.Log.e("PasswordUnlockActivity", "Error during logout", e) + setResult(RESULT_MAX_ATTEMPTS_REACHED) + finish() + } + } + } + private fun applyWindowInsets() { findViewById(android.R.id.content).setOnApplyWindowInsetsListener { _, insets -> val systemBarsInsets = insets.systemWindowInsets diff --git a/apps/mobile-app/android/app/src/main/res/values/strings.xml b/apps/mobile-app/android/app/src/main/res/values/strings.xml index 08d409ca2..08025064f 100644 --- a/apps/mobile-app/android/app/src/main/res/values/strings.xml +++ b/apps/mobile-app/android/app/src/main/res/values/strings.xml @@ -82,4 +82,6 @@ Unlock Incorrect password. Please try again. Failed to verify password + Incorrect password. You will be logged out if you enter the wrong password %d more times. + Too many failed unlock attempts. You have been logged out for security reasons. \ No newline at end of file diff --git a/apps/mobile-app/app/unlock.tsx b/apps/mobile-app/app/unlock.tsx index 1ef8a7c0d..c06c90809 100644 --- a/apps/mobile-app/app/unlock.tsx +++ b/apps/mobile-app/app/unlock.tsx @@ -135,6 +135,13 @@ export default function UnlockScreen() : React.ReactNode { return; } + // Check if max attempts reached + if (err && typeof err === 'object' && 'code' in err && err.code === 'MAX_ATTEMPTS_REACHED') { + // Max attempts reached - vault has been cleared, force logout + await logoutForced(); + return; + } + console.error('Unlock error:', err); const errorCode = getAppErrorCode(err); @@ -216,6 +223,13 @@ export default function UnlockScreen() : React.ReactNode { return; } + // Check if max attempts reached + if (err && typeof err === 'object' && 'code' in err && err.code === 'MAX_ATTEMPTS_REACHED') { + // Max attempts reached - vault has been cleared, force logout + await logoutForced(); + return; + } + // Try to extract error code from the error const errorCode = getAppErrorCode(err); diff --git a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift index aa9f79bf5..f4bdfbb25 100644 --- a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift +++ b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift @@ -199,6 +199,9 @@ public class VaultManager: NSObject { @objc func clearSession() { vaultStore.clearSession() + + // Reset password unlock failed attempts counter on logout + UserDefaults.standard.removeObject(forKey: "password_unlock_failed_attempts") } /// Clear all vault data including from persisted storage. @@ -876,6 +879,20 @@ public class VaultManager: NSObject { rootVC.dismiss(animated: true) { resolve(nil) } + }, + logoutHandler: { [weak self] in + // Clear vault on max failed attempts + try? self?.vaultStore.clearVault() + + // Throw error to signal max attempts reached to React Native + await MainActor.run { + rootVC.dismiss(animated: true) { + reject("MAX_ATTEMPTS_REACHED", "Too many failed unlock attempts", nil) + } + } + + // Throw to stop further processing in ViewModel + throw NSError(domain: "VaultManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Max attempts reached"]) } ) diff --git a/apps/mobile-app/ios/VaultUI/Auth/PasswordUnlockViewModel.swift b/apps/mobile-app/ios/VaultUI/Auth/PasswordUnlockViewModel.swift index df05663b4..a3fcefa66 100644 --- a/apps/mobile-app/ios/VaultUI/Auth/PasswordUnlockViewModel.swift +++ b/apps/mobile-app/ios/VaultUI/Auth/PasswordUnlockViewModel.swift @@ -16,19 +16,33 @@ public class PasswordUnlockViewModel: ObservableObject { private let unlockHandler: (String) async throws -> Void private let cancelHandler: () -> Void + private let logoutHandler: (() async throws -> Void)? + + // Brute force protection + private static let maxFailedAttempts = 10 + private static let warningThreshold = 5 + private static let failedAttemptsKey = "password_unlock_failed_attempts" + + @Published private var failedAttempts: Int = 0 + private var isMaxAttemptsReached = false public init( customTitle: String?, customSubtitle: String?, customButtonText: String?, unlockHandler: @escaping (String) async throws -> Void, - cancelHandler: @escaping () -> Void + cancelHandler: @escaping () -> Void, + logoutHandler: (() async throws -> Void)? = nil ) { self.customTitle = customTitle self.customSubtitle = customSubtitle self.customButtonText = customButtonText self.unlockHandler = unlockHandler self.cancelHandler = cancelHandler + self.logoutHandler = logoutHandler + + // Load failed attempts from UserDefaults + self.failedAttempts = UserDefaults.standard.integer(forKey: Self.failedAttemptsKey) } public func unlock() async { @@ -40,15 +54,64 @@ public class PasswordUnlockViewModel: ObservableObject { do { try await unlockHandler(password) + // Success - reset failed attempts + resetFailedAttempts() } catch { - // Show error and clear password + // Handle failed attempt + await handleFailedAttempt() + } + } + + public func cancel() { + // If max attempts was reached, this will be handled in logoutUser + if !isMaxAttemptsReached { + cancelHandler() + } + } + + private func handleFailedAttempt() async { + failedAttempts += 1 + saveFailedAttempts() + + let remainingAttempts = Self.maxFailedAttempts - failedAttempts + + if failedAttempts >= Self.maxFailedAttempts { + // Max attempts reached - logout user + isMaxAttemptsReached = true + self.error = String(localized: "max_attempts_reached", bundle: locBundle) + await logoutUser() + } else if failedAttempts >= Self.warningThreshold { + // Show warning about remaining attempts + let format = String(localized: "attempts_warning", bundle: locBundle) + self.error = String(format: format, remainingAttempts) + self.password = "" + self.isProcessing = false + } else { + // Show standard incorrect password error self.error = String(localized: "incorrect_password", bundle: locBundle) self.password = "" self.isProcessing = false } } - public func cancel() { - cancelHandler() + private func saveFailedAttempts() { + UserDefaults.standard.set(failedAttempts, forKey: Self.failedAttemptsKey) + } + + private func resetFailedAttempts() { + failedAttempts = 0 + UserDefaults.standard.removeObject(forKey: Self.failedAttemptsKey) + } + + private func logoutUser() async { + // Delay to let user read the message + try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds + + // Call logout handler if provided - this will throw MAX_ATTEMPTS_REACHED error + do { + try await logoutHandler?() + } catch { + // Error from logout handler (will be caught by calling code) + } } } diff --git a/apps/mobile-app/ios/VaultUI/en.lproj/Localizable.strings b/apps/mobile-app/ios/VaultUI/en.lproj/Localizable.strings index 559605953..b5529bcb7 100644 --- a/apps/mobile-app/ios/VaultUI/en.lproj/Localizable.strings +++ b/apps/mobile-app/ios/VaultUI/en.lproj/Localizable.strings @@ -72,6 +72,8 @@ "enter_password_to_unlock" = "Enter your master password"; "unlock" = "Unlock"; "incorrect_password" = "Incorrect password. Please try again."; +"attempts_warning" = "Incorrect password. You will be logged out if you enter the wrong password %d more times."; +"max_attempts_reached" = "Too many failed unlock attempts. You have been logged out for security reasons."; /* PIN Setup */ "pin_setup_title" = "Setup PIN"; From 8cf06a597005f13f55f10c6dac6f9d329620b0a9 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sun, 8 Mar 2026 13:44:16 +0100 Subject: [PATCH 24/26] Add local password unlock rate limit to browser extension (#1824) --- .../background/VaultMessageHandler.ts | 3 ++ .../entrypoints/popup/pages/auth/Unlock.tsx | 52 +++++++++++++++++-- .../src/i18n/locales/en.json | 4 +- .../src/utils/LocalPreferencesService.ts | 38 ++++++++++++++ 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts index 9a8ce106d..e4f50f6bd 100644 --- a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts +++ b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts @@ -293,6 +293,9 @@ export async function handleClearSession(): Promise { 'session:navigationHistory', ]); + // Reset password unlock failed attempts counter on logout + await LocalPreferencesService.resetPasswordUnlockFailedAttempts(); + // Clear cached client since session ended cachedSqliteClient = null; cachedVaultBlob = null; diff --git a/apps/browser-extension/src/entrypoints/popup/pages/auth/Unlock.tsx b/apps/browser-extension/src/entrypoints/popup/pages/auth/Unlock.tsx index ba96abc5e..3d6810a8b 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/auth/Unlock.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/auth/Unlock.tsx @@ -61,6 +61,11 @@ const Unlock: React.FC = () => { // Password unlock state const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); + const [passwordFailedAttempts, setPasswordFailedAttempts] = useState(0); + + // Brute force protection constants + const MAX_PASSWORD_ATTEMPTS = 10; + const PASSWORD_WARNING_THRESHOLD = 5; // PIN unlock state const [pin, setPin] = useState(''); @@ -138,6 +143,10 @@ const Unlock: React.FC = () => { setUnlockMode('password'); } + // Load password failed attempts counter + const storedAttempts = await LocalPreferencesService.getPasswordUnlockFailedAttempts(); + setPasswordFailedAttempts(storedAttempts); + // Then check API status await checkStatus(); }; @@ -294,8 +303,10 @@ const Unlock: React.FC = () => { // Clear dismiss until await LocalPreferencesService.setVaultLockedDismissUntil(0); - // Reset PIN failed attempts on successful password unlock + // Reset PIN and password failed attempts on successful unlock await resetFailedAttempts(); + await LocalPreferencesService.resetPasswordUnlockFailedAttempts(); + setPasswordFailedAttempts(0); // Navigate to reinitialize which will call syncVault to sync with server navigate('/reinitialize', { replace: true }); @@ -307,13 +318,13 @@ const Unlock: React.FC = () => { // Check if it's a decryption failure (E-203): this means wrong password const errorCode = extractErrorCode(getErrorMessage(err, '')); if (errorCode === AppErrorCode.VAULT_DECRYPT_FAILED) { - setError(t('common.errors.wrongPassword')); + await handlePasswordFailedAttempt(); } else { // Other error codes, show the formatted message as-is setError(getErrorMessage(err, t('common.errors.wrongPassword'))); } } else { - setError(t('common.errors.wrongPassword')); + await handlePasswordFailedAttempt(); } console.error('Unlock error:', err); } finally { @@ -445,6 +456,35 @@ const Unlock: React.FC = () => { } }; + /** + * Handle failed password attempt with brute force protection + */ + const handlePasswordFailedAttempt = async (): Promise => { + const newAttempts = passwordFailedAttempts + 1; + setPasswordFailedAttempts(newAttempts); + await LocalPreferencesService.setPasswordUnlockFailedAttempts(newAttempts); + + const remainingAttempts = MAX_PASSWORD_ATTEMPTS - newAttempts; + + // Clear password field + setPassword(''); + + if (newAttempts >= MAX_PASSWORD_ATTEMPTS) { + // Max attempts reached - logout user + setError(t('auth.maxAttemptsReached')); + // Delay to let user read the message + setTimeout(async () => { + await authContext.clearAuthUserInitiated(); + }, 2000); + } else if (newAttempts >= PASSWORD_WARNING_THRESHOLD) { + // Show warning about remaining attempts + setError(t('auth.passwordAttemptsWarning', { remainingAttempts })); + } else { + // Show standard incorrect password error + setError(t('common.errors.wrongPassword')); + } + }; + /** * Handle logout click - opens the logout confirmation modal. */ @@ -514,8 +554,10 @@ const Unlock: React.FC = () => { // Clear dismiss until await LocalPreferencesService.setVaultLockedDismissUntil(0); - // Reset PIN failed attempts on successful unlock + // Reset PIN and password failed attempts on successful unlock await resetFailedAttempts(); + await LocalPreferencesService.resetPasswordUnlockFailedAttempts(); + setPasswordFailedAttempts(0); // Navigate to reinitialize which will call syncVault to sync with server navigate('/reinitialize', { replace: true }); @@ -671,7 +713,7 @@ const Unlock: React.FC = () => { {/* Error Message */} - {error && } + {error && }