Merge branch 'main' into 773-feature-request-add-encrypted-aliasvault-vault-export-and-import

* main: (26 commits)
  Prevent keyboard shortcuts from firing when interacting with autofill popup (#1832)
  Auto-disable email alias when item is deleted in browser extension (#1830)
  Add local password unlock rate limit to browser extension (#1824)
  Add local password unlock rate limit to iOS and Android apps (#1824)
  Fix password unlock error disappearing too quickly on Android (#1824)
  Add release notes for 0.27.2
  New translations en.json (Danish) Update translations from Crowdin [ci skip]
  Update FormDetector.ts (#1821)
  Update Android boot splash icon to be transparent (#1819)
  Tweak settings UI so both label and value will wrap text if necessary (#1819)
  Tweak unlock screen button sizing to work with longer translations (#1819)
  Update FormDetector.ts
  Update print-latest-changelogs.sh
  Bump version to 0.28.0-alpha
  Add release notes for 0.27.1
  New Crowdin updates (#1781)
  Update DatabaseMessageStore.cs
  Bump the nuget group with 1 update
  Update linting (#1812)
  Update confirm dialog to show text in center when it wraps due to translations (#1812)
  ...
This commit is contained in:
Leendert de Borst
2026-03-10 13:37:05 +01:00
184 changed files with 1676 additions and 678 deletions

View File

@@ -1 +1 @@
27
28

View File

@@ -1 +1 @@
0.27.0-alpha
0.28.0-alpha

View File

@@ -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"
}

View File

@@ -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",
@@ -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",

View File

@@ -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,

View File

@@ -293,6 +293,9 @@ export async function handleClearSession(): Promise<messageBoolResponse> {
'session:navigationHistory',
]);
// Reset password unlock failed attempts counter on logout
await LocalPreferencesService.resetPasswordUnlockFailedAttempts();
// Clear cached client since session ended
cachedSqliteClient = null;
cachedVaultBlob = null;

View File

@@ -208,6 +208,21 @@ async function checkAndRestoreSavePromptEarly(ctx: Parameters<typeof createShado
* Mount handler for early save prompt restore.
*/
onMount(container) {
/**
* Stop keyboard event propagation to prevent host page shortcuts from triggering
* when typing in save prompt input fields.
*/
const handleKeyboardEvent = (e: KeyboardEvent): void => {
const target = e.target as HTMLElement;
if (target && container.contains(target)) {
e.stopPropagation();
}
};
container.addEventListener('keydown', handleKeyboardEvent, true);
container.addEventListener('keyup', handleKeyboardEvent, true);
container.addEventListener('keypress', handleKeyboardEvent, true);
// Restore the appropriate prompt type based on persisted state
if (persistedState.promptType === 'add-url') {
void restoreAddUrlPromptFromState(
@@ -434,6 +449,23 @@ export default defineContentScript({
* Handle mount.
*/
onMount(container) {
/**
* Stop keyboard event propagation to prevent host page shortcuts from triggering
* when typing in extension popups (e.g., Discourse "u" shortcut for "go back").
*/
const handleKeyboardEvent = (e: KeyboardEvent): void => {
// Only stop propagation if the event originated from within our shadow DOM
const target = e.target as HTMLElement;
if (target && container.contains(target)) {
e.stopPropagation();
}
};
// Capture keyboard events at the container level to prevent bubbling to host page
container.addEventListener('keydown', handleKeyboardEvent, true);
container.addEventListener('keyup', handleKeyboardEvent, true);
container.addEventListener('keypress', handleKeyboardEvent, true);
/**
* Handle input field focus.
*/

View File

@@ -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<void> => {
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 = () => {
</div>
{/* Error Message */}
{error && <AlertMessage type="error" message={error} className="mb-4 text-center" />}
{error && <AlertMessage type="error" message={error} className="mb-4" />}
<div className="mb-4">
<label className="block text-gray-700 dark:text-gray-200 font-medium mb-2" htmlFor="password">

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -30,7 +30,9 @@
"switchAccounts": "Switch accounts?",
"loginWithMobile": "Log in using Mobile App",
"unlockWithMobile": "Unlock using Mobile App",
"scanQrCode": "Scan this QR code with your AliasVault mobile app to log in and unlock your vault."
"scanQrCode": "Scan this QR code with your AliasVault mobile app to log in and unlock your vault.",
"passwordAttemptsWarning": "Incorrect password. You will be logged out if you enter the wrong password {{remainingAttempts}} more times.",
"maxAttemptsReached": "Too many failed unlock attempts. You have been logged out for security reasons."
},
"menu": {
"vault": "Vault",

View File

@@ -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",

View File

@@ -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"

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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": "Неверный формат секретного ключа."
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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).

View File

@@ -39,6 +39,9 @@ const KEYS = {
// Session/Navigation state
PENDING_REDIRECT_URL: 'session:pendingRedirectUrl',
SKIP_FORM_RESTORE: 'local:aliasvault_skip_form_restore',
// Brute force protection
PASSWORD_UNLOCK_FAILED_ATTEMPTS: 'local:password_unlock_failed_attempts',
} as const;
/**
@@ -407,4 +410,39 @@ export const LocalPreferencesService = {
async setLoginSaveBlockedDomains(domains: string[]): Promise<void> {
await storage.setItem(KEYS.LOGIN_SAVE_BLOCKED_DOMAINS, domains);
},
/*
* ============================================
* Brute Force Protection
* ============================================
*/
/**
* Get the password unlock failed attempts count.
* @returns The number of failed password unlock attempts. Defaults to 0.
*/
async getPasswordUnlockFailedAttempts(): Promise<number> {
const value = await storage.getItem(KEYS.PASSWORD_UNLOCK_FAILED_ATTEMPTS) as number | string | null;
if (typeof value === 'number') {
return value;
}
if (typeof value === 'string') {
return parseInt(value, 10) || 0;
}
return 0;
},
/**
* Set the password unlock failed attempts count.
*/
async setPasswordUnlockFailedAttempts(attempts: number): Promise<void> {
await storage.setItem(KEYS.PASSWORD_UNLOCK_FAILED_ATTEMPTS, attempts);
},
/**
* Reset the password unlock failed attempts counter.
*/
async resetPasswordUnlockFailedAttempts(): Promise<void> {
await storage.removeItem(KEYS.PASSWORD_UNLOCK_FAILED_ATTEMPTS);
},
};

View File

@@ -226,7 +226,8 @@ export class ItemQueries {
AND fv.Value IS NOT NULL
AND fv.Value != ''
AND fv.IsDeleted = 0
AND i.IsDeleted = 0`;
AND i.IsDeleted = 0
AND i.DeletedAt IS NULL`;
}
/**

View File

@@ -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: <form>, 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) {
@@ -399,28 +405,30 @@ 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.
* 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 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) {
// Cache and return false for this element and all its parents
let parent: HTMLElement | null = current;
while (parent) {
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(parent, false);
this.visibilityCache.set(current, false);
}
parent = parent.parentElement;
return false;
}
return false;
}
/*

View File

@@ -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';"
},

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)
@@ -1346,6 +1351,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.

View File

@@ -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)
@@ -148,8 +167,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
@@ -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
@@ -278,9 +298,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 +312,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 +321,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)
@@ -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<View>(android.R.id.content).setOnApplyWindowInsetsListener { _, insets ->
val systemBarsInsets = insets.systemWindowInsets

View File

@@ -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

View File

@@ -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),

View File

@@ -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
}
}
/**

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="240dp"
android:height="240dp"
android:viewportWidth="700"
android:viewportHeight="700">
<group
android:translateX="100"
android:translateY="100">
<path
android:fillColor="#EEC170"
android:pathData="M459.87,294.95c0.016205,5.4005 0.03241,10.801 -0.35022,16.873c-1.111,6.3392 -1.1941,12.173 -2.6351,17.649c-10.922,41.508 -36.731,69.481 -77.351,83.408c-7.2157,2.4739 -14.972,3.3702 -22.479,4.995c-23.629,0.042205 -47.257,0.11453 -70.886,0.12027c-46.762,0.011322 -93.523,-0.01416 -140.95,-0.43411c-8.59,-2.0024 -16.766,-2.8352 -24.398,-5.3326c-21.595,-7.0666 -39.523,-19.656 -53.708,-37.552c-10.227,-12.903 -17.579,-27.17 -21.28,-43.221c-1.475,-6.3967 -2.4711,-12.904 -3.6852,-19.361c-0.051849,-5.747 -0.1037,-11.494 0.26915,-17.886c4.159,-42.973 27.68,-71.638 63.562,-92.153c0,-0.70761 -0.001961,-1.6988 3.12e-4,-2.69c0.022484,-9.8293 -1.3071,-19.894 0.35664,-29.438c3.2391,-18.579 11.08,-35.272 23.763,-49.773c12.098,-13.832 26.457,-23.989 43.609,-30.029c7.813,-2.7512 16.14,-4.0417 24.234,-5.9948c7.392,-0.025734 14.784,-0.05146 22.835,0.32253c4.1959,0.95392 7.7946,1.2538 11.258,2.1053c17.16,4.2192 32.287,12.176 45.469,24.104c2.2558,2.0411 4.372,6.6241 9.621,3.868c16.839,-8.8419 34.718,-11.597 53.603,-8.594c16.791,2.6699 31.602,9.4308 44.236,20.636c11.531,10.227 19.84,22.841 25.393,37.236c6.3436,16.445 10.389,33.163 6.0798,49.389c7.9587,8.9321 15.807,16.704 22.421,25.414c9.162,12.065 15.33,25.746 18.144,40.776c0.97046,5.1848 1.9111,10.375 2.8654,15.563m-71.597,71.012c5.5615,-5.2284 12.002,-9.7986 16.508,-15.817c10.474,-13.992 14.333,-29.916 11.288,-47.446c-2.2496,-12.95 -8.1973,-24.076 -17.243,-33.063c-12.746,-12.663 -28.865,-18.614 -46.786,-18.569c-69.912,0.17712 -139.82,0.56831 -209.74,0.96176c-15.922,0.089599 -29.168,7.4209 -39.685,18.296c-14.45,14.944 -20.408,33.343 -16.655,54.368c2.2763,12.754 8.2167,23.748 17.158,32.66c13.299,13.255 30.097,18.653 48.728,18.651c59.321,-0.005188 118.64,0.042358 177.96,-0.046601c9.5912,-0.014374 19.181,-0.86588 28.773,-0.88855c10.649,-0.025146 19.978,-3.825 29.687,-9.1074z"/>
<path
android:fillColor="#EEC170"
android:pathData="M162.77,293c15.654,4.3883 20.627,22.967 10.304,34.98c-5.3104,6.1795 -14.817,8.3208 -24.278,5.0472c-7.0723,-2.4471 -12.332,-10.362 -12.876,-17.933c-1.0451,-14.542 11.089,-23.176 21.705,-23.046c1.5794,0.019287 3.1517,0.61566 5.1461,0.95184z"/>
<path
android:fillColor="#EEC170"
android:pathData="M227.18,293.64c7.8499,2.3973 11.938,8.2143 13.524,15.077c1.8591,8.0439 -0.44817,15.706 -7.1588,21.121c-6.7633,5.4572 -14.417,6.8794 -22.578,3.1483c-8.2972,-3.7933 -12.836,-10.849 -12.736,-19.438c0.1687,-14.497 14.13,-25.368 28.948,-19.908z"/>
<path
android:fillColor="#EEC170"
android:pathData="M261.57,319.07c-2.495,-14.418 4.6853,-22.603 14.596,-26.108c9.8945,-3.4995 23.181,3.4303 26.267,13.779c4.6504,15.591 -7.1651,29.064 -21.665,28.161c-8.5254,-0.53088 -17.202,-6.5094 -19.198,-15.831z"/>
<path
android:fillColor="#EEC170"
android:pathData="M336.91,333.41c-9.0175,-4.2491 -15.337,-14.349 -13.829,-21.682c3.0825,-14.989 13.341,-20.304 23.018,-19.585c10.653,0.79141 17.93,7.407 19.765,17.547c1.9588,10.824 -4.1171,19.939 -13.494,23.703c-5.272,2.1162 -10.091,1.5086 -15.46,0.017883z"/>
</group>
</vector>

View File

@@ -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"

View File

@@ -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" />
<TextView

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -7,6 +7,7 @@
<string name="common_close">Затвори</string>
<string name="common_next">Следващ</string>
<string name="common_cancel">Отмени</string>
<string name="common_back">Back</string>
<string name="unknown_error">An unknown error occurred</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Confirm PIN</string>
<string name="pin_confirm_description">Re-enter your PIN to confirm</string>
<string name="pin_mismatch">PINs do not match. Please try again.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Unlock Vault</string>
<string name="password_unlock_subtitle">Enter your master password</string>
<string name="password_unlock_password_hint">Password</string>
<string name="password_unlock_button">Отключи</string>
<string name="password_unlock_incorrect">Incorrect password. Please try again.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Tanca</string>
<string name="common_next">Següent</string>
<string name="common_cancel">Cancel·la</string>
<string name="common_back">Enrere</string>
<string name="unknown_error">An unknown error occurred</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Confirm PIN</string>
<string name="pin_confirm_description">Re-enter your PIN to confirm</string>
<string name="pin_mismatch">PINs do not match. Please try again.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Unlock Vault</string>
<string name="password_unlock_subtitle">Enter your master password</string>
<string name="password_unlock_password_hint">Contrasenya</string>
<string name="password_unlock_button">Unlock</string>
<string name="password_unlock_incorrect">Incorrect password. Please try again.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Close</string>
<string name="common_next">Next</string>
<string name="common_cancel">Cancel</string>
<string name="common_back">Back</string>
<string name="unknown_error">An unknown error occurred</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Confirm PIN</string>
<string name="pin_confirm_description">Re-enter your PIN to confirm</string>
<string name="pin_mismatch">PINs do not match. Please try again.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Unlock Vault</string>
<string name="password_unlock_subtitle">Enter your master password</string>
<string name="password_unlock_password_hint">Password</string>
<string name="password_unlock_button">Unlock</string>
<string name="password_unlock_incorrect">Incorrect password. Please try again.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Luk</string>
<string name="common_next">Næste</string>
<string name="common_cancel">Annuller</string>
<string name="common_back">Tilbage</string>
<string name="unknown_error">En ukendt fejl opstod</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Kunne ikke hente, åben app</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Bekræft PIN-kode</string>
<string name="pin_confirm_description">Indtast din PIN-kode igen for at bekræfte</string>
<string name="pin_mismatch">Pinkoden stemmer ikke overens. Prøv venligst igen.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Lås boks op</string>
<string name="password_unlock_subtitle">Indtast din hovedadgangskode</string>
<string name="password_unlock_password_hint">Adgangskode</string>
<string name="password_unlock_button">Lås op</string>
<string name="password_unlock_incorrect">Forkert adgangskode. Prøv venligst igen.</string>
<string name="password_unlock_error">Kunne ikke bekræfte adgangskode</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Schließen</string>
<string name="common_next">Weiter</string>
<string name="common_cancel">Abbrechen</string>
<string name="common_back">Zurück</string>
<string name="unknown_error">Ein unbekannter Fehler ist aufgetreten</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Abruf der Daten fehlgeschlagen. Öffne die App</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">PIN bestätigen</string>
<string name="pin_confirm_description">Zur Bestätigung PIN erneut eingeben</string>
<string name="pin_mismatch">PINs stimmen nicht überein. Bitte versuche es erneut.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Tresor entsperren</string>
<string name="password_unlock_subtitle">Gib Dein Master-Passwort ein</string>
<string name="password_unlock_password_hint">Passwort</string>
<string name="password_unlock_button">Entsperren</string>
<string name="password_unlock_incorrect">Falsches Passwort. Bitte versuche es erneut.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Cerrar</string>
<string name="common_next">Siguiente</string>
<string name="common_cancel">Cancelar</string>
<string name="common_back">Atrás</string>
<string name="unknown_error">Se produjo un error desconocido</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Fallo al recuperar, abrir aplicación</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Confirmar PIN</string>
<string name="pin_confirm_description">Vuelva a introducir su PIN para confirmar</string>
<string name="pin_mismatch">Los PIN no coinciden. Por favor, inténtalo de nuevo.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Desbloquear bóveda</string>
<string name="password_unlock_subtitle">Ingrese su contraseña maestra</string>
<string name="password_unlock_password_hint">Contraseña</string>
<string name="password_unlock_button">Desbloquear</string>
<string name="password_unlock_incorrect">Contraseña incorrecta. Por favor, inténtelo de nuevo.</string>
<string name="password_unlock_error">Error al verificar la contraseña</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Close</string>
<string name="common_next">Next</string>
<string name="common_cancel">Cancel</string>
<string name="common_back">Back</string>
<string name="unknown_error">An unknown error occurred</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Confirm PIN</string>
<string name="pin_confirm_description">Re-enter your PIN to confirm</string>
<string name="pin_mismatch">PINs do not match. Please try again.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Unlock Vault</string>
<string name="password_unlock_subtitle">Enter your master password</string>
<string name="password_unlock_password_hint">Password</string>
<string name="password_unlock_button">Unlock</string>
<string name="password_unlock_incorrect">Incorrect password. Please try again.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Sulje</string>
<string name="common_next">Seuraava</string>
<string name="common_cancel">Peruuta</string>
<string name="common_back">Takaisin</string>
<string name="unknown_error">Tapahtui tuntematon virhe</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Nouto epäonnistui, avaa sovellus</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Vahvista PIN</string>
<string name="pin_confirm_description">Syötä PIN-koodi uudelleen vahvistaaksesi</string>
<string name="pin_mismatch">PIN-koodit eivät täsmää. Yritä uudelleen.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Avaa holvin lukitus</string>
<string name="password_unlock_subtitle">Syötä pääsalasanasi</string>
<string name="password_unlock_password_hint">Salasana:</string>
<string name="password_unlock_button">Avaa lukitus</string>
<string name="password_unlock_incorrect">Virheellinen salasana. Yritä uudelleen. </string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Fermer</string>
<string name="common_next">Suivant</string>
<string name="common_cancel">Annuler</string>
<string name="common_back">Retour</string>
<string name="unknown_error">Une erreur inconnue s\'est produite</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Échec de la récupération, ouvrez l\'application</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Confirmer le code PIN</string>
<string name="pin_confirm_description">Entrez à nouveau votre code PIN pour confirmer</string>
<string name="pin_mismatch">Les codes PIN ne correspondent pas. Veuillez réessayer.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Déverrouiller le coffre</string>
<string name="password_unlock_subtitle">Entrez votre mot de passe maître</string>
<string name="password_unlock_password_hint">Mot de passe</string>
<string name="password_unlock_button">Déverrouiller</string>
<string name="password_unlock_incorrect">Mot de passe incorrect. Veuillez réessayer.</string>
<string name="password_unlock_error">Échec de la vérification du mot de passe</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">סגירה</string>
<string name="common_next">הבא</string>
<string name="common_cancel">ביטול</string>
<string name="common_back">חזרה</string>
<string name="unknown_error">אירעה שגיאה לא ידועה</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">המשיכה נכשלה, נא לפתוח את היישום</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">אישור קוד אישי</string>
<string name="pin_confirm_description">נא למלא את הקוד האישי שלך מחדש לאישור</string>
<string name="pin_mismatch">הקודים האישיים לא תואמים. נא לנסות שוב.</string>
<!-- Password unlock -->
<string name="password_unlock_title">שחרור נעילת כספת</string>
<string name="password_unlock_subtitle">נא למלא את סיסמת העל שלך</string>
<string name="password_unlock_password_hint">סיסמה</string>
<string name="password_unlock_button">שחרור נעילה</string>
<string name="password_unlock_incorrect">סיסמה שגויה. נא לנסות שוב.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Chiudi</string>
<string name="common_next">Avanti</string>
<string name="common_cancel">Annulla</string>
<string name="common_back">Indietro</string>
<string name="unknown_error">Si è verificato un errore sconosciuto</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Impossibile recuperare, aprire l\'app</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Conferma PIN</string>
<string name="pin_confirm_description">Reinserisci il tuo PIN per confermare.</string>
<string name="pin_mismatch">I PIN non corrispondono. Riprova.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Sblocca Cassaforte</string>
<string name="password_unlock_subtitle">Inserisci la password principale</string>
<string name="password_unlock_password_hint">Password</string>
<string name="password_unlock_button">Sblocca</string>
<string name="password_unlock_incorrect">Password errata. Riprovare.</string>
<string name="password_unlock_error">Verifica della password non riuscita</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Close</string>
<string name="common_next">Next</string>
<string name="common_cancel">Cancel</string>
<string name="common_back">Back</string>
<string name="unknown_error">An unknown error occurred</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Confirm PIN</string>
<string name="pin_confirm_description">Re-enter your PIN to confirm</string>
<string name="pin_mismatch">PINs do not match. Please try again.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Unlock Vault</string>
<string name="password_unlock_subtitle">Enter your master password</string>
<string name="password_unlock_password_hint">Password</string>
<string name="password_unlock_button">Unlock</string>
<string name="password_unlock_incorrect">Incorrect password. Please try again.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -1,8 +1,5 @@
<resources>
<!-- Splashscreen and autofill background (dark mode) -->
<color name="splashscreen_background">#202020</color>
<!-- AliasVault Dark Mode Colors -->
<color name="av_text">#ECEDEE</color>
<color name="av_text_muted">#9BA1A6</color>
<color name="av_background">#000000</color>
@@ -14,7 +11,5 @@
<color name="av_tertiary">#eabf69</color>
<color name="av_icon">#9BA1A6</color>
<color name="av_error">#ef4444</color>
<!-- Loading overlay background (dark mode: black with 80% opacity) -->
<color name="av_loading_overlay">#CC000000</color>
</resources>
</resources>

View File

@@ -23,4 +23,13 @@
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
<!-- Dark mode splash screen theme -->
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
<item name="windowSplashScreenAnimationDuration">0</item>
<item name="android:windowSplashScreenIconBackgroundColor">@android:color/transparent</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Sluiten</string>
<string name="common_next">Volgende</string>
<string name="common_cancel">Annuleren</string>
<string name="common_back">Terug</string>
<string name="unknown_error">Er is een onbekende fout opgetreden</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Ophalen mislukt, open app</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Bevestig pincode</string>
<string name="pin_confirm_description">Voer je pincode opnieuw in om te bevestigen</string>
<string name="pin_mismatch">Pincodes komen niet overeen. Probeer het opnieuw.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Ontgrendel je vault</string>
<string name="password_unlock_subtitle">Voer je hoofdwachtwoord in</string>
<string name="password_unlock_password_hint">Wachtwoord</string>
<string name="password_unlock_button">Ontgrendelen</string>
<string name="password_unlock_incorrect">Onjuist wachtwoord. Probeer het opnieuw.</string>
<string name="password_unlock_error">Verifiëren wachtwoord mislukt</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Zamknij</string>
<string name="common_next">Dalej</string>
<string name="common_cancel">Anuluj</string>
<string name="common_back">Wstecz</string>
<string name="unknown_error">Wystąpił nieznany błąd</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Nie udało się pobrać, otwórz aplikację</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Potwierdź kod PIN</string>
<string name="pin_confirm_description">Wprowadź ponownie swój kod PIN, aby potwierdzić</string>
<string name="pin_mismatch">Kody PIN nie pasują. Spróbuj jeszcze raz.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Odblokuj sejf</string>
<string name="password_unlock_subtitle">Wprowadź swoje hasło główne</string>
<string name="password_unlock_password_hint">Hasło</string>
<string name="password_unlock_button">Odblokuj</string>
<string name="password_unlock_incorrect">Nieprawidłowe hasło. Spróbuj ponownie.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Fechar</string>
<string name="common_next">Próximo</string>
<string name="common_cancel">Cancelar</string>
<string name="common_back">Voltar</string>
<string name="unknown_error">Ocorreu um erro desconhecido</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Falha ao recuperar, abra o aplicativo</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Confirmar PIN</string>
<string name="pin_confirm_description">Digite seu PIN novamente para confirmar</string>
<string name="pin_mismatch">PINs não correspondem. Por favor, tente novamente.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Desbloquear Cofre</string>
<string name="password_unlock_subtitle">Digite sua senha mestre</string>
<string name="password_unlock_password_hint">Senha</string>
<string name="password_unlock_button">Desbloquear</string>
<string name="password_unlock_incorrect">Senha incorreta. Por favor tente novamente.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Închide</string>
<string name="common_next">Înainte</string>
<string name="common_cancel">Anulează</string>
<string name="common_back">Înapoi</string>
<string name="unknown_error">A apărut o eroare necunoscută</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Preluare eșuată, deschide aplicația</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Confirmă PIN</string>
<string name="pin_confirm_description">Introdu PIN-ul din nou pentru confirmare</string>
<string name="pin_mismatch">Codurile PIN nu se potrivesc. Încearcă din nou.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Deblochează seiful</string>
<string name="password_unlock_subtitle">Introdu parola principală</string>
<string name="password_unlock_password_hint">Parolă</string>
<string name="password_unlock_button">Deblochează</string>
<string name="password_unlock_incorrect">Parolă incorectă. Încearcă din nou.</string>
<string name="password_unlock_error">Verificarea parolei a eșuat</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Закрыть</string>
<string name="common_next">Далее</string>
<string name="common_cancel">Отмена</string>
<string name="common_back">Назад</string>
<string name="unknown_error">Произошла неизвестная ошибка</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Не удалось извлечь, открыть приложение</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Подтвердите ПИН-код</string>
<string name="pin_confirm_description">Введите PIN-код еще раз для подтверждения</string>
<string name="pin_mismatch">PIN-коды не совпадают. Повторите попытку.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Разблокировать хранилище</string>
<string name="password_unlock_subtitle">Введите свой мастер-пароль</string>
<string name="password_unlock_password_hint">Пароль</string>
<string name="password_unlock_button">Разблокировать</string>
<string name="password_unlock_incorrect">Неверный пароль. Повторите попытку.</string>
<string name="password_unlock_error">Не удалось проверить пароль</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Stäng</string>
<string name="common_next">Nästa</string>
<string name="common_cancel">Avbryt</string>
<string name="common_back">Tillbaka</string>
<string name="unknown_error">Ett okänt fel har inträffat</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Det gick inte att hämta, öppna app</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Bekräfta PIN-kod</string>
<string name="pin_confirm_description">Ange din PIN-kod igen för att bekräfta</string>
<string name="pin_mismatch">PIN-koderna matchar inte. Var god försök igen.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Lås upp valv</string>
<string name="password_unlock_subtitle">Ange ditt huvudlösenord</string>
<string name="password_unlock_password_hint">Lösenord</string>
<string name="password_unlock_button">Lås upp</string>
<string name="password_unlock_incorrect">Felaktigt lösenord. Försök igen.</string>
<string name="password_unlock_error">Kunde inte verifiera lösenord</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Kapat</string>
<string name="common_next">İleri</string>
<string name="common_cancel">İptal</string>
<string name="common_back">Geri</string>
<string name="unknown_error">An unknown error occurred</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Confirm PIN</string>
<string name="pin_confirm_description">Re-enter your PIN to confirm</string>
<string name="pin_mismatch">PINs do not match. Please try again.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Kasa Kilidini Aç</string>
<string name="password_unlock_subtitle">Enter your master password</string>
<string name="password_unlock_password_hint">Parola</string>
<string name="password_unlock_button">Kilidi aç</string>
<string name="password_unlock_incorrect">Parola yanlış. Lütfen yeniden deneyin.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">Закрити</string>
<string name="common_next">Далі</string>
<string name="common_cancel">Скасувати</string>
<string name="common_back">Назад</string>
<string name="unknown_error">Сталася невідома помилка</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Не вдалося отримати, відкрийте додаток</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Підтвердити ПІН-код</string>
<string name="pin_confirm_description">Введіть PIN-код ще раз для підтвердження</string>
<string name="pin_mismatch">PIN-коди не збігаються. Спробуйте ще раз.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Розблокувати Vault</string>
<string name="password_unlock_subtitle">Введіть ваш головний пароль</string>
<string name="password_unlock_password_hint">Пароль</string>
<string name="password_unlock_button">Розблокувати</string>
<string name="password_unlock_incorrect">Невірний пароль. Будь ласка, спробуйте ще раз.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">بند کریں</string>
<string name="common_next">آگے جائیں</string>
<string name="common_cancel">منسوخ کریں</string>
<string name="common_back">واپس جائیں</string>
<string name="unknown_error">An unknown error occurred</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">Confirm PIN</string>
<string name="pin_confirm_description">Re-enter your PIN to confirm</string>
<string name="pin_mismatch">PINs do not match. Please try again.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Unlock Vault</string>
<string name="password_unlock_subtitle">Enter your master password</string>
<string name="password_unlock_password_hint">پاس ورڈ</string>
<string name="password_unlock_button">Unlock</string>
<string name="password_unlock_incorrect">Incorrect password. Please try again.</string>
<string name="password_unlock_error">Failed to verify password</string>
</resources>

View File

@@ -7,6 +7,7 @@
<string name="common_close">关闭</string>
<string name="common_next">下一步</string>
<string name="common_cancel">取消</string>
<string name="common_back">返回</string>
<string name="unknown_error">发生未知错误</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">检索失败,请打开应用</string>
@@ -81,4 +82,11 @@
<string name="pin_confirm_title">确认 PIN</string>
<string name="pin_confirm_description">重新输入您的 PIN 以确认</string>
<string name="pin_mismatch">PIN 不匹配,请重试。</string>
<!-- Password unlock -->
<string name="password_unlock_title">解锁密码库</string>
<string name="password_unlock_subtitle">输入您的主密码</string>
<string name="password_unlock_password_hint">密码</string>
<string name="password_unlock_button">解锁</string>
<string name="password_unlock_incorrect">密码错误,请重试。</string>
<string name="password_unlock_error">密码验证失败</string>
</resources>

View File

@@ -1,11 +1,7 @@
<resources>
<color name="splashscreen_background">#ffffff</color>
<color name="ic_launcher_background">#FFFFFF</color>
<!-- Legacy color -->
<color name="colorPrimary">#023c69</color>
<!-- AliasVault Light Mode Colors -->
<color name="av_light_text">#11181C</color>
<color name="av_light_text_muted">#4b5563</color>
<color name="av_light_background">#f3f4f6</color>
@@ -16,8 +12,6 @@
<color name="av_light_tertiary">#eabf69</color>
<color name="av_light_icon">#687076</color>
<color name="av_light_error">#D32F2F</color>
<!-- AliasVault Dark Mode Colors (in values/colors.xml, will be overridden in values-night/colors.xml) -->
<color name="av_text">#11181C</color>
<color name="av_text_muted">#4b5563</color>
<color name="av_background">#f3f4f6</color>
@@ -29,10 +23,8 @@
<color name="av_tertiary">#eabf69</color>
<color name="av_icon">#687076</color>
<color name="av_error">#D32F2F</color>
<!-- Loading overlay background (light mode: white with 80% opacity) -->
<color name="av_loading_overlay">#CCFFFFFF</color>
<!-- Alias for primary color (used in PIN unlock) -->
<color name="primary">@color/av_primary</color>
</resources>
<color name="iconBackground">#000000</color>
<color name="colorPrimaryDark">#ffffff</color>
</resources>

View File

@@ -5,27 +5,19 @@
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
<string name="autofill_service_description" translatable="true">AliasVault AutoFill</string>
<string name="aliasvault_icon">AliasVault icon</string>
<!-- Common strings -->
<string name="common_close">Close</string>
<string name="common_next">Next</string>
<string name="common_cancel">Cancel</string>
<string name="common_back">Back</string>
<string name="unknown_error">An unknown error occurred</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
<string name="autofill_no_match_found">No match found, create new?</string>
<string name="autofill_open_app">Open app</string>
<string name="autofill_vault_locked">Vault locked</string>
<!-- Biometric prompts -->
<string name="biometric_store_key_title">Store Encryption Key</string>
<string name="biometric_store_key_subtitle">Authenticate to securely store your encryption key in the Android Keystore. This enables secure access to your vault.</string>
<string name="biometric_unlock_vault_title">Unlock Vault</string>
<string name="biometric_unlock_vault_subtitle">Authenticate to access your vault</string>
<!-- Passkey registration -->
<string name="passkey_registration_title">Create Passkey</string>
<string name="create_passkey_title">Create New Passkey</string>
<string name="create_passkey_subtitle">Register a new passkey for this website. It will be securely stored in your vault and automatically synced across your devices with AliasVault.</string>
@@ -56,8 +48,6 @@
<string name="passkey_retrieving">Retrieving passkey…</string>
<string name="passkey_verifying">Verifying…</string>
<string name="passkey_authenticating">Authenticating…</string>
<!-- Vault sync error messages -->
<string name="connection_error_title">Connection Error</string>
<string name="connection_error_message">No connection to the server can be made. Please check your internet connection and try creating the passkey again.</string>
<string name="session_expired_title">Session Expired</string>
@@ -72,32 +62,26 @@
<string name="network_error_message">A network error occurred. Please check your connection and try again.</string>
<string name="server_version_not_supported_title">Server Update Required</string>
<string name="server_version_not_supported_message">The server version is outdated. Please contact your administrator to update the server.</string>
<!-- Passkey authentication and unlock error messages -->
<string name="error_unlock_method_required">Please enable biometric or PIN authentication in the main AliasVault app in order to continue</string>
<string name="error_unlock_vault_first">Please unlock vault in AliasVault app first</string>
<string name="error_vault_decrypt_failed">Failed to decrypt vault</string>
<string name="error_biometric_cancelled">Biometric authentication cancelled</string>
<string name="error_encryption_key_failed">Failed to retrieve encryption key</string>
<!-- PIN unlock -->
<string name="pin_unlock_vault">Unlock Vault</string>
<string name="pin_enter_to_unlock">Enter your PIN to unlock your vault</string>
<string name="pin_locked_max_attempts">PIN locked after too many failed attempts</string>
<string name="pin_incorrect_attempts_remaining">Incorrect PIN. %d attempts remaining</string>
<!-- PIN setup -->
<string name="pin_setup_title">Setup PIN</string>
<string name="pin_setup_description">Choose a PIN to unlock your vault</string>
<string name="pin_confirm_title">Confirm PIN</string>
<string name="pin_confirm_description">Re-enter your PIN to confirm</string>
<string name="pin_mismatch">PINs do not match. Please try again.</string>
<!-- Password unlock -->
<string name="password_unlock_title">Unlock Vault</string>
<string name="password_unlock_subtitle">Enter your master password</string>
<string name="password_unlock_password_hint">Password</string>
<string name="password_unlock_button">Unlock</string>
<string name="password_unlock_incorrect">Incorrect password. Please try again.</string>
<string name="password_unlock_error">Failed to verify password</string>
<string name="password_unlock_attempts_warning">Incorrect password. You will be logged out if you enter the wrong password %d more times.</string>
<string name="password_unlock_max_attempts_reached">Too many failed unlock attempts. You have been logged out for security reasons.</string>
</resources>

View File

@@ -1,25 +1,15 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.EdgeToEdge.Light">
<style name="AppTheme" parent="Theme.EdgeToEdge">
<item name="android:textColor">@android:color/black</item>
<item name="android:editTextStyle">@style/ResetEditText</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">@color/av_background</item>
<item name="android:navigationBarColor">#fff</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<!-- Light mode: use dark icons/text on light background -->
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item>
<item name="enforceNavigationBarContrast">false</item>
</style>
<style name="ResetEditText" parent="@android:style/Widget.EditText">
<item name="android:padding">0dp</item>
<item name="android:textColorHint">#c8c8c8</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
<style name="PasskeyRegistrationTheme" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/av_primary</item>
<item name="colorOnPrimary">#FFFFFF</item>
@@ -47,4 +37,11 @@
<item name="android:insetBottom">2dp</item>
<item name="rippleColor">@color/av_primary</item>
</style>
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
<item name="windowSplashScreenAnimationDuration">0</item>
<item name="android:windowSplashScreenIconBackgroundColor">@android:color/transparent</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
</resources>

View File

@@ -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

View File

@@ -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",
@@ -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",
{

View File

@@ -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<void> => {
// 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);

View File

@@ -16,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';
@@ -671,9 +672,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 +914,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 +979,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({

View File

@@ -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]);

View File

@@ -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]);

View File

@@ -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<void> => {
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])
);
/**
@@ -282,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,

View File

@@ -1,10 +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 type { RefreshToken } from '@/utils/dist/core/models/webapi';
import { HapticsUtility } from '@/utils/HapticsUtility';
import { useColors } from '@/hooks/useColorScheme';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
@@ -27,6 +27,7 @@ export default function ActiveSessionsScreen() : React.ReactNode {
const [refreshTokens, setRefreshTokens] = useState<RefreshToken[]>([]);
const [isLoading, setIsLoading] = useMinDurationLoading(true, 200);
const [isRefreshing, setIsRefreshing] = useMinDurationLoading(false, 200);
const [expandedSessions, setExpandedSessions] = useState<Set<string>>(new Set());
const styles = StyleSheet.create({
detailText: {
@@ -135,9 +136,7 @@ export default function ActiveSessionsScreen() : React.ReactNode {
*/
const onRefresh = async () : Promise<void> => {
// 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();
@@ -158,6 +157,25 @@ 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
HapticsUtility.impact();
};
return (
<ThemedContainer>
<ThemedScrollView
@@ -184,7 +202,18 @@ export default function ActiveSessionsScreen() : React.ReactNode {
refreshTokens.map((item) => (
<View key={item.id} style={styles.sessionItem}>
<View style={styles.sessionHeader}>
<ThemedText style={styles.deviceName} numberOfLines={2}>{item.deviceIdentifier}</ThemedText>
<TouchableOpacity
style={{ flex: 1 }}
onPress={() => toggleSessionExpansion(item.id)}
activeOpacity={0.7}
>
<ThemedText
style={styles.deviceName}
numberOfLines={expandedSessions.has(item.id) ? undefined : 2}
>
{item.deviceIdentifier}
</ThemedText>
</TouchableOpacity>
<TouchableOpacity onPress={() => handleRevokeSession(item.id)}>
<ThemedText style={styles.revokeButton}>{t('settings.securitySettings.activeSessions.revoke')}</ThemedText>
</TouchableOpacity>

View File

@@ -1,11 +1,11 @@
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 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';
@@ -109,9 +109,7 @@ export default function AuthLogsScreen() : React.ReactNode {
*/
const onRefresh = async () : Promise<void> => {
// 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();

View File

@@ -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<AuthMethod[]>([]);
// PIN state
const [pinEnabled, setPinEnabled] = useState(false);
@@ -36,64 +33,44 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
*/
const initializeAuth = async () : Promise<void> => {
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<void> => {
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<void> => {
if (value && !hasBiometrics) {
@@ -107,9 +84,9 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
/**
* Handle the open settings press.
*/
onPress: () : void => {
onPress: async () : Promise<void> => {
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<void> => {
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.

View File

@@ -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<void> => {
// 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();
/*

View File

@@ -4,6 +4,8 @@ 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 { HapticsUtility } from '@/utils/HapticsUtility';
import { AppErrorCode, getAppErrorCode, getErrorTranslationKey, formatErrorWithCode } from '@/utils/types/errors/AppErrorCodes';
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError';
@@ -25,7 +27,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 +76,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;
}
@@ -124,9 +126,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) {
@@ -135,13 +135,18 @@ 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);
// 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'));
@@ -162,7 +167,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.
@@ -204,9 +209,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
@@ -220,13 +223,18 @@ 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);
// 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 +271,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 +307,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 +325,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);
@@ -403,15 +405,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,
@@ -430,14 +436,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,

View File

@@ -187,6 +187,7 @@ export const ConfirmDialog: React.FC<IConfirmDialogProps> = ({
gap: 8,
justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 8,
},
buttonDisabled: {
opacity: 0.6,
@@ -194,6 +195,8 @@ export const ConfirmDialog: React.FC<IConfirmDialogProps> = ({
buttonText: {
fontSize: 16,
fontWeight: '500',
textAlign: 'center',
flexShrink: 1,
},
});

View File

@@ -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<IFolderModalProps> = ({
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) {

View File

@@ -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<AdvancedPasswordFieldRef, Adva
setShowPasswordState(true);
// Haptic feedback for password generation
if (Platform.OS === 'ios' || Platform.OS === 'android') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
HapticsUtility.impact();
}
}
}, [currentSettings, generatePassword, onChangeText, setShowPasswordState]);

View File

@@ -8,6 +8,7 @@ import DraggableFlatList, {
import type { FieldType } from '@/utils/dist/core/models/vault';
import { FieldTypes } from '@/utils/dist/core/models/vault';
import { HapticsUtility } from '@/utils/HapticsUtility';
import { useColors } from '@/hooks/useColorScheme';
@@ -162,7 +163,7 @@ export const DraggableCustomFieldsList: React.FC<DraggableCustomFieldsListProps>
* Handle drag begin
*/
const handleDragBegin = useCallback(() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
HapticsUtility.impact(Haptics.ImpactFeedbackStyle.Medium);
}, []);
/**

View File

@@ -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<FormInputCopyToClipboardProps> = ({
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) {

Some files were not shown because too many files have changed in this diff Show More