Compare commits

...

34 Commits

Author SHA1 Message Date
Leendert de Borst
48414dcae4 Bump install.sh version (#1254) 2025-09-19 14:39:13 +02:00
Leendert de Borst
151548f6f7 Bump versions (#1254) 2025-09-19 14:39:13 +02:00
Leendert de Borst
fd5c8096ad New Crowdin updates (#1222)
* New translations start.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* Add Ukrainian language (#1183)

* Add Hebrew language to all apps (#1182)

* New translations emailmodal.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations emailpreview.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations emailrow.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations recentemails.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations editemailformrow.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Spanish)
Update translations from Crowdin [ci skip]

* New translations en.json (Catalan)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Swedish)
Update translations from Crowdin [ci skip]

* New translations en.json (Turkish)
Update translations from Crowdin [ci skip]

* New translations en.json (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Spanish)
Update translations from Crowdin [ci skip]

* New translations en.json (Catalan)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Swedish)
Update translations from Crowdin [ci skip]

* New translations en.json (Turkish)
Update translations from Crowdin [ci skip]

* New translations en.json (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations register.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations passwordstep.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations termsandconditionsstep.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations usernamestep.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations totpviewer.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations emailmodal.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations emailpreview.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations emailrow.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations recentemails.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations editemailformrow.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations defaultpasswordsettings.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations importservicecard.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations passwordsettingspopup.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations activesessionssection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations deleteaccountsection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations passwordchangesection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations recentauthlogssection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations showrecoverycodes.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations twofactorauthenticationsection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations createnewidentitywidget.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations searchwidget.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations footer.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations forgotpassword.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations logout.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations setup.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations delete.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations view.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations apps.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations security.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations creating.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations vaultdecryptionprogress.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations apierrors.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations changepassword.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations deleteaccount.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations validationmessages.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations resetvaultsection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations infoplist.strings (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations forgotpassword.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations setup.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations setup.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations apps.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]
2025-09-19 14:38:19 +02:00
Leendert de Borst
09cfee2888 Add test case for nested form elements, refactor logic (#1252) 2025-09-19 12:48:02 +02:00
Leendert de Borst
74cb2eae7d Update password autofill to prevent duplicate character entry (#1252) 2025-09-19 12:48:02 +02:00
Leendert de Borst
35b8f0abae Prepopulate service title and URL based on current tab in browser extension (#1250) 2025-09-18 18:58:20 +02:00
Leendert de Borst
08517e3469 Add credential create popout icon in inline credential create as fallback (#1247) 2025-09-18 17:07:25 +02:00
Leendert de Borst
f3dabc3a39 Update last email/username placeholder to work like suggestions (#1247) 2025-09-18 17:07:25 +02:00
Leendert de Borst
d98f047963 Fix missing translations in confirm modals (#1244) 2025-09-18 13:30:18 +02:00
Leendert de Borst
599966996e Add liquid glass design optimized app icon to iOS app (#1239) 2025-09-18 12:45:33 +02:00
Leendert de Borst
952cfd9a28 Add argon2kt native implementation to Android (#1241) 2025-09-18 10:09:38 +02:00
Leendert de Borst
81a5155734 Replace argon2id react native with native iOS implementation to satisfy Xcode 26 reqs (#1241) 2025-09-18 10:09:38 +02:00
Leendert de Borst
3a953ec7c8 Add monochrome icon support to Android app (#1229) 2025-09-18 08:00:36 +02:00
dependabot[bot]
392dbd626c Bump rexml in /docs in the bundler group across 1 directory
Bumps the bundler group with 1 update in the /docs directory: [rexml](https://github.com/ruby/rexml).


Updates `rexml` from 3.3.9 to 3.4.2
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.9...v3.4.2)

---
updated-dependencies:
- dependency-name: rexml
  dependency-version: 3.4.2
  dependency-type: indirect
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-18 08:00:24 +02:00
Leendert de Borst
b6d3f9e70f Run automatic Docker image cleanup after build and update (#1232) 2025-09-17 20:23:04 +02:00
Leendert de Borst
c2f2511f6a Delete CNAME 2025-09-17 19:09:27 +02:00
Leendert de Borst
ce2e21900f Add plausible to docs 2025-09-17 19:06:53 +02:00
Leendert de Borst
660b286ee9 Add clear alias fields button to web app (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
133037dcd8 Do not pregenerate password on credential create screen initialize (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
03b65a63ba Only overwrite email/username/pass if values were autogenerated during alias generation (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
f7a8189b86 Fix password field settings initialization (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
38973de6f1 Add clear alias fields button to mobile app (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
9ddd00bfa4 Add clear alias fields button to browser extension (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
88013161d1 Update email domain field behavior in browser extension and mobile app (#1231) 2025-09-17 12:58:04 +02:00
Leendert de Borst
b0da0d8590 Create CNAME 2025-09-17 09:50:56 +02:00
Leendert de Borst
7dcfd6bfd1 Delete CNAME 2025-09-17 09:41:31 +02:00
Leendert de Borst
586b0a3495 Update volume bind mounts to use local folder mounts 2025-09-17 09:14:33 +02:00
Leendert de Borst
30a009c5c4 Add docs local production docker-compose.yml 2025-09-17 09:10:10 +02:00
Leendert de Borst
7d73222ee1 Create SECURITY.txt 2025-09-16 15:10:11 +02:00
Leendert de Borst
6d191a1bd5 Rename SECURITY.md to ARCHITECTURE.md 2025-09-16 14:30:12 +02:00
Leendert de Borst
e5c68c6c6e Bump version to 0.23.1 (#1227) 2025-09-16 13:43:20 +02:00
Leendert de Borst
58c39815e4 Add more browser like behavior to improve FaviconExtractor success rate (#1225) 2025-09-16 13:19:22 +02:00
Leendert de Borst
4b706f466f Improve favicon extractor request handling (#1225) 2025-09-16 13:19:22 +02:00
Leendert de Borst
19f72b1386 Update self-signed SSL cert logic to use correct IP vs DNS name labels (#1223) 2025-09-16 11:40:00 +02:00
238 changed files with 9136 additions and 651 deletions

2
.vscode/tasks.json vendored
View File

@@ -199,7 +199,7 @@
{
"label": "Build and watch Docs",
"type": "shell",
"command": "docker compose build && docker compose up",
"command": "docker compose -f docker-compose.dev.yml build && docker compose -f docker-compose.dev.yml up",
"problemMatcher": [],
"group": {
"kind": "build",

View File

17
SECURITY.txt Normal file
View File

@@ -0,0 +1,17 @@
Contact: mailto:security@support.aliasvault.net
Expires: 2026-09-16T12:00:00.000Z
Preferred-Languages: en
Canonical: https://raw.githubusercontent.com/aliasvault/aliasvault/main/SECURITY.txt
# Security Policy for AliasVault
#
# We take security seriously and appreciate responsible disclosure of vulnerabilities.
# Please report security issues to the email above rather than opening public issues.
#
# Include the following information in your report:
# - Description of the vulnerability
# - Steps to reproduce
# - Potential impact
# - Suggested remediation (if any)
#
# We will acknowledge receipt within 48 hours and provide updates as we investigate.

View File

@@ -2,7 +2,7 @@
"name": "aliasvault-browser-extension",
"description": "AliasVault Browser Extension",
"private": true,
"version": "0.23.0",
"version": "0.23.2",
"type": "module",
"scripts": {
"dev:chrome": "wxt -b chrome",

View File

@@ -447,7 +447,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -460,7 +460,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -479,7 +479,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -492,7 +492,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -515,7 +515,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -530,7 +530,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -554,7 +554,7 @@
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,

View File

@@ -3,7 +3,7 @@ import { onMessage, sendMessage } from "webext-bridge/background";
import { handleResetAutoLockTimer, handlePopupHeartbeat, handleSetAutoLockTimeout } from '@/entrypoints/background/AutolockTimeoutHandler';
import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardClearTimeout, handleSetClipboardClearTimeout, handleGetClipboardCountdownState } from '@/entrypoints/background/ClipboardClearHandler';
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
import { handleOpenPopup, handlePopupWithCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
import { handleOpenPopup, handlePopupWithCredential, handleOpenPopupCreateCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetEncryptionKey, handleGetEncryptionKeyDerivationParams, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreEncryptionKey, handleStoreEncryptionKeyDerivationParams, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants';
@@ -40,6 +40,7 @@ export default defineBackground({
onMessage('OPEN_POPUP', () => handleOpenPopup());
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));
onMessage('OPEN_POPUP_CREATE_CREDENTIAL', ({ data }) => handleOpenPopupCreateCredential(data));
onMessage('TOGGLE_CONTEXT_MENU', ({ data }) => handleToggleContextMenu(data));
onMessage('PERSIST_FORM_VALUES', ({ data }) => handlePersistFormValues(data));

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
import { SKIP_FORM_RESTORE_KEY } from '@/utils/Constants';
import { BoolResponse } from '@/utils/types/messaging/BoolResponse';
import { browser } from '#imports';
@@ -37,6 +38,53 @@ export function handlePopupWithCredential(message: any) : Promise<BoolResponse>
})();
}
/**
* Handle opening the popup on create credential page with prefilled service name.
*/
export function handleOpenPopupCreateCredential(message: any) : Promise<BoolResponse> {
return (async () : Promise<BoolResponse> => {
const serviceName = encodeURIComponent(message.serviceName || '');
// Use the URL passed from the content script (current page URL)
let serviceUrl = '';
if (message.currentUrl) {
try {
const url = new URL(message.currentUrl);
// Only include http/https URLs
if (url.protocol === 'http:' || url.protocol === 'https:') {
serviceUrl = encodeURIComponent(url.origin + url.pathname);
}
} catch (error) {
console.error('Error parsing current URL:', error);
}
}
// Set a localStorage flag to skip restoring previously persisted form values as we want to start fresh with this explicit create credential request.
await browser.storage.local.set({ [SKIP_FORM_RESTORE_KEY]: true });
const urlParams = new URLSearchParams();
urlParams.set('expanded', 'true');
if (serviceName) {
urlParams.set('serviceName', serviceName);
}
if (serviceUrl) {
urlParams.set('serviceUrl', serviceUrl);
}
if (message.currentUrl) {
urlParams.set('currentUrl', message.currentUrl);
}
browser.windows.create({
url: browser.runtime.getURL(`/popup.html?${urlParams.toString()}#/credentials/add`),
type: 'popup',
width: 400,
height: 600,
focused: true
});
return { success: true };
})();
}
/**
* Handle toggling the context menu.
*/

View File

@@ -3,12 +3,12 @@ import { sendMessage } from 'webext-bridge/content-script';
import { filterCredentials, AutofillMatchingMode } from '@/entrypoints/contentScript/Filter';
import { fillCredential } from '@/entrypoints/contentScript/Form';
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, LAST_CUSTOM_EMAIL_KEY, LAST_CUSTOM_USERNAME_KEY, AUTOFILL_MATCHING_MODE_KEY } from '@/utils/Constants';
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, AUTOFILL_MATCHING_MODE_KEY, CUSTOM_EMAIL_HISTORY_KEY, CUSTOM_USERNAME_HISTORY_KEY } from '@/utils/Constants';
import { CreateIdentityGenerator } from '@/utils/dist/shared/identity-generator';
import type { Credential } from '@/utils/dist/shared/models/vault';
import { CreatePasswordGenerator, PasswordGenerator, PasswordSettings } from '@/utils/dist/shared/password-generator';
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { ClickValidator } from '@/utils/security/ClickValidator';
import { ServiceDetectionUtility } from '@/utils/serviceDetection/ServiceDetectionUtility';
import { SqliteClient } from '@/utils/SqliteClient';
import { CredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
import { IdentitySettingsResponse } from '@/utils/types/messaging/IdentitySettingsResponse';
@@ -227,8 +227,8 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials:
e.stopPropagation();
e.stopImmediatePropagation();
const suggestedNames = FormDetector.getSuggestedServiceName(document, window.location);
const result = await createAliasCreationPopup(suggestedNames, rootContainer);
const serviceInfo = ServiceDetectionUtility.getServiceInfo(document, window.location);
const result = await createAliasCreationPopup(serviceInfo.suggestedNames, rootContainer);
if (!result) {
// User cancelled
@@ -762,9 +762,9 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
// Close existing popup
removeExistingPopup(rootContainer);
// Load last used values
const lastEmail = await storage.getItem(LAST_CUSTOM_EMAIL_KEY) as string ?? '';
const lastUsername = await storage.getItem(LAST_CUSTOM_USERNAME_KEY) as string ?? '';
// Load history
const emailHistory = await storage.getItem(CUSTOM_EMAIL_HISTORY_KEY) as string[] ?? [];
const usernameHistory = await storage.getItem(CUSTOM_USERNAME_HISTORY_KEY) as string[] ?? [];
return new Promise((resolve) => {
(async (): Promise<void> => {
@@ -829,11 +829,20 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
${randomIdentityIcon}
<h3 class="av-create-popup-title">${randomIdentityTitle}</h3>
</div>
<button class="av-create-popup-mode-dropdown">
<svg class="av-icon" viewBox="0 0 24 24">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
<div class="av-create-popup-header-buttons">
<button class="av-create-popup-mode-dropdown">
<svg class="av-icon" viewBox="0 0 24 24">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
<button class="av-create-popup-popout" title="Open in main popup">
<svg class="av-icon" viewBox="0 0 24 24">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</button>
</div>
</div>
</div>
@@ -888,8 +897,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
id="custom-email"
class="av-create-popup-input"
placeholder="${enterEmailAddressText}"
data-default-value="${lastEmail}"
>
<div class="av-field-suggestions" id="email-suggestions"></div>
</div>
<div class="av-create-popup-field-group">
<label for="custom-username">${usernameText}</label>
@@ -898,8 +907,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
id="custom-username"
class="av-create-popup-input"
placeholder="${enterUsernameText}"
data-default-value="${lastUsername}"
>
<div class="av-field-suggestions" id="username-suggestions"></div>
</div>
<div class="av-create-popup-field-group">
<label>${passwordText}</label>
@@ -960,6 +969,7 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
const customMode = popup.querySelector('.av-create-popup-custom-mode') as HTMLElement;
const dropdownMenu = popup.querySelector('.av-create-popup-mode-dropdown-menu') as HTMLElement;
const titleContainer = popup.querySelector('.av-create-popup-title-container') as HTMLElement;
const popoutBtn = popup.querySelector('.av-create-popup-popout') as HTMLButtonElement;
const cancelBtn = popup.querySelector('#cancel-btn') as HTMLButtonElement;
const customCancelBtn = popup.querySelector('#custom-cancel-btn') as HTMLButtonElement;
const saveBtn = popup.querySelector('#save-btn') as HTMLButtonElement;
@@ -970,41 +980,154 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
const passwordPreview = popup.querySelector('#password-preview') as HTMLInputElement;
const regenerateBtn = popup.querySelector('#regenerate-password') as HTMLButtonElement;
const toggleVisibilityBtn = popup.querySelector('#toggle-password-visibility') as HTMLButtonElement;
const emailSuggestions = popup.querySelector('#email-suggestions') as HTMLElement;
const usernameSuggestions = popup.querySelector('#username-suggestions') as HTMLElement;
/**
* Setup default value for input with placeholder styling.
* Update history with new value (max 2 unique entries)
*/
const setupDefaultValue = (input: HTMLInputElement) : void => {
const defaultValue = input.dataset.defaultValue;
if (defaultValue) {
input.value = defaultValue;
input.classList.add('av-create-popup-input-default');
const updateHistory = async (value: string, historyKey: typeof CUSTOM_EMAIL_HISTORY_KEY | typeof CUSTOM_USERNAME_HISTORY_KEY, maxItems: number = 2): Promise<string[]> => {
const history = await storage.getItem(historyKey) as string[] ?? [];
// Remove the value if it already exists
const filteredHistory = history.filter((item: string) => item !== value);
// Add the new value at the beginning
if (value.trim()) {
filteredHistory.unshift(value);
}
// Keep only the first maxItems
const updatedHistory = filteredHistory.slice(0, maxItems);
// Save the updated history
await storage.setItem(historyKey, updatedHistory);
return updatedHistory;
};
setupDefaultValue(customEmail);
setupDefaultValue(customUsername);
/**
* Remove item from history
*/
const removeFromHistory = async (value: string, historyKey: typeof CUSTOM_EMAIL_HISTORY_KEY | typeof CUSTOM_USERNAME_HISTORY_KEY): Promise<string[]> => {
const history = await storage.getItem(historyKey) as string[] ?? [];
const updatedHistory = history.filter((item: string) => item !== value);
await storage.setItem(historyKey, updatedHistory);
return updatedHistory;
};
// Handle input changes
customEmail.addEventListener('input', () => {
const value = customEmail.value.trim();
if (value || value === '') {
customEmail.classList.remove('av-create-popup-input-default');
storage.setItem(LAST_CUSTOM_EMAIL_KEY, value);
/**
* Format suggestions HTML as pill-style buttons
*/
const formatSuggestionsHtml = async (history: string[], currentValue: string): Promise<string> => {
// Filter out the current value from history and limit to 2 items
const filteredHistory = history
.filter(item => item.toLowerCase() !== currentValue.toLowerCase())
.slice(0, 2);
if (filteredHistory.length === 0) {
return '';
}
// Build HTML with pill-style buttons
return filteredHistory.map(item =>
`<span class="av-suggestion-pill">
<span class="av-suggestion-pill-text" data-value="${item}">${item}</span>
<span class="av-suggestion-pill-delete" data-value="${item}" title="Remove">×</span>
</span>`
).join(' ');
};
/**
* Update suggestions display
*/
const updateSuggestions = async (input: HTMLInputElement, suggestionsContainer: HTMLElement, history: string[]): Promise<void> => {
const currentValue = input.value.trim();
const html = await formatSuggestionsHtml(history, currentValue);
suggestionsContainer.innerHTML = html;
suggestionsContainer.style.display = html ? 'flex' : 'none';
};
// Initial display of suggestions
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
// Handle popout button click
popoutBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const serviceName = inputServiceName.value.trim();
const encodedServiceInfo = ServiceDetectionUtility.getEncodedServiceInfo(document, window.location);
sendMessage('OPEN_POPUP_CREATE_CREDENTIAL', {
serviceName: serviceName || encodedServiceInfo.serviceName,
currentUrl: encodedServiceInfo.currentUrl
}, 'background');
closePopup(null);
});
// Handle email input
customEmail.addEventListener('input', async () => {
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
});
// Handle username input
customUsername.addEventListener('input', async () => {
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
});
// Handle suggestion clicks for email
emailSuggestions.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const target = e.target as HTMLElement;
// Check if delete button was clicked
if (target.classList.contains('av-suggestion-pill-delete')) {
const value = target.dataset.value;
if (value) {
const updatedHistory = await removeFromHistory(value, CUSTOM_EMAIL_HISTORY_KEY);
emailHistory.splice(0, emailHistory.length, ...updatedHistory);
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
}
} else {
customEmail.classList.add('av-create-popup-input-default');
storage.setItem(LAST_CUSTOM_EMAIL_KEY, '');
// Check if pill or pill text was clicked
let pillElement = target.closest('.av-suggestion-pill') as HTMLElement;
if (pillElement) {
const textElement = pillElement.querySelector('.av-suggestion-pill-text') as HTMLElement;
const value = textElement?.dataset.value;
if (value) {
customEmail.value = value;
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
}
}
}
});
customUsername.addEventListener('input', () => {
const value = customUsername.value.trim();
if (value || value === '') {
customUsername.classList.remove('av-create-popup-input-default');
storage.setItem(LAST_CUSTOM_USERNAME_KEY, value);
// Handle suggestion clicks for username
usernameSuggestions.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const target = e.target as HTMLElement;
// Check if delete button was clicked
if (target.classList.contains('av-suggestion-pill-delete')) {
const value = target.dataset.value;
if (value) {
const updatedHistory = await removeFromHistory(value, CUSTOM_USERNAME_HISTORY_KEY);
usernameHistory.splice(0, usernameHistory.length, ...updatedHistory);
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
}
} else {
customUsername.classList.add('av-create-popup-input-default');
storage.setItem(LAST_CUSTOM_USERNAME_KEY, '');
// Check if pill or pill text was clicked
let pillElement = target.closest('.av-suggestion-pill') as HTMLElement;
if (pillElement) {
const textElement = pillElement.querySelector('.av-suggestion-pill-text') as HTMLElement;
const value = textElement?.dataset.value;
if (value) {
customUsername.value = value;
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
}
}
}
});
@@ -1372,12 +1495,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
if (serviceName) {
const email = customEmail.value.trim();
const username = customUsername.value.trim();
const hasDefaultEmail = customEmail.classList.contains('av-create-popup-input-default');
const hasDefaultUsername = customUsername.classList.contains('av-create-popup-input-default');
// If using default values, use the dataset values
const finalEmail = hasDefaultEmail ? customEmail.dataset.defaultValue : email;
const finalUsername = hasDefaultUsername ? customUsername.dataset.defaultValue : username;
const finalEmail = email;
const finalUsername = username;
if (!finalEmail && !finalUsername) {
// Add error styling to fields
@@ -1424,6 +1543,14 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
return;
}
// Update history when saving
if (finalEmail) {
await updateHistory(finalEmail, CUSTOM_EMAIL_HISTORY_KEY);
}
if (finalUsername) {
await updateHistory(finalUsername, CUSTOM_USERNAME_HISTORY_KEY);
}
closePopup({
serviceName,
isCustomCredential: true,

View File

@@ -539,6 +539,62 @@ body {
box-shadow: 0 0 0 1px #ef4444 !important;
}
/* Field Suggestions - Pill Style */
.av-field-suggestions {
margin-top: 8px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.av-suggestion-pill {
display: inline-flex;
align-items: center;
background: #4b5563;
border: 1px solid #6b7280;
border-radius: 16px;
padding: 4px 8px 4px 12px;
font-size: 13px;
color: #e5e7eb;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.av-suggestion-pill:hover {
background: #6b7280;
border-color: #9ca3af;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.av-suggestion-pill-text {
display: inline-block;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.av-suggestion-pill-delete {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 6px;
padding: 0 2px;
color: #9ca3af;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s;
border-left: 1px solid #6b7280;
padding-left: 6px;
}
.av-suggestion-pill-delete:hover {
color: #ef4444;
}
.av-create-popup-error-text {
color: #ef4444;
font-size: 0.875rem;
@@ -728,28 +784,41 @@ body {
.av-create-popup-title-container {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
justify-content: center;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s;
position: relative;
}
.av-create-popup-title-wrapper {
position: absolute;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: #d68338;
pointer-events: none;
}
.av-create-popup-header-buttons {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
}
.av-create-popup-title-wrapper .av-icon {
width: 20px;
height: 20px;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: auto;
}
.av-create-popup-title-wrapper .av-create-popup-title {
@@ -757,6 +826,7 @@ body {
font-size: 18px;
font-weight: 600;
color: #f8f9fa;
pointer-events: auto;
}
.av-create-popup-title-container:hover {
@@ -785,6 +855,34 @@ body {
height: 16px;
}
.av-create-popup-popout {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: #9ca3af;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s, color 0.2s;
}
.av-create-popup-popout:hover {
background-color: #4b5563;
color: #d68338;
}
.av-create-popup-popout .av-icon {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.av-create-popup-mode-dropdown-menu {
position: absolute;
left: 50%;

View File

@@ -90,7 +90,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
setIsCustomDomain(!isKnownDomain);
} else {
setLocalPart(value);
setIsCustomDomain(false);
// Don't reset isCustomDomain here - preserve the current mode
// Set default domain if not already set
if (!selectedDomain && !value.includes('@')) {
@@ -107,6 +107,13 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const handleLocalPartChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const newLocalPart = e.target.value;
// If in custom domain mode, always pass through the full value
if (isCustomDomain) {
onChange(newLocalPart);
// Stay in custom domain mode - don't auto-switch back
return;
}
// Check if new value contains '@' symbol, if so, switch to custom domain mode
if (newLocalPart.includes('@')) {
setIsCustomDomain(true);
@@ -115,10 +122,11 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
}
setLocalPart(newLocalPart);
if (!isCustomDomain && selectedDomain) {
// If the local part is empty, treat the whole field as empty
if (!newLocalPart || newLocalPart.trim() === '') {
onChange('');
} else if (selectedDomain) {
onChange(`${newLocalPart}@${selectedDomain}`);
} else {
onChange(newLocalPart);
}
}, [isCustomDomain, selectedDomain, onChange]);
@@ -126,7 +134,12 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const selectDomain = useCallback((domain: string) => {
setSelectedDomain(domain);
const cleanLocalPart = localPart.includes('@') ? localPart.split('@')[0] : localPart;
onChange(`${cleanLocalPart}@${domain}`);
// If the local part is empty, treat the whole field as empty
if (!cleanLocalPart || cleanLocalPart.trim() === '') {
onChange('');
} else {
onChange(`${cleanLocalPart}@${domain}`);
}
setIsCustomDomain(false);
setIsPopupVisible(false);
}, [localPart, onChange]);
@@ -136,13 +149,30 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const newIsCustom = !isCustomDomain;
setIsCustomDomain(newIsCustom);
if (!newIsCustom && !value.includes('@')) {
// Switching to domain chooser mode, add default domain
if (newIsCustom) {
/*
* Switching to custom domain mode
* If we have a domain-based value, extract just the local part
*/
if (value && value.includes('@')) {
const [local] = value.split('@');
onChange(local);
setLocalPart(local);
}
} else {
// Switching to domain chooser mode
const defaultDomain = showPrivateDomains && privateEmailDomains[0]
? privateEmailDomains[0]
: PUBLIC_EMAIL_DOMAINS[0];
onChange(`${localPart}@${defaultDomain}`);
setSelectedDomain(defaultDomain);
// Only add domain if we have a local part
if (localPart && localPart.trim()) {
onChange(`${localPart}@${defaultDomain}`);
} else if (value && !value.includes('@')) {
// If we have a value without @, add the domain
onChange(`${value}@${defaultDomain}`);
}
}
}, [isCustomDomain, value, localPart, showPrivateDomains, privateEmailDomains, onChange]);

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import type { PasswordSettings } from '@/utils/dist/shared/models/vault';
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
@@ -15,7 +17,6 @@ interface IPasswordFieldProps {
error?: string;
showPassword?: boolean;
onShowPasswordChange?: (show: boolean) => void;
initialSettings: PasswordSettings;
}
/**
@@ -29,13 +30,14 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
placeholder,
error,
showPassword: controlledShowPassword,
onShowPasswordChange,
initialSettings
onShowPasswordChange
}) => {
const { t } = useTranslation();
const dbContext = useDb();
const [internalShowPassword, setInternalShowPassword] = useState(false);
const [showConfigDialog, setShowConfigDialog] = useState(false);
const [currentSettings, setCurrentSettings] = useState<PasswordSettings>(initialSettings);
const [currentSettings, setCurrentSettings] = useState<PasswordSettings | null>(null);
const [isLoaded, setIsLoaded] = useState(false);
// Use controlled or uncontrolled showPassword state
const showPassword = controlledShowPassword !== undefined ? controlledShowPassword : internalShowPassword;
@@ -51,11 +53,24 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
}
}, [controlledShowPassword, onShowPasswordChange]);
// Initialize settings only once when component mounts
// Load password settings from database
useEffect(() => {
setCurrentSettings({ ...initialSettings });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only run on mount to avoid resetting user changes
/**
* Load password settings from the database.
*/
const loadSettings = async (): Promise<void> => {
try {
if (dbContext.sqliteClient) {
const settings = dbContext.sqliteClient.getPasswordSettings();
setCurrentSettings(settings);
setIsLoaded(true);
}
} catch (error) {
console.error('Error loading password settings:', error);
}
};
void loadSettings();
}, [dbContext.sqliteClient]);
const generatePassword = useCallback((settings: PasswordSettings) => {
try {
@@ -69,6 +84,9 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
}, [onChange, setShowPassword]);
const handleLengthChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (!currentSettings) {
return;
}
const length = parseInt(e.target.value, 10);
const newSettings = { ...currentSettings, Length: length };
setCurrentSettings(newSettings);
@@ -78,6 +96,9 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
}, [currentSettings, generatePassword]);
const handleRegeneratePassword = useCallback(() => {
if (!currentSettings) {
return;
}
generatePassword(currentSettings);
}, [generatePassword, currentSettings]);
@@ -98,6 +119,18 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
setShowConfigDialog(true);
}, []);
// Don't render until settings are loaded
if (!currentSettings || !isLoaded) {
return (
<div className="space-y-2">
<label htmlFor={id} className="block text-sm font-medium text-gray-900 dark:text-white">
{label}
</label>
<div className="animate-pulse bg-gray-200 dark:bg-gray-700 h-10 rounded-lg"></div>
</div>
);
}
return (
<div className="space-y-2">
{/* Label */}

View File

@@ -23,9 +23,13 @@ import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
import { SKIP_FORM_RESTORE_KEY } from '@/utils/Constants';
import { IdentityHelperUtils, CreateIdentityGenerator, CreateUsernameEmailGenerator, Identity, Gender } from '@/utils/dist/shared/identity-generator';
import type { Attachment, Credential } from '@/utils/dist/shared/models/vault';
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
import { ServiceDetectionUtility } from '@/utils/serviceDetection/ServiceDetectionUtility';
import { browser } from '#imports';
type CredentialMode = 'random' | 'manual';
@@ -90,6 +94,13 @@ const CredentialAddEdit: React.FC = () => {
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
const webApi = useWebApi();
// Track last generated values to avoid overwriting manual entries
const [lastGeneratedValues, setLastGeneratedValues] = useState<{
username: string | null;
password: string | null;
email: string | null;
}>({ username: null, password: null, email: null });
const serviceNameRef = useRef<HTMLInputElement>(null);
const { handleSubmit, setValue, watch, formState: { errors } } = useForm<Credential>({
@@ -223,20 +234,80 @@ const CredentialAddEdit: React.FC = () => {
}
if (!id) {
// On create mode, focus the service name field after a short delay to ensure the component is mounted.
// On create mode, check for URL parameters first, then fallback to tab detection
const urlParams = new URLSearchParams(window.location.search);
const serviceName = urlParams.get('serviceName');
const serviceUrl = urlParams.get('serviceUrl');
const currentUrl = urlParams.get('currentUrl');
/**
* Initialize service detection from URL parameters or current tab
*/
const initializeServiceDetection = async (): Promise<void> => {
try {
// If URL parameters are present (e.g., from content script popout), use them
if (serviceName || serviceUrl || currentUrl) {
if (serviceName) {
setValue('ServiceName', decodeURIComponent(serviceName));
}
if (serviceUrl) {
setValue('ServiceUrl', decodeURIComponent(serviceUrl));
}
// If we have currentUrl but missing serviceName or serviceUrl, derive them
if (currentUrl && (!serviceName || !serviceUrl)) {
const decodedCurrentUrl = decodeURIComponent(currentUrl);
const serviceInfo = ServiceDetectionUtility.getServiceInfoFromTab(decodedCurrentUrl);
if (!serviceName && serviceInfo.suggestedNames.length > 0) {
setValue('ServiceName', serviceInfo.suggestedNames[0]);
}
if (!serviceUrl && serviceInfo.serviceUrl) {
setValue('ServiceUrl', serviceInfo.serviceUrl);
}
}
return;
}
// Otherwise, detect from current active tab (for dashboard case)
const [activeTab] = await browser.tabs.query({ active: true, currentWindow: true });
if (activeTab?.url) {
const serviceInfo = ServiceDetectionUtility.getServiceInfoFromTab(
activeTab.url,
activeTab.title
);
if (serviceInfo.suggestedNames.length > 0) {
setValue('ServiceName', serviceInfo.suggestedNames[0]);
}
if (serviceInfo.serviceUrl) {
setValue('ServiceUrl', serviceInfo.serviceUrl);
}
}
} catch (error) {
console.error('Error detecting service information:', error);
}
};
initializeServiceDetection();
// Focus the service name field after a short delay to ensure the component is mounted.
setTimeout(() => {
serviceNameRef.current?.focus();
}, 100);
setIsInitialLoading(false);
// Load persisted form values if they exist.
loadPersistedValues().then(() => {
// Generate default password if no persisted password exists
if (!watch('Password')) {
const passwordSettings = dbContext.sqliteClient!.getPasswordSettings();
const passwordGenerator = CreatePasswordGenerator(passwordSettings);
const defaultPassword = passwordGenerator.generateRandomPassword();
setValue('Password', defaultPassword);
// Check if we should skip form restoration (e.g., when opened from popout button)
browser.storage.local.get([SKIP_FORM_RESTORE_KEY]).then((result) => {
if (result[SKIP_FORM_RESTORE_KEY]) {
// Clear the flag after using it
browser.storage.local.remove([SKIP_FORM_RESTORE_KEY]);
// Don't load persisted values, but set local loading to false
setLocalLoading(false);
} else {
// Load persisted form values normally
loadPersistedValues();
}
});
return;
@@ -271,7 +342,7 @@ const CredentialAddEdit: React.FC = () => {
console.error('Error loading credential:', err);
setIsInitialLoading(false);
}
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues, watch]);
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues, clearPersistedValues]);
/**
* Handle the delete button click.
@@ -331,35 +402,63 @@ const CredentialAddEdit: React.FC = () => {
const defaultEmailDomain = dbContext.sqliteClient!.getDefaultEmailDomain(privateEmailDomains, publicEmailDomains);
const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
setValue('Alias.Email', email);
// Check current values
const currentUsername = watch('Username') ?? '';
const currentPassword = watch('Password') ?? '';
const currentEmail = watch('Alias.Email') ?? '';
// Only overwrite email if it's empty or matches the last generated value
if (!currentEmail || currentEmail === lastGeneratedValues.email) {
setValue('Alias.Email', email);
}
setValue('Alias.FirstName', identity.firstName);
setValue('Alias.LastName', identity.lastName);
setValue('Alias.NickName', identity.nickName);
setValue('Alias.Gender', identity.gender);
setValue('Alias.BirthDate', IdentityHelperUtils.normalizeBirthDateForDisplay(identity.birthDate.toISOString()));
// In edit mode, preserve existing username and password if they exist
if (isEditMode && watch('Username')) {
// Keep the existing username in edit mode, so don't do anything here.
} else {
// Use the newly generated username
// Only overwrite username if it's empty or matches the last generated value
if (!currentUsername || currentUsername === lastGeneratedValues.username) {
setValue('Username', identity.nickName);
}
if (isEditMode && watch('Password')) {
// Keep the existing password in edit mode, so don't do anything here.
} else {
// Use the newly generated password
// Only overwrite password if it's empty or matches the last generated value
if (!currentPassword || currentPassword === lastGeneratedValues.password) {
setValue('Password', password);
}
}, [isEditMode, watch, setValue, initializeGenerators, dbContext]);
// Update tracking with new generated values
setLastGeneratedValues({
username: identity.nickName,
password: password,
email: email
});
}, [watch, setValue, initializeGenerators, dbContext, lastGeneratedValues, setLastGeneratedValues]);
/**
* Clear all alias fields.
*/
const clearAliasFields = useCallback(() => {
setValue('Alias.FirstName', '');
setValue('Alias.LastName', '');
setValue('Alias.NickName', '');
setValue('Alias.Gender', '');
setValue('Alias.BirthDate', '');
}, [setValue]);
// Check if any alias fields have values.
const hasAliasValues = !!(watch('Alias.FirstName') || watch('Alias.LastName') || watch('Alias.NickName') || watch('Alias.Gender') || watch('Alias.BirthDate'));
/**
* Handle the generate random alias button press.
*/
const handleGenerateRandomAlias = useCallback(() => {
void generateRandomAlias();
}, [generateRandomAlias]);
if (hasAliasValues) {
clearAliasFields();
} else {
void generateRandomAlias();
}
}, [generateRandomAlias, clearAliasFields, hasAliasValues]);
const generateRandomUsername = useCallback(async () => {
try {
@@ -382,15 +481,17 @@ const CredentialAddEdit: React.FC = () => {
};
const username = usernameEmailGenerator.generateUsername(identity);
setValue('Username', username);
const currentUsername = watch('Username') ?? '';
// Only overwrite username if it's empty or matches the last generated value
if (!currentUsername || currentUsername === lastGeneratedValues.username) {
setValue('Username', username);
// Update the tracking for username
setLastGeneratedValues(prev => ({ ...prev, username: username }));
}
} catch (error) {
console.error('Error generating random username:', error);
}
}, [setValue, watch]);
const initialPasswordSettings = useMemo(() => {
return dbContext.sqliteClient?.getPasswordSettings();
}, [dbContext.sqliteClient]);
}, [setValue, watch, lastGeneratedValues, setLastGeneratedValues]);
/**
* Handle form submission.
@@ -609,18 +710,15 @@ const CredentialAddEdit: React.FC = () => {
error={errors.Username?.message}
onRegenerate={generateRandomUsername}
/>
{initialPasswordSettings && (
<PasswordField
id="password"
label={t('common.password')}
value={watch('Password') ?? ''}
onChange={(value) => setValue('Password', value)}
error={errors.Password?.message}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
initialSettings={initialPasswordSettings}
/>
)}
<PasswordField
id="password"
label={t('common.password')}
value={watch('Password') ?? ''}
onChange={(value) => setValue('Password', value)}
error={errors.Password?.message}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
/>
</div>
</div>
@@ -630,17 +728,33 @@ const CredentialAddEdit: React.FC = () => {
<button
type="button"
onClick={handleGenerateRandomAlias}
className="w-full text-sm bg-primary-500 text-white py-2 px-4 rounded hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 flex items-center justify-center gap-2"
className={`w-full text-sm py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-offset-2 flex items-center justify-center gap-2 ${
hasAliasValues
? 'bg-gray-500 text-white hover:bg-gray-600 focus:ring-gray-500'
: 'bg-primary-500 text-white hover:bg-primary-600 focus:ring-primary-500'
}`}
>
<svg className='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8" cy="8" r="1"/>
<circle cx="16" cy="8" r="1"/>
<circle cx="12" cy="12" r="1"/>
<circle cx="8" cy="16" r="1"/>
<circle cx="16" cy="16" r="1"/>
</svg>
<span>{t('credentials.generateRandomAlias')}</span>
{hasAliasValues ? (
<>
<svg className='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
<span>{t('credentials.clearAliasFields')}</span>
</>
) : (
<>
<svg className='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8" cy="8" r="1"/>
<circle cx="16" cy="8" r="1"/>
<circle cx="12" cy="12" r="1"/>
<circle cx="8" cy="16" r="1"/>
<circle cx="16" cy="16" r="1"/>
</svg>
<span>{t('credentials.generateRandomAlias')}</span>
</>
)}
</button>
<FormInput
id="firstName"

View File

@@ -6,8 +6,10 @@
import deTranslations from './locales/de.json';
import enTranslations from './locales/en.json';
import fiTranslations from './locales/fi.json';
import heTranslations from './locales/he.json';
import itTranslations from './locales/it.json';
import nlTranslations from './locales/nl.json';
import ukTranslations from './locales/uk.json';
import zhTranslations from './locales/zh.json';
/**
@@ -24,12 +26,18 @@ export const LANGUAGE_RESOURCES = {
fi: {
translation: fiTranslations
},
he: {
translation: heTranslations
},
it: {
translation: itTranslations
},
nl: {
translation: nlTranslations
},
uk: {
translation: ukTranslations
},
zh: {
translation: zhTranslations
},
@@ -58,6 +66,12 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
nativeName: 'Suomi',
flag: '🇫🇮'
},
{
code: 'he',
name: 'Hebrew',
nativeName: 'עברית',
flag: '🇮🇱'
},
{
code: 'it',
name: 'Italian',
@@ -70,6 +84,12 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
nativeName: 'Nederlands',
flag: '🇳🇱'
},
{
code: 'uk',
name: 'Ukrainian',
nativeName: 'Українська',
flag: '🇺🇦'
},
{
code: 'zh',
name: 'Chinese',
@@ -77,12 +97,6 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
flag: '🇨🇳'
},
/*
* {
* code: 'de',
* name: 'German',
* nativeName: 'Deutsch',
* flag: '🇩🇪'
* },
* {
* code: 'es',
* name: 'Spanish',
@@ -95,12 +109,6 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
* nativeName: 'Français',
* flag: '🇫🇷'
* },
* {
* code: 'uk',
* name: 'Ukrainian',
* nativeName: 'Українська',
* flag: '🇺🇦'
* }
*/
];

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Mehrdeutige Zeichen (1, l, I, 0, O, etc.) vermeiden",
"generateNewPreview": "Neue Vorschau erstellen",
"generateRandomAlias": "Zufälligen Alias generieren",
"clearAliasFields": "Alias-Felder löschen",
"alias": "Alias",
"firstName": "Vorname",
"lastName": "Nachname",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Vältä epäselviä merkkejä (o, 0 jne.)",
"generateNewPreview": "Luo uusi esikatselu",
"generateRandomAlias": "Luo sattumanvarainen alias",
"clearAliasFields": "Tyhjennä aliaksen kentät",
"alias": "Alias",
"firstName": "Etunimi",
"lastName": "Sukunimi",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Éviter les caractères ambigus (o, 0, etc.)",
"generateNewPreview": "Générer un nouvel aperçu",
"generateRandomAlias": "Créer un alias aléatoire",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "Prénom",
"lastName": "Nom",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "עדיף להימנע מאותיות וספרות שדומים זה לזה (o, 0 וכו׳)",
"generateNewPreview": "יצירת תצוגה מקדימה חדשה",
"generateRandomAlias": "יצירת כינוי אקראי",
"clearAliasFields": "Clear Alias Fields",
"alias": "כינוי",
"firstName": "שם פרטי",
"lastName": "שם משפחה",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Evita caratteri ambigui (o, 0, ecc.)",
"generateNewPreview": "Genera nuova anteprima",
"generateRandomAlias": "Genera alias casuale",
"clearAliasFields": "Cancella Campi Alias",
"alias": "Alias",
"firstName": "Nome",
"lastName": "Cognome",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Onduidelijke tekens vermijden (o, 0, etc.)",
"generateNewPreview": "Genereer nieuw voorbeeld",
"generateRandomAlias": "Alias genereren",
"clearAliasFields": "Leeg alias velden",
"alias": "Alias",
"firstName": "Voornaam",
"lastName": "Achternaam",

View File

@@ -0,0 +1,393 @@
{
"auth": {
"loginTitle": "Log in to AliasVault",
"username": "Username or email",
"usernamePlaceholder": "name / name@company.com",
"password": "Password",
"passwordPlaceholder": "Enter your password",
"rememberMe": "Remember me",
"loginButton": "Login",
"noAccount": "No account yet?",
"createVault": "Create new vault",
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
"authCode": "Authentication Code",
"authCodePlaceholder": "Enter 6-digit code",
"verify": "Verify",
"cancel": "Cancel",
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
"masterPassword": "Master Password",
"unlockVault": "Unlock Vault",
"unlockTitle": "Unlock Your Vault",
"unlockDescription": "Enter your master password to unlock your vault.",
"logout": "Logout",
"logoutConfirm": "Are you sure you want to logout?",
"sessionExpired": "Your session has expired. Please log in again.",
"unlockSuccess": "Vault unlocked successfully!",
"unlockSuccessTitle": "Your vault is successfully unlocked",
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
"closePopup": "Close this popup",
"browseVault": "Browse vault contents",
"connectingTo": "Connecting to",
"switchAccounts": "Switch accounts?",
"loggedIn": "Logged in",
"errors": {
"invalidCode": "Please enter a valid 6-digit authentication code.",
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
"noToken": "Login failed -- no token returned",
"migrationError": "An error occurred while checking for pending migrations.",
"wrongPassword": "Incorrect password. Please try again.",
"accountLocked": "Account temporarily locked due to too many failed attempts.",
"networkError": "Network error. Please check your connection and try again.",
"loginDataMissing": "Login session expired. Please try again."
}
},
"menu": {
"credentials": "Credentials",
"emails": "Emails",
"settings": "Settings"
},
"common": {
"appName": "AliasVault",
"loading": "Loading...",
"error": "Error",
"success": "Success",
"cancel": "Cancel",
"use": "Use",
"delete": "Delete",
"close": "Close",
"copied": "Copied!",
"openInNewWindow": "Open in new window",
"language": "Language",
"enabled": "Enabled",
"disabled": "Disabled",
"showPassword": "Show password",
"hidePassword": "Hide password",
"copyToClipboard": "Copy to clipboard",
"loadingEmails": "Loading emails...",
"loadingTotpCodes": "Loading TOTP codes...",
"attachments": "Attachments",
"loadingAttachments": "Loading attachments...",
"settings": "Settings",
"recentEmails": "Recent emails",
"loginCredentials": "Login credentials",
"twoFactorAuthentication": "Two-factor authentication",
"alias": "Alias",
"notes": "Notes",
"fullName": "Full Name",
"firstName": "First Name",
"lastName": "Last Name",
"birthDate": "Birth Date",
"nickname": "Nickname",
"email": "Email",
"username": "Username",
"password": "Password",
"syncingVault": "Syncing vault",
"savingChangesToVault": "Saving changes to vault",
"uploadingVaultToServer": "Uploading vault to server",
"checkingVaultUpdates": "Checking for vault updates",
"syncingUpdatedVault": "Syncing updated vault",
"executingOperation": "Executing operation...",
"loadMore": "Load more",
"errors": {
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
"unknownError": "An unknown error occurred",
"failedToStoreVault": "Failed to store vault",
"vaultNotAvailable": "Vault not available",
"failedToRetrieveData": "Failed to retrieve data",
"vaultIsLocked": "Vault is locked",
"failedToUploadVault": "Failed to upload vault",
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
},
"apiErrors": {
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
"USERNAME_REQUIRED": "Username is required.",
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
"USERNAME_AVAILABLE": "Username is available.",
"USERNAME_MISMATCH": "Username does not match the current user.",
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
"USERNAME_INVALID_EMAIL": "Invalid email address.",
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
"INTERNAL_SERVER_ERROR": "Internal server error.",
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
}
},
"content": {
"or": "or",
"new": "New",
"cancel": "Cancel",
"search": "Search",
"vaultLocked": "AliasVault is locked.",
"creatingNewAlias": "Creating new alias...",
"noMatchesFound": "No matches found",
"searchVault": "Search vault...",
"serviceName": "Service name",
"email": "Email",
"username": "Username",
"password": "Password",
"enterServiceName": "Enter service name",
"enterEmailAddress": "Enter email address",
"enterUsername": "Enter username",
"hideFor1Hour": "Hide for 1 hour (current site)",
"hidePermanently": "Hide permanently (current site)",
"createRandomAlias": "Create random alias",
"createUsernamePassword": "Create username/password",
"randomAlias": "Random alias",
"usernamePassword": "Username/password",
"createAndSaveAlias": "Create and save alias",
"createAndSaveCredential": "Create and save credential",
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
"randomIdentityDescriptionDropdown": "Random identity with random email",
"manualCredentialDescription": "Specify your own email address and username.",
"manualCredentialDescriptionDropdown": "Manual username and password",
"failedToCreateIdentity": "Failed to create identity. Please try again.",
"enterEmailAndOrUsername": "Enter email and/or username",
"autofillWithAliasVault": "Autofill with AliasVault",
"generateRandomPassword": "Generate random password (copy to clipboard)",
"generateNewPassword": "Generate new password",
"togglePasswordVisibility": "Toggle password visibility",
"passwordCopiedToClipboard": "Password copied to clipboard",
"enterEmailAndOrUsernameError": "Enter email and/or username",
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
"vaultUpgradeRequired": "Vault upgrade required.",
"dismissPopup": "Dismiss popup"
},
"credentials": {
"title": "Credentials",
"addCredential": "Add Credential",
"editCredential": "Edit Credential",
"deleteCredential": "Delete Credential",
"credentialDetails": "Credential Details",
"serviceName": "Service Name",
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
"website": "Website",
"websitePlaceholder": "https://example.com",
"username": "Username",
"usernamePlaceholder": "Enter username",
"password": "Password",
"passwordPlaceholder": "Enter password",
"generatePassword": "Generate Password",
"copyPassword": "Copy Password",
"showPassword": "Show Password",
"hidePassword": "Hide Password",
"notes": "Notes",
"notesPlaceholder": "Additional notes...",
"totp": "Two-Factor Authentication",
"totpCode": "TOTP Code",
"copyTotp": "Copy TOTP",
"totpSecret": "TOTP Secret",
"totpSecretPlaceholder": "Enter TOTP secret key",
"noCredentials": "No credentials found",
"noCredentialsDescription": "Add your first credential to get started",
"searchPlaceholder": "Search credentials...",
"welcomeTitle": "Welcome to AliasVault!",
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
"createdAt": "Created",
"updatedAt": "Last updated",
"autofill": "Autofill",
"fillForm": "Fill Form",
"deleteConfirm": "Are you sure you want to delete this credential?",
"saveSuccess": "Credential saved successfully",
"tags": "Tags",
"addTag": "Add Tag",
"removeTag": "Remove Tag",
"folder": "Folder",
"selectFolder": "Select Folder",
"createFolder": "Create Folder",
"saveCredential": "Save credential",
"deleteCredentialTitle": "Delete Credential",
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
"randomAlias": "Random Alias",
"manual": "Manual",
"service": "Service",
"serviceUrl": "Service URL",
"loginCredentials": "Login Credentials",
"generateRandomUsername": "Generate random username",
"generateRandomPassword": "Generate random password",
"changePasswordComplexity": "Change password complexity",
"passwordLength": "Password length",
"includeLowercase": "Include lowercase letters",
"includeUppercase": "Include uppercase letters",
"includeNumbers": "Include numbers",
"includeSpecialChars": "Include special characters",
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",
"nickName": "Nick Name",
"gender": "Gender",
"birthDate": "Birth Date",
"birthDatePlaceholder": "YYYY-MM-DD",
"metadata": "Metadata",
"validation": {
"required": "This field is required",
"serviceNameRequired": "Service name is required",
"invalidEmail": "Invalid email format",
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
},
"privateEmailTitle": "Private Email",
"privateEmailAliasVaultServer": "AliasVault server",
"privateEmailDescription": "E2E encrypted, fully private.",
"publicEmailTitle": "Public Temp Email Providers",
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
"useDomainChooser": "Use domain chooser",
"enterCustomDomain": "Enter custom domain",
"enterFullEmail": "Enter full email address",
"enterEmailPrefix": "Enter email prefix"
},
"emails": {
"title": "Emails",
"deleteEmailTitle": "Delete Email",
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
"from": "From",
"to": "To",
"date": "Date",
"emailContent": "Email content",
"attachments": "Attachments",
"emailNotFound": "Email not found",
"noEmails": "No emails found",
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
"dateFormat": {
"justNow": "just now",
"minutesAgo_single": "{{count}} min ago",
"minutesAgo_plural": "{{count}} mins ago",
"hoursAgo_single": "{{count}} hr ago",
"hoursAgo_plural": "{{count}} hrs ago",
"yesterday": "yesterday"
},
"errors": {
"emailLoadError": "An error occurred while loading emails. Please try again later.",
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
},
"apiErrors": {
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again."
}
},
"settings": {
"title": "Settings",
"serverUrl": "Server URL",
"language": "Language",
"autofillEnabled": "Enable Autofill",
"version": "Version",
"openInNewWindow": "Open in new window",
"openWebApp": "Open web app",
"loggedIn": "Logged in",
"logout": "Logout",
"globalSettings": "Global Settings",
"autofillPopup": "Autofill popup",
"activeOnAllSites": "Active on all sites (unless disabled below)",
"disabledOnAllSites": "Disabled on all sites",
"enabled": "Enabled",
"disabled": "Disabled",
"rightClickContextMenu": "Right-click context menu",
"autofillMatching": "Autofill Matching",
"autofillMatchingMode": "Autofill matching mode",
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
"autofillMatchingDefault": "URL + subdomain + name wildcard",
"autofillMatchingUrlSubdomain": "URL + subdomain",
"autofillMatchingUrlExact": "Exact URL domain only",
"siteSpecificSettings": "Site-Specific Settings",
"autofillPopupOn": "Autofill popup on: ",
"enabledForThisSite": "Enabled for this site",
"disabledForThisSite": "Disabled for this site",
"temporarilyDisabledUntil": "Temporarily disabled until ",
"resetAllSiteSettings": "Reset all site-specific settings",
"appearance": "Appearance",
"theme": "Theme",
"useDefault": "Use default",
"light": "Light",
"dark": "Dark",
"keyboardShortcuts": "Keyboard Shortcuts",
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
"configure": "Configure",
"security": "Security",
"clipboardClearTimeout": "Clear clipboard after copying",
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
"clipboardClearDisabled": "Never clear",
"clipboardClear5Seconds": "Clear after 5 seconds",
"clipboardClear10Seconds": "Clear after 10 seconds",
"clipboardClear15Seconds": "Clear after 15 seconds",
"autoLockTimeout": "Auto-lock Timeout",
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
"autoLockNever": "Never",
"autoLock15Seconds": "15 seconds",
"autoLock1Minute": "1 minute",
"autoLock5Minutes": "5 minutes",
"autoLock15Minutes": "15 minutes",
"autoLock30Minutes": "30 minutes",
"autoLock1Hour": "1 hour",
"autoLock4Hours": "4 hours",
"autoLock8Hours": "8 hours",
"autoLock24Hours": "24 hours",
"versionPrefix": "Version ",
"preferences": "Preferences",
"autofillSettings": "Autofill Settings",
"clipboardSettings": "Clipboard Settings",
"contextMenuSettings": "Context Menu Settings",
"contextMenu": "Context Menu",
"contextMenuEnabled": "Context menu is enabled",
"contextMenuDisabled": "Context menu is disabled",
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
"selectLanguage": "Select Language",
"validation": {
"apiUrlRequired": "API URL is required",
"apiUrlInvalid": "Please enter a valid API URL",
"clientUrlRequired": "Client URL is required",
"clientUrlInvalid": "Please enter a valid client URL"
}
},
"upgrade": {
"title": "Upgrade Vault",
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
"versionInformation": "Version Information",
"yourVault": "Your vault:",
"newVersion": "New version:",
"upgrade": "Upgrade Vault",
"upgrading": "Upgrading...",
"logout": "Logout",
"whatsNew": "What's New",
"whatsNewDescription": "An upgrade is required to support the following changes:",
"noDescriptionAvailable": "No description available for this version.",
"okay": "Ok",
"status": {
"preparingUpgrade": "Preparing upgrade...",
"vaultAlreadyUpToDate": "Vault is already up to date",
"startingDatabaseTransaction": "Starting database transaction...",
"applyingDatabaseMigrations": "Applying database migrations...",
"applyingMigration": "Applying migration {{current}} of {{total}}...",
"committingChanges": "Committing changes..."
},
"alerts": {
"error": "Error",
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
"selfHostedServer": "Self-Hosted Server",
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
"cancel": "Cancel",
"continueUpgrade": "Continue Upgrade",
"upgradeFailed": "Upgrade Failed",
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
}
}
}

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Избегать двусмысленных символов (o, 0 и т.д.).",
"generateNewPreview": "Создать новый предварительный просмотр",
"generateRandomAlias": "Сгенерировать случайный псевдоним",
"clearAliasFields": "Очистить поля псевдонимов",
"alias": "Псевдоним",
"firstName": "Имя",
"lastName": "Фамилия",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Уникайте неоднозначних символів (o, 0 тощо)",
"generateNewPreview": "Згенерувати новий попередній перегляд",
"generateRandomAlias": "Генерувати випадковий псевдонім",
"clearAliasFields": "Clear Alias Fields",
"alias": "Псевдонім",
"firstName": "Ім’я",
"lastName": "Прізвище",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "避免易混淆字符o、0 等)",
"generateNewPreview": "生成新预览",
"generateRandomAlias": "生成随机别名",
"clearAliasFields": "清除别名字段",
"alias": "别名",
"firstName": "名",
"lastName": "姓",

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.23.0';
public static readonly VERSION = '0.23.2';
/**
* The minimum supported AliasVault server (API) version. If the server version is below this, the

View File

@@ -1,4 +1,3 @@
// TODO: store generic setting constants somewhere else.
export const DISABLED_SITES_KEY = 'local:aliasvault_disabled_sites';
export const GLOBAL_AUTOFILL_POPUP_ENABLED_KEY = 'local:aliasvault_global_autofill_popup_enabled';
export const GLOBAL_CONTEXT_MENU_ENABLED_KEY = 'local:aliasvault_global_context_menu_enabled';
@@ -9,5 +8,6 @@ export const AUTO_LOCK_TIMEOUT_KEY = 'local:aliasvault_auto_lock_timeout';
export const AUTOFILL_MATCHING_MODE_KEY = 'local:aliasvault_autofill_matching_mode';
// TODO: store these settings in the actual vault when updating the datamodel for roadmap v1.0.
export const LAST_CUSTOM_EMAIL_KEY = 'local:aliasvault_last_custom_email';
export const LAST_CUSTOM_USERNAME_KEY = 'local:aliasvault_last_custom_username';
export const CUSTOM_EMAIL_HISTORY_KEY = 'local:aliasvault_custom_email_history';
export const CUSTOM_USERNAME_HISTORY_KEY = 'local:aliasvault_custom_username_history';
export const SKIP_FORM_RESTORE_KEY = 'local:aliasvault_skip_form_restore';

View File

@@ -479,14 +479,18 @@ export class FormDetector {
excludeElements: HTMLInputElement[] = []
): HTMLInputElement | null {
const all = this.findAllInputFields(form, patterns, types, excludeElements);
// Filter out parent-child duplicates
const filtered = this.filterOutNestedDuplicates(all);
// if email type explicitly requested, prefer actual <input type="email">
if (types.includes('email')) {
const emailMatch = all.find(i => (i.type || '').toLowerCase() === 'email');
const emailMatch = filtered.find(i => (i.type || '').toLowerCase() === 'email');
if (emailMatch) {
return emailMatch;
}
}
return all.length > 0 ? all[0] : null;
return filtered.length > 0 ? filtered[0] : null;
}
/**
@@ -496,25 +500,32 @@ export class FormDetector {
primary: HTMLInputElement | null,
confirm: HTMLInputElement | null
} {
// Find primary email field
const primaryEmail = this.findInputField(
// Find all email fields first
const emailFields = this.findAllInputFields(
form,
CombinedFieldPatterns.email,
['text', 'email']
);
// Filter out parent-child relationships
const filteredEmailFields = this.filterOutNestedDuplicates(emailFields);
const primaryEmail = filteredEmailFields[0] ?? null;
/*
* Find confirmation email field if primary exists
* and ensure it's not the same as the primary email field.
*/
const confirmEmail = primaryEmail
? this.findInputField(
const confirmEmailFields = primaryEmail
? this.findAllInputFields(
form,
CombinedFieldPatterns.emailConfirm,
['text', 'email'],
[primaryEmail]
)
: null;
: [];
const filteredConfirmFields = this.filterOutNestedDuplicates(confirmEmailFields);
const confirmEmail = filteredConfirmFields[0] ?? null;
return {
primary: primaryEmail,
@@ -667,6 +678,56 @@ export class FormDetector {
};
}
/**
* Filter out nested duplicates where a parent element and its child are both detected.
* This happens with custom elements that contain actual input elements.
* We prefer the innermost actual input element over the parent custom element.
*/
private filterOutNestedDuplicates(fields: HTMLInputElement[]): HTMLInputElement[] {
if (fields.length <= 1) {
return fields;
}
const filtered: HTMLInputElement[] = [];
for (const field of fields) {
let shouldInclude = true;
// Check if this field is a parent of any other field in the list
for (const otherField of fields) {
if (field !== otherField) {
// Check if field contains otherField (field is parent)
if (field.contains(otherField)) {
shouldInclude = false;
break;
}
// Check if field's shadow DOM contains otherField
const fieldWithShadow = field as HTMLElement & { shadowRoot?: ShadowRoot };
if (fieldWithShadow.shadowRoot && fieldWithShadow.shadowRoot.contains(otherField)) {
shouldInclude = false;
break;
}
}
}
if (shouldInclude) {
// Also check if this field is not already represented by its actual input
const actualInput = this.getActualInputElement(field);
if (actualInput !== field) {
// If the actual input is also in the list, skip the parent
if (fields.includes(actualInput as HTMLInputElement)) {
continue;
}
}
filtered.push(field);
}
}
return filtered;
}
/**
* Find the password field in a form.
*/
@@ -676,9 +737,12 @@ export class FormDetector {
} {
const passwordFields = this.findAllInputFields(form, CombinedFieldPatterns.password, ['password']);
// Filter out parent-child relationships to avoid detecting the same field twice
const filteredFields = this.filterOutNestedDuplicates(passwordFields);
return {
primary: passwordFields[0] ?? null,
confirm: passwordFields[1] ?? null
primary: filteredFields[0] ?? null,
confirm: filteredFields[1] ?? null
};
}

View File

@@ -33,7 +33,12 @@ export class FormFiller {
return;
}
this.fillBasicFields(credential);
// Fill basic fields and password fields in parallel
await Promise.all([
this.fillBasicFields(credential),
this.fillPasswordFields(credential)
]);
this.fillBirthdateFields(credential);
this.fillGenderFields(credential);
}
@@ -61,7 +66,7 @@ export class FormFiller {
clientY: window.innerHeight / 2
});
// Note: isTrusted is read-only and set by the browser
if (!await this.clickValidator.validateClick(dummyEvent)) {
console.warn('[AliasVault Security] Form autofill blocked: Page-wide attack detected');
return false;
@@ -94,7 +99,7 @@ export class FormFiller {
*/
private getAllFormFields(): HTMLElement[] {
const fields: HTMLElement[] = [];
if (this.form.usernameField) {
fields.push(this.form.usernameField);
}
@@ -110,7 +115,7 @@ export class FormFiller {
if (this.form.emailConfirmField) {
fields.push(this.form.emailConfirmField);
}
return fields;
}
@@ -132,8 +137,8 @@ export class FormFiller {
const centerY = rect.top + rect.height / 2;
// Check if field is within viewport
if (rect.width === 0 || rect.height === 0 ||
centerX < 0 || centerY < 0 ||
if (rect.width === 0 || rect.height === 0 ||
centerX < 0 || centerY < 0 ||
centerX > window.innerWidth || centerY > window.innerHeight) {
console.warn('[AliasVault Security] Field outside viewport or zero-sized:', rect);
return false;
@@ -142,16 +147,16 @@ export class FormFiller {
// Use elementsFromPoint to check what's actually at the field center
try {
const elementsAtPoint = document.elementsFromPoint(centerX, centerY);
if (elementsAtPoint.length === 0) {
console.warn('[AliasVault Security] No elements found at field center');
return false;
}
// Check if our field is in the element stack (or its parents/children)
const fieldFound = elementsAtPoint.some(element =>
element === field ||
field.contains(element) ||
const fieldFound = elementsAtPoint.some(element =>
element === field ||
field.contains(element) ||
element.contains(field)
);
@@ -167,7 +172,7 @@ export class FormFiller {
}
const style = getComputedStyle(element);
// Check for nearly transparent overlays
const opacity = parseFloat(style.opacity);
if (opacity > 0 && opacity < 0.1) {
@@ -184,7 +189,7 @@ export class FormFiller {
// Check for elements covering large areas (potential clickjacking overlays)
const elementRect = element.getBoundingClientRect();
if (elementRect.width >= window.innerWidth * 0.8 &&
if (elementRect.width >= window.innerWidth * 0.8 &&
elementRect.height >= window.innerHeight * 0.8) {
console.warn('[AliasVault Security] Large covering element detected:', element);
return true;
@@ -207,35 +212,35 @@ export class FormFiller {
try {
// Find all forms on the page
const allForms = Array.from(document.querySelectorAll('form'));
if (allForms.length <= 1) {
return false; // Only one form, no decoy risk
}
let suspiciousFormCount = 0;
for (const form of allForms) {
const hasPasswordField = form.querySelector('input[type="password"]');
const hasEmailField = form.querySelector('input[type="email"], input[name*="email" i], input[placeholder*="email" i]');
const hasUsernameField = form.querySelector('input[type="text"], input[name*="user" i], input[placeholder*="user" i]');
// Count forms with login-like patterns
if (hasPasswordField && (hasEmailField || hasUsernameField)) {
const formRect = form.getBoundingClientRect();
const isVisible = formRect.width > 0 && formRect.height > 0;
if (isVisible) {
suspiciousFormCount++;
}
}
}
// If more than 2 visible login forms, it's suspicious
if (suspiciousFormCount > 2) {
console.warn('[AliasVault Security] Multiple login forms detected:', suspiciousFormCount);
return true;
}
return false;
} catch (error) {
console.warn('[AliasVault Security] Decoy form detection error:', error);
@@ -251,7 +256,7 @@ export class FormFiller {
private setElementValue(element: HTMLInputElement | HTMLSelectElement, value: string): void {
// Try to set value directly on the element
element.value = value;
// If it's a custom element with shadow DOM, try to find and fill the actual input
if (element.shadowRoot) {
const shadowInput = element.shadowRoot.querySelector('input, textarea') as HTMLInputElement;
@@ -261,7 +266,7 @@ export class FormFiller {
this.triggerInputEvents(shadowInput, false);
}
}
// Also check if the element contains a regular child input (non-shadow DOM)
const childInput = element.querySelector('input, textarea') as HTMLInputElement;
if (childInput && childInput !== element) {
@@ -274,18 +279,9 @@ export class FormFiller {
* Fill the basic fields of the form.
* @param credential The credential to fill the form with.
*/
private fillBasicFields(credential: Credential): void {
private async fillBasicFields(credential: Credential): Promise<void> {
if (this.form.usernameField && credential.Username) {
this.setElementValue(this.form.usernameField, credential.Username);
this.triggerInputEvents(this.form.usernameField);
}
if (this.form.passwordField && credential.Password) {
this.fillPasswordField(this.form.passwordField, credential.Password);
}
if (this.form.passwordConfirmField && credential.Password) {
this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
await this.fillTextFieldWithTyping(this.form.usernameField, credential.Username);
}
if (this.form.emailField && (credential.Alias?.Email !== undefined || credential.Username !== undefined)) {
@@ -329,6 +325,70 @@ export class FormFiller {
}
}
/**
* Fill a text field with character-by-character typing to better simulate human input.
* This method is similar to fillPasswordField but optimized for regular text fields.
*
* @param field The text field to fill.
* @param text The text to fill the field with.
*/
private async fillTextFieldWithTyping(field: HTMLInputElement, text: string): Promise<void> {
// Find the actual input element (could be in shadow DOM)
let actualInput = field;
// Check for shadow DOM input
if (field.shadowRoot) {
const shadowInput = field.shadowRoot.querySelector('input, textarea') as HTMLInputElement;
if (shadowInput) {
actualInput = shadowInput;
}
} else if (field.tagName.toLowerCase() !== 'input' && field.tagName.toLowerCase() !== 'textarea') {
// Check for child input (non-shadow DOM) only if field is not already an input
const childInput = field.querySelector('input, textarea') as HTMLInputElement;
if (childInput) {
actualInput = childInput;
}
}
// Clear the field first without triggering events
actualInput.value = '';
// Type each character with a small delay
for (let i = 0; i < text.length; i++) {
actualInput.value += text[i];
/*
* Small delay between characters to simulate human typing
* This helps with sites that have input event handlers
*/
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 10));
}
// Trigger events once after all typing is complete
this.triggerInputEvents(actualInput, true);
}
/**
* Fill password fields sequentially to avoid visual conflicts.
* First fills the main password field, then the confirm field if present.
* @param credential The credential containing the password.
*/
private async fillPasswordFields(credential: Credential): Promise<void> {
if (!credential.Password) {
return;
}
// Fill main password field first
if (this.form.passwordField) {
await this.fillPasswordField(this.form.passwordField, credential.Password);
}
// Then fill password confirm field after main field is complete
if (this.form.passwordConfirmField) {
await this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
}
}
/**
* Fill the password field with the given password. This uses a small delay between each character to simulate human typing.
* Simulates actual keystroke behavior by appending characters one by one.
@@ -340,48 +400,37 @@ export class FormFiller {
private async fillPasswordField(field: HTMLInputElement, password: string): Promise<void> {
// Find the actual input element (could be in shadow DOM)
let actualInput = field;
let isCustomElement = false;
// Check for shadow DOM input
if (field.shadowRoot) {
const shadowInput = field.shadowRoot.querySelector('input[type="password"], input') as HTMLInputElement;
if (shadowInput) {
actualInput = shadowInput;
isCustomElement = true;
}
} else if (field.tagName.toLowerCase() !== 'input') {
// Check for child input (non-shadow DOM) only if field is not already an input
const childInput = field.querySelector('input[type="password"], input') as HTMLInputElement;
if (childInput) {
actualInput = childInput;
isCustomElement = true;
}
}
// Clear the field first
// Clear the field first without triggering events
actualInput.value = '';
if (isCustomElement) {
field.value = '';
}
this.triggerInputEvents(actualInput, true);
// Type each character with a small delay
for (const char of password) {
// Append the character to the actual input
actualInput.value += char;
if (isCustomElement) {
// Also update the custom element's value property for compatibility
field.value += char;
}
// Small random delay between 5-15ms to simulate human typing
this.triggerInputEvents(actualInput, false);
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 5));
for (let i = 0; i < password.length; i++) {
actualInput.value += password[i];
/*
* Small delay between characters to simulate human typing
* This helps with sites that have input event handlers
*/
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 10));
}
this.triggerInputEvents(actualInput, false);
if (isCustomElement) {
this.triggerInputEvents(field, false);
}
// Trigger events once after all typing is complete
this.triggerInputEvents(actualInput, true);
}
/**

View File

@@ -79,4 +79,75 @@ describe('FormDetector generic tests', () => {
expect(form).toBe(false);
});
});
describe('Nested custom elements (parent-child duplicate prevention)', () => {
describe('TrueNAS-style nested custom elements', () => {
const htmlFile = 'nested-custom-elements.html';
it('should not detect both parent custom element and child input as separate password fields', () => {
const dom = createTestDom(htmlFile);
const document = dom.window.document;
// Click on the actual password input element
const passwordInput = document.getElementById('password-field');
const formDetector = new FormDetector(document, passwordInput as HTMLElement);
// Get the detected form
const form = formDetector.getForm();
expect(form).toBeTruthy();
// Should detect only ONE password field
expect(form?.passwordField).toBeTruthy();
expect(form?.passwordConfirmField).toBeFalsy();
// The detected password field should be the actual input element
expect(form?.passwordField?.tagName.toLowerCase()).toBe('input');
expect(form?.passwordField?.type).toBe('password');
expect(form?.passwordField?.id).toBe('password-field');
});
it('should detect username field correctly without duplication', () => {
const dom = createTestDom(htmlFile);
const document = dom.window.document;
const usernameInput = document.getElementById('username-field');
const formDetector = new FormDetector(document, usernameInput as HTMLElement);
const form = formDetector.getForm();
expect(form).toBeTruthy();
// Should detect the username field
expect(form?.usernameField).toBeTruthy();
expect(form?.usernameField?.tagName.toLowerCase()).toBe('input');
expect(form?.usernameField?.id).toBe('username-field');
});
});
describe('Nested custom elements with actual password confirm field', () => {
const htmlFile = 'nested-custom-elements-confirm.html';
it('should correctly identify actual password confirm fields vs parent-child duplicates', () => {
const dom = createTestDom(htmlFile);
const document = dom.window.document;
const passwordElement = document.getElementById('password-field');
const formDetector = new FormDetector(document, passwordElement as HTMLElement);
const form = formDetector.getForm();
expect(form).toBeTruthy();
// Should correctly detect both password and confirm as separate fields
expect(form?.passwordField).toBeTruthy();
expect(form?.passwordConfirmField).toBeTruthy();
// Both should be actual input elements
expect(form?.passwordField?.tagName.toLowerCase()).toBe('input');
expect(form?.passwordConfirmField?.tagName.toLowerCase()).toBe('input');
// They should be different elements
expect(form?.passwordField?.id).toBe('password-field');
expect(form?.passwordConfirmField?.id).toBe('password-confirm-field');
});
});
});
});

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>Registration Form - Nested Custom Elements with Confirm</title>
</head>
<body>
<form id="registration-form">
<div class="field-group">
<ix-input formcontrolname="username" type="text" ix-label="Username" name="username">
<input id="username-field" type="text" aria-label="Username" name="username" class="mat-input-element">
</ix-input>
</div>
<div class="field-group">
<ix-input formcontrolname="password" type="password" ix-label="Password" name="password">
<input id="password-field" type="password" aria-label="Password" name="password" class="mat-input-element">
</ix-input>
</div>
<div class="field-group">
<ix-input formcontrolname="passwordConfirm" type="password" ix-label="Confirm Password" name="passwordConfirm">
<input id="password-confirm-field" type="password" aria-label="Confirm Password" name="passwordConfirm" class="mat-input-element">
</ix-input>
</div>
</form>
</body>
</html>

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title>TrueNAS Login - Nested Custom Elements</title>
</head>
<body>
<form id="login-form">
<div class="field-group">
<ix-input id="username-wrapper" formcontrolname="username" type="text" ix-label="Username" name="username">
<ix-label><label><span>Username</span></label></ix-label>
<div class="input-container">
<input id="username-field" type="text" aria-label="Username" name="username" class="mat-input-element">
</div>
</ix-input>
</div>
<div class="field-group">
<ix-input id="password-wrapper" formcontrolname="password" type="password" ix-label="Password" name="password">
<ix-label><label><span>Password</span></label></ix-label>
<div class="input-container">
<input id="password-field" type="password" aria-label="Password" name="password" class="mat-input-element">
</div>
</ix-input>
</div>
</form>
</body>
</html>

View File

@@ -0,0 +1,148 @@
import { FormDetector } from '../formDetector/FormDetector';
/**
* Utility for detecting service name and URL information
* Shared between content script and popup dashboard
*/
export class ServiceDetectionUtility {
/**
* Get service information from the current page
*/
public static getServiceInfo(document: Document, location: Location): ServiceInfo {
// Get suggested service names using FormDetector
const suggestedNames = FormDetector.getSuggestedServiceName(document, location);
// Get the current URL
const currentUrl = location.href;
// Process the URL to extract service URL (origin + pathname)
let serviceUrl = '';
try {
const url = new URL(currentUrl);
// Only include http/https URLs
if (url.protocol === 'http:' || url.protocol === 'https:') {
serviceUrl = url.origin + url.pathname;
// Remove trailing slash
if (serviceUrl.endsWith('/')) {
serviceUrl = serviceUrl.slice(0, -1);
}
}
} catch (error) {
console.error('Error parsing current URL:', error);
}
return {
suggestedNames,
currentUrl,
serviceUrl,
domain: location.hostname.replace(/^www\./, '')
};
}
/**
* Get service information from tab data (for use in popup dashboard)
*/
public static getServiceInfoFromTab(tabUrl: string, tabTitle?: string): ServiceInfo {
try {
const url = new URL(tabUrl);
const location = {
href: tabUrl,
hostname: url.hostname,
protocol: url.protocol,
pathname: url.pathname,
origin: url.origin
} as Location;
// Create a minimal document object for service name detection
const mockDocument = {
title: tabTitle || url.hostname
} as Document;
// Use FormDetector logic for service name detection
const suggestedNames = FormDetector.getSuggestedServiceName(mockDocument, location);
// Get service URL (origin + pathname)
let serviceUrl = '';
if (url.protocol === 'http:' || url.protocol === 'https:') {
serviceUrl = url.origin + url.pathname;
// Remove trailing slash
if (serviceUrl.endsWith('/')) {
serviceUrl = serviceUrl.slice(0, -1);
}
}
return {
suggestedNames,
currentUrl: tabUrl,
serviceUrl,
domain: url.hostname.replace(/^www\./, '')
};
} catch (error) {
console.error('Error parsing tab URL:', error);
// Fallback to basic hostname detection
const domain = tabUrl.replace(/^https?:\/\/(www\.)?/, '').split('/')[0];
return {
suggestedNames: [domain],
currentUrl: tabUrl,
serviceUrl: tabUrl,
domain
};
}
}
/**
* Get encoded service information suitable for URL parameters
*/
public static getEncodedServiceInfo(document: Document, location: Location): EncodedServiceInfo {
const serviceInfo = this.getServiceInfo(document, location);
return {
serviceName: serviceInfo.suggestedNames.length > 0 ? encodeURIComponent(serviceInfo.suggestedNames[0]) : '',
serviceUrl: serviceInfo.serviceUrl ? encodeURIComponent(serviceInfo.serviceUrl) : '',
currentUrl: encodeURIComponent(serviceInfo.currentUrl),
domain: encodeURIComponent(serviceInfo.domain)
};
}
/**
* Get encoded service information from tab data
*/
public static getEncodedServiceInfoFromTab(tabUrl: string, tabTitle?: string): EncodedServiceInfo {
const serviceInfo = this.getServiceInfoFromTab(tabUrl, tabTitle);
return {
serviceName: serviceInfo.suggestedNames.length > 0 ? encodeURIComponent(serviceInfo.suggestedNames[0]) : '',
serviceUrl: serviceInfo.serviceUrl ? encodeURIComponent(serviceInfo.serviceUrl) : '',
currentUrl: encodeURIComponent(serviceInfo.currentUrl),
domain: encodeURIComponent(serviceInfo.domain)
};
}
}
/**
* Service information interface
*/
export type ServiceInfo = {
/** Array of suggested service names */
suggestedNames: string[];
/** Current page URL */
currentUrl: string;
/** Service URL (origin + pathname) */
serviceUrl: string;
/** Domain name without www prefix */
domain: string;
}
/**
* Encoded service information interface
*/
export type EncodedServiceInfo = {
/** URL-encoded primary service name */
serviceName: string;
/** URL-encoded service URL */
serviceUrl: string;
/** URL-encoded current page URL */
currentUrl: string;
/** URL-encoded domain */
domain: string;
}

View File

@@ -20,7 +20,7 @@ export default defineConfig({
return {
name: "AliasVault",
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
version: "0.23.0",
version: "0.23.2",
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 230000
versionName "0.23.0"
versionCode 230200
versionName "0.23.2"
}
signingConfigs {
debug {
@@ -186,6 +186,9 @@ dependencies {
// Add vector drawable support for SVG
implementation("com.caverock:androidsvg-aar:1.4")
// Add Argon2 library for password key derivation
implementation("com.lambdapioneer.argon2kt:argon2kt:1.4.0")
// Test dependencies
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:4.0.0'

View File

@@ -778,6 +778,27 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
}
}
/**
* Derive a key from a password using Argon2Id.
* @param password The password to derive from
* @param salt The salt to use
* @param encryptionType The type of encryption (should be "Argon2Id")
* @param encryptionSettings JSON string with encryption parameters
* @param promise The promise to resolve
*/
@ReactMethod
override fun deriveKeyFromPassword(password: String, salt: String, encryptionType: String, encryptionSettings: String, promise: Promise) {
try {
val derivedKey = vaultStore.deriveKeyFromPassword(password, salt, encryptionType, encryptionSettings)
// Return as base64 string
val base64Key = android.util.Base64.encodeToString(derivedKey, android.util.Base64.NO_WRAP)
promise.resolve(base64Key)
} catch (e: Exception) {
Log.e(TAG, "Error deriving key from password", e)
promise.reject("ERR_DERIVE_KEY", "Failed to derive key from password: ${e.message}", e)
}
}
/**
* Open the autofill settings page.
* @param promise The promise to resolve

View File

@@ -6,6 +6,9 @@ import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.util.Log
import com.lambdapioneer.argon2kt.Argon2Kt
import com.lambdapioneer.argon2kt.Argon2Mode
import com.lambdapioneer.argon2kt.Argon2Version
import net.aliasvault.app.vaultstore.interfaces.CredentialOperationCallback
import net.aliasvault.app.vaultstore.interfaces.CryptoOperationCallback
import net.aliasvault.app.vaultstore.keystoreprovider.KeystoreOperationCallback
@@ -240,6 +243,43 @@ class VaultStore(
return this.storageProvider.getKeyDerivationParams()
}
/**
* Derive a key from a password using Argon2Id.
* @param password The password to derive from
* @param salt The salt to use
* @param encryptionType The type of encryption (should be "Argon2Id")
* @param encryptionSettings JSON string with encryption parameters
* @return The derived key as a ByteArray
*/
fun deriveKeyFromPassword(password: String, salt: String, encryptionType: String, encryptionSettings: String): ByteArray {
if (encryptionType != "Argon2Id") {
throw IllegalArgumentException("Unsupported encryption type: $encryptionType")
}
// Parse encryption settings JSON
val settings = JSONObject(encryptionSettings)
val iterations = settings.getInt("Iterations")
val memorySize = settings.getInt("MemorySize")
val parallelism = settings.getInt("DegreeOfParallelism")
// Create Argon2 instance
val argon2 = Argon2Kt()
// Hash the password using Argon2Id
val hashResult = argon2.hash(
mode = Argon2Mode.ARGON2_ID,
password = password.toByteArray(Charsets.UTF_8),
salt = salt.toByteArray(Charsets.UTF_8),
tCostInIterations = iterations,
mCostInKibibyte = memorySize,
parallelism = parallelism,
hashLengthInBytes = 32,
version = Argon2Version.V13,
)
return hashResult.rawHashAsByteArray()
}
/**
* Store the encrypted database in the storage provider.
* @param encryptedData The encrypted database as a base64 encoded string

View File

@@ -4,10 +4,10 @@
android:viewportWidth="500"
android:viewportHeight="500">
<group
android:scaleX="0.6"
android:scaleY="0.6"
android:translateX="100"
android:translateY="100">
android:scaleX="0.56"
android:scaleY="0.56"
android:translateX="110"
android:translateY="110">
<path
android:fillColor="#EEC170"
android:pathData="m459.87,294.95c0.016,5.4 0.032,10.801 -0.35,16.873c-1.111,6.339 -1.194,12.173 -2.635,17.649c-10.922,41.508 -36.731,69.481 -77.351,83.408c-7.216,2.474 -14.972,3.37 -22.479,4.995c-23.629,0.042 -47.257,0.115 -70.886,0.12c-46.762,0.011 -93.523,-0.014 -140.95,-0.434c-8.59,-2.002 -16.766,-2.835 -24.398,-5.333c-21.595,-7.067 -39.523,-19.656 -53.708,-37.552c-10.227,-12.903 -17.579,-27.17 -21.28,-43.221c-1.475,-6.397 -2.471,-12.904 -3.685,-19.361c-0.052,-5.747 -0.104,-11.494 0.269,-17.886c4.159,-42.973 27.68,-71.638 63.562,-92.153c0,-0.708 -0.002,-1.699 0,-2.69c0.022,-9.829 -1.307,-19.894 0.357,-29.438c3.239,-18.579 11.08,-35.272 23.763,-49.773c12.098,-13.832 26.457,-23.989 43.609,-30.029c7.813,-2.751 16.14,-4.042 24.234,-5.995c7.392,-0.026 14.784,-0.051 22.835,0.323c4.196,0.954 7.795,1.254 11.258,2.105c17.16,4.219 32.287,12.176 45.469,24.104c2.256,2.041 4.372,6.624 9.621,3.868c16.839,-8.842 34.718,-11.597 53.603,-8.594c16.791,2.67 31.602,9.431 44.236,20.636c11.531,10.227 19.84,22.841 25.393,37.236c6.344,16.445 10.389,33.163 6.08,49.389c7.959,8.932 15.807,16.704 22.421,25.414c9.162,12.065 15.33,25.746 18.144,40.776c0.97,5.185 1.911,10.375 2.865,15.563m-71.597,71.012c5.562,-5.228 12.002,-9.799 16.508,-15.817c10.474,-13.992 14.333,-29.916 11.288,-47.446c-2.25,-12.95 -8.197,-24.076 -17.243,-33.063c-12.746,-12.663 -28.865,-18.614 -46.786,-18.569c-69.912,0.177 -139.82,0.568 -209.74,0.962c-15.922,0.09 -29.168,7.421 -39.685,18.296c-14.45,14.944 -20.408,33.343 -16.655,54.368c2.276,12.754 8.217,23.748 17.158,32.66c13.299,13.255 30.097,18.653 48.728,18.651c59.321,-0.005 118.64,0.042 177.96,-0.047c9.591,-0.014 19.181,-0.866 28.773,-0.889c10.649,-0.025 19.978,-3.825 29.687,-9.107z" />

View File

@@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- AliasVault logo scaled and centered with padding -->
<group
android:translateX="23.5"
android:translateY="23.5"
android:scaleX="0.122"
android:scaleY="0.122">
<!-- Main vault shape -->
<path
android:fillColor="#000000"
android:pathData="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.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.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z"/>
<!-- First dot -->
<path
android:fillColor="#000000"
android:pathData="m162.77 293c15.654 4.3883 20.627 22.967 10.304 34.98-5.3104 6.1795-14.817 8.3208-24.278 5.0472-7.0723-2.4471-12.332-10.362-12.876-17.933-1.0451-14.542 11.089-23.176 21.705-23.046 1.5794 0.019287 3.1517 0.61566 5.1461 0.95184z"/>
<!-- Second dot -->
<path
android:fillColor="#000000"
android:pathData="m227.18 293.64c7.8499 2.3973 11.938 8.2143 13.524 15.077 1.8591 8.0439-0.44817 15.706-7.1588 21.121-6.7633 5.4572-14.417 6.8794-22.578 3.1483-8.2972-3.7933-12.836-10.849-12.736-19.438 0.1687-14.497 14.13-25.368 28.948-19.908z"/>
<!-- Third dot -->
<path
android:fillColor="#000000"
android:pathData="m261.57 319.07c-2.495-14.418 4.6853-22.603 14.596-26.108 9.8945-3.4995 23.181 3.4303 26.267 13.779 4.6504 15.591-7.1651 29.064-21.665 28.161-8.5254-0.53088-17.202-6.5094-19.198-15.831z"/>
<!-- Fourth dot -->
<path
android:fillColor="#000000"
android:pathData="m336.91 333.41c-9.0175-4.2491-15.337-14.349-13.829-21.682 3.0825-14.989 13.341-20.304 23.018-19.585 10.653 0.79141 17.93 7.407 19.765 17.547 1.9588 10.824-4.1171 19.939-13.494 23.703-5.272 2.1162-10.091 1.5086-15.46 0.017883z"/>
</group>
</vector>

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<!-- mipmap-anydpi-v26/ic_launcher_round.xml -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AliasVault</string>
<string name="autofill_service_description" translatable="true">AliasVault AutoFill</string>
<string name="aliasvault_icon">AliasVault icon</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>
</resources>

View File

@@ -1,4 +1,5 @@
<resources>
<color name="splashscreen_background">#ffffff</color>
<color name="colorPrimary">#023c69</color>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -3,7 +3,9 @@
<locale android:name="de" />
<locale android:name="en" />
<locale android:name="fi" />
<locale android:name="he" />
<locale android:name="it" />
<locale android:name="nl" />
<locale android:name="uk" />
<locale android:name="zh" />
</locale-config>

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "AliasVault",
"slug": "AliasVault",
"version": "0.23.0",
"version": "0.23.2",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "net.aliasvault.app",

View File

@@ -58,6 +58,13 @@ export default function AddEditCredentialScreen() : React.ReactNode {
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
const { t } = useTranslation();
// Track last generated values to avoid overwriting manual entries
const [lastGeneratedValues, setLastGeneratedValues] = useState<{
username: string | null;
password: string | null;
email: string | null;
}>({ username: null, password: null, email: null });
const { control, handleSubmit, setValue, watch } = useForm<Credential>({
resolver: yupResolver(createCredentialSchema(t)) as Resolver<Credential>,
defaultValues: {
@@ -189,30 +196,56 @@ export default function AddEditCredentialScreen() : React.ReactNode {
const defaultEmailDomain = await dbContext.sqliteClient!.getDefaultEmailDomain();
const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
setValue('Alias.Email', email);
// Check current values
const currentUsername = watch('Username') ?? '';
const currentPassword = watch('Password') ?? '';
const currentEmail = watch('Alias.Email') ?? '';
// Only overwrite email if it's empty or matches the last generated value
if (!currentEmail || currentEmail === lastGeneratedValues.email) {
setValue('Alias.Email', email);
}
setValue('Alias.FirstName', identity.firstName);
setValue('Alias.LastName', identity.lastName);
setValue('Alias.NickName', identity.nickName);
setValue('Alias.Gender', identity.gender);
setValue('Alias.BirthDate', IdentityHelperUtils.normalizeBirthDateForDisplay(identity.birthDate.toISOString()));
// In edit mode, preserve existing username and password if they exist
if (isEditMode && watch('Username')) {
// Keep the existing username in edit mode, so don't do anything here.
} else {
// Use the newly generated username
// Only overwrite username if it's empty or matches the last generated value
if (!currentUsername || currentUsername === lastGeneratedValues.username) {
setValue('Username', identity.nickName);
}
if (isEditMode && watch('Password')) {
// Keep the existing password in edit mode, so don't do anything here.
} else {
// Use the newly generated password
// Only overwrite password if it's empty or matches the last generated value
if (!currentPassword || currentPassword === lastGeneratedValues.password) {
setValue('Password', password);
// Make password visible when newly generated
setIsPasswordVisible(true);
}
}, [isEditMode, watch, setValue, setIsPasswordVisible, initializeGenerators, dbContext.sqliteClient]);
// Update tracking with new generated values
setLastGeneratedValues({
username: identity.nickName,
password: password,
email: email
});
}, [watch, setValue, setIsPasswordVisible, initializeGenerators, dbContext.sqliteClient, lastGeneratedValues, setLastGeneratedValues]);
/**
* Clear all alias fields.
*/
const clearAliasFields = useCallback(() => {
setValue('Alias.FirstName', '');
setValue('Alias.LastName', '');
setValue('Alias.NickName', '');
setValue('Alias.Gender', '');
setValue('Alias.BirthDate', '');
}, [setValue]);
/**
* Check if any alias fields have values.
*/
const hasAliasValues = watch('Alias.FirstName') || watch('Alias.LastName') || watch('Alias.NickName') || watch('Alias.Gender') || watch('Alias.BirthDate');
/**
* Handle the generate random alias button press.
@@ -224,8 +257,13 @@ export default function AddEditCredentialScreen() : React.ReactNode {
} else if (Platform.OS === 'android') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
await generateRandomAlias();
}, [generateRandomAlias]);
if (hasAliasValues) {
clearAliasFields();
} else {
await generateRandomAlias();
}
}, [generateRandomAlias, clearAliasFields, hasAliasValues]);
/**
* Submit the form for either creating or updating a credential.
@@ -355,8 +393,13 @@ export default function AddEditCredentialScreen() : React.ReactNode {
// Generate identity with gender preference
const identity = identityGenerator.generateRandomIdentity(genderPreference);
// Set the username to the identity's nickname
setValue('Username', identity.nickName);
// Only overwrite username if it's empty or matches the last generated value
const currentUsername = watch('Username') ?? '';
if (!currentUsername || currentUsername === lastGeneratedValues.username) {
setValue('Username', identity.nickName);
// Update the tracking for username
setLastGeneratedValues(prev => ({ ...prev, username: identity.nickName }));
}
} catch (error) {
console.error('Error generating random username:', error);
Toast.show({
@@ -445,13 +488,18 @@ export default function AddEditCredentialScreen() : React.ReactNode {
},
generateButton: {
alignItems: 'center',
backgroundColor: colors.primary,
borderRadius: 8,
flexDirection: 'row',
marginBottom: 8,
paddingHorizontal: 12,
paddingVertical: 8,
},
generateButtonPrimary: {
backgroundColor: colors.primary,
},
generateButtonSecondary: {
backgroundColor: colors.textMuted,
},
generateButtonText: {
color: colors.primarySurfaceText,
fontWeight: '600',
@@ -645,11 +693,20 @@ export default function AddEditCredentialScreen() : React.ReactNode {
<View style={styles.section}>
<ThemedText style={styles.sectionTitle}>{t('credentials.alias')}</ThemedText>
<RobustPressable
style={styles.generateButton}
style={[
styles.generateButton,
hasAliasValues ? styles.generateButtonSecondary : styles.generateButtonPrimary
]}
onPress={handleGenerateRandomAlias}
>
<MaterialIcons name="auto-fix-high" size={20} color="#fff" />
<ThemedText style={styles.generateButtonText}>{t('credentials.generateRandomAlias')}</ThemedText>
<MaterialIcons
name={hasAliasValues ? "clear" : "auto-fix-high"}
size={20}
color="#fff"
/>
<ThemedText style={styles.generateButtonText}>
{hasAliasValues ? t('credentials.clearAliasFields') : t('credentials.generateRandomAlias')}
</ThemedText>
</RobustPressable>
<ValidatedFormField
control={control}

View File

@@ -69,17 +69,16 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
if (dbContext.sqliteClient) {
const settings = await dbContext.sqliteClient.getPasswordSettings();
setCurrentSettings(settings);
// Only set slider value from settings if we don't have a password value yet
if (!hasSetInitialLength.current && isNewCredential) {
setSliderValue(settings.Length);
}
// Always set slider value from loaded settings
setSliderValue(settings.Length);
hasSetInitialLength.current = true;
}
} catch (error) {
console.error('Error loading password settings:', error);
}
};
loadSettings();
}, [dbContext.sqliteClient, isNewCredential]);
}, [dbContext.sqliteClient]);
useImperativeHandle(ref, () => ({

View File

@@ -97,7 +97,7 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
setIsCustomDomain(!isKnownDomain);
} else {
setLocalPart(value);
setIsCustomDomain(false);
// Don't reset isCustomDomain here - preserve the current mode
// Set default domain if not already set
if (!selectedDomain && !value.includes('@')) {
@@ -112,6 +112,13 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
// Handle local part changes
const handleLocalPartChange = useCallback((newText: string) => {
// If in custom domain mode, always pass through the full value
if (isCustomDomain) {
onChange(newText);
// Stay in custom domain mode - don't auto-switch back
return;
}
// Check if new value contains '@' symbol, if so, switch to custom domain mode
if (newText.includes('@')) {
setIsCustomDomain(true);
@@ -120,10 +127,11 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
}
setLocalPart(newText);
if (!isCustomDomain && selectedDomain) {
// If the local part is empty, treat the whole field as empty
if (!newText || newText.trim() === '') {
onChange('');
} else if (selectedDomain) {
onChange(`${newText}@${selectedDomain}`);
} else {
onChange(newText);
}
}, [isCustomDomain, selectedDomain, onChange]);
@@ -131,7 +139,12 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const selectDomain = useCallback((domain: string) => {
setSelectedDomain(domain);
const cleanLocalPart = localPart.includes('@') ? localPart.split('@')[0] : localPart;
onChange(`${cleanLocalPart}@${domain}`);
// If the local part is empty, treat the whole field as empty
if (!cleanLocalPart || cleanLocalPart.trim() === '') {
onChange('');
} else {
onChange(`${cleanLocalPart}@${domain}`);
}
setIsCustomDomain(false);
setIsModalVisible(false);
}, [localPart, onChange]);
@@ -141,13 +154,28 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const newIsCustom = !isCustomDomain;
setIsCustomDomain(newIsCustom);
if (!newIsCustom && !value.includes('@')) {
// Switching to domain chooser mode, add default domain
if (newIsCustom) {
// Switching to custom domain mode
// If we have a domain-based value, extract just the local part
if (value && value.includes('@')) {
const [local] = value.split('@');
onChange(local);
setLocalPart(local);
}
} else {
// Switching to domain chooser mode
const defaultDomain = showPrivateDomains && privateEmailDomains[0]
? privateEmailDomains[0]
: PUBLIC_EMAIL_DOMAINS[0];
onChange(`${localPart}@${defaultDomain}`);
setSelectedDomain(defaultDomain);
// Only add domain if we have a local part
if (localPart && localPart.trim()) {
onChange(`${localPart}@${defaultDomain}`);
} else if (value && !value.includes('@')) {
// If we have a value without @, add the domain
onChange(`${value}@${defaultDomain}`);
}
}
}, [isCustomDomain, value, localPart, showPrivateDomains, privateEmailDomains, onChange]);

View File

@@ -5,16 +5,20 @@ import { initReactI18next } from 'react-i18next';
import de from './locales/de.json';
import en from './locales/en.json';
import fi from './locales/fi.json';
import he from './locales/he.json';
import it from './locales/it.json';
import nl from './locales/nl.json';
import uk from './locales/uk.json';
import zh from './locales/zh.json';
const resources = {
de: { translation: de },
en: { translation: en },
fi: { translation: fi },
he: { translation: he },
nl: { translation: nl },
it: { translation: it },
uk: { translation: uk },
zh: { translation: zh },
};

View File

@@ -109,6 +109,7 @@
"randomAlias": "Random Alias",
"manual": "Manual",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"enterFullEmail": "Enter full email address",
"enterEmailPrefix": "Enter email prefix",
"useDomainChooser": "Use domain chooser",

View File

@@ -109,6 +109,7 @@
"randomAlias": "Zufälliger Alias",
"manual": "Manuell",
"generateRandomAlias": "Zufällige Alias generieren",
"clearAliasFields": "Alias-Felder löschen",
"enterFullEmail": "Vollständige E-Mail-Adresse eingeben",
"enterEmailPrefix": "E-Mail-Präfix eingeben",
"useDomainChooser": "Domain-Auswahl verwenden",

View File

@@ -109,6 +109,7 @@
"randomAlias": "Random Alias",
"manual": "Manual",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"enterFullEmail": "Enter full email address",
"enterEmailPrefix": "Enter email prefix",
"useDomainChooser": "Use domain chooser",

View File

@@ -109,6 +109,7 @@
"randomAlias": "Random Alias",
"manual": "Manual",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"enterFullEmail": "Enter full email address",
"enterEmailPrefix": "Enter email prefix",
"useDomainChooser": "Use domain chooser",

View File

@@ -109,6 +109,7 @@
"randomAlias": "Satunnainen Alias",
"manual": "Käyttöopas",
"generateRandomAlias": "Luo satunnainen alias",
"clearAliasFields": "Tyhjennä aliaksen kentät",
"enterFullEmail": "Syötä täysi sähköpostiosoite",
"enterEmailPrefix": "Syötä sähköpostin etuliite",
"useDomainChooser": "Käytä verkkotunnuksen valintaa",

View File

@@ -109,6 +109,7 @@
"randomAlias": "Random Alias",
"manual": "Manual",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"enterFullEmail": "Enter full email address",
"enterEmailPrefix": "Enter email prefix",
"useDomainChooser": "Use domain chooser",

View File

@@ -1,7 +1,7 @@
{
"common": {
"cancel": "ביטול",
"close": "Close",
"close": "סגירה",
"delete": "מחיקה",
"save": "שמירה",
"yes": "כן",
@@ -15,8 +15,8 @@
"copied": "הועתק ללוח הגזירים",
"loadMore": "לטעון עוד",
"use": "להשתמש",
"confirm": "Confirm",
"unknownError": "Unknown error"
"confirm": "אישור",
"unknownError": "שגיאה לא ידועה"
},
"auth": {
"login": "כניסה",
@@ -109,6 +109,7 @@
"randomAlias": "כינוי אקראי",
"manual": "ידני",
"generateRandomAlias": "יצירת כינוי אקראי",
"clearAliasFields": "Clear Alias Fields",
"enterFullEmail": "נא למלא כתובת דוא״ל מלאה",
"enterEmailPrefix": "נא למלא קידומת דוא״ל",
"useDomainChooser": "להשתמש בבורר שמות התחום",
@@ -219,17 +220,17 @@
"batteryOptimizationHelpDescription": "מיטוב הסוללה של Android מונע פינוי אמין של לוח הגזירים כשהיישום פועל ברקע. השבתת מיטוב הסוללה ל־AliasVault מאפשרת פינוי מדויק של לוח הגזירים ברקע ומאשרת אוטומטית הרשאות התראות הכרחיות.",
"disableBatteryOptimization": "השבתת מיטוב סוללה",
"identityGenerator": "מייצר זהויות",
"passwordGenerator": "Password Generator",
"importExport": "Import / Export",
"importSectionTitle": "Import",
"importSectionDescription": "Import your passwords from other password managers or from a previous AliasVault export.",
"importWebNote": "To import credentials from existing password managers, please login to the web app. The import feature is currently only available on the web version.",
"exportSectionTitle": "Export",
"exportSectionDescription": "Export your vault data to a CSV file. This file can be used as a back-up and can also be imported into other password managers.",
"exportCsvButton": "Export vault to CSV file",
"exporting": "Exporting...",
"exportConfirmTitle": "Export Vault",
"exportWarning": "Warning: Exporting your vault to an unencrypted file will expose all of your passwords and sensitive information in plain text. Only do this on trusted devices and ensure you:\n\n• Store the exported file in a secure location\n• Delete the file when you no longer need it\n• Never share the exported file with others\n\nAre you sure you want to continue with the export?",
"passwordGenerator": "יוצר סיסמאות",
"importExport": "ייבוא / ייצוא",
"importSectionTitle": "ייבוא",
"importSectionDescription": "אפשר לייבא את הסיסמאות שלך ממנהלי סיסמאות אחרים או מנתונים שיוצאו מ־AliasVault בעבר.",
"importWebNote": "כדי לייבא פרטי גישה ממנהלי סיסמאות קיימים, נא להיכנס דרך יישום הדפדפן. יכולת הייבוא זמינה רק במהדורת הדפדפן.",
"exportSectionTitle": "ייצוא",
"exportSectionDescription": "ייצוא נתוני הכספת שלך לקובץ CSV. אפשר להשתמש בקובץ הזה כגיבוי וגם לייבא אותו למנהלי סיסמאות אחרים.",
"exportCsvButton": "ייצוא כספת לקובץ CSV",
"exporting": "מתבצע ייצוא…",
"exportConfirmTitle": "ייצוא כספת",
"exportWarning": "אזהרה: ייצוא הכספת שלך לקובץ לא מוצפן תחשוף את כל הסיסמאות והפרטים הרגישים שלך בטקסט גלוי. יש לעשות זאת על מכשירים מהימנים וגם לוודא:\n\n• שאחסנת את הקובץ המיוצר במקום בטוח\n• שמחקת את הקובץ אם אין בו צורך עוד\n לעולם לא לשתף את הקובץ המיוצא עם אחרים\n\nלהמשיך בייצוא?",
"security": "אבטחה",
"appVersion": "גרסת היישום היא {{version}} ({{url}})",
"autoLockOptions": {
@@ -286,8 +287,8 @@
}
},
"passwordGeneratorSettings": {
"description": "Configure the default settings used when generating new passwords. These settings will be used for all new passwords unless overridden for specific entries.",
"preview": "Preview"
"description": "הגדרת תצורת ברירת המחדל לשימוש בעת יצירת סיסמאות חדשות. ההגדרות האלו תשמשנה לכל הסיסמאות החדשות אלא אם כן ההגדרות האלו נדרסו עבור רשומות מסוימות.",
"preview": "תצוגה מקדימה"
},
"securitySettings": {
"title": "אבטחה",
@@ -439,9 +440,9 @@
"retryingConnection": "מתבצע ניסיון להתחבר מחדש…"
},
"offline": {
"banner": "Offline mode (read-only)",
"backOnline": "Back online",
"stillOffline": "Still offline"
"banner": "מצב בלתי מקוון (קריאה בלבד)",
"backOnline": "להתחבר בחזרה",
"stillOffline": "עדיין בלתי מקוון"
},
"alerts": {
"syncIssue": "תקלת סנכרון",

View File

@@ -109,6 +109,7 @@
"randomAlias": "Alias casuale",
"manual": "Manuale",
"generateRandomAlias": "Genera alias casuale",
"clearAliasFields": "Cancella Campi Alias",
"enterFullEmail": "Inserisci l'indirizzo email completo",
"enterEmailPrefix": "Inserisci prefisso email",
"useDomainChooser": "Usa selettore di dominio",

View File

@@ -109,6 +109,7 @@
"randomAlias": "Alias",
"manual": "Handmatig",
"generateRandomAlias": "Genereer willekeurige alias",
"clearAliasFields": "Leeg alias velden",
"enterFullEmail": "Voer volledig e-mailadres in",
"enterEmailPrefix": "Emailvoorvoegsel invoeren",
"useDomainChooser": "Domein kiezen",

View File

@@ -0,0 +1,507 @@
{
"common": {
"cancel": "Cancelar",
"close": "Fechar",
"delete": "Excluir",
"save": "Salvar",
"yes": "Sim",
"no": "Não",
"ok": "Ok",
"continue": "Continuar",
"loading": "Carregando...",
"error": "Erro",
"success": "Sucesso",
"never": "Nunca",
"copied": "Copiado para a área de transferência",
"loadMore": "Carregar mais",
"use": "Utilizar",
"confirm": "Confirmar",
"unknownError": "Erro desconhecido"
},
"auth": {
"login": "Login",
"logout": "Sair",
"username": "Usuário ou e-mail",
"password": "Senha",
"authCode": "Código de Autenticação",
"unlock": "Desbloquear",
"unlocking": "Desbloqueando...",
"loggingIn": "Fazendo login",
"validatingCredentials": "Validando credenciais",
"syncingVault": "Sincronizando cofre",
"verifyingAuthCode": "Verificando código de autenticação",
"verify": "Verificar",
"unlockVault": "Desbloquear cofre",
"enterPassword": "Digite sua senha para desbloquear o cofre",
"enterPasswordPlaceholder": "Senha",
"enterAuthCode": "Digite o código de 6 dígitos",
"usernamePlaceholder": "nome / nome@empresa.com",
"passwordPlaceholder": "Digite sua senha",
"enableBiometric": "Habilitar {{biometric}}?",
"biometricPrompt": "Gostaria de utilizar {{biometric}} para desbloquear seu cofre?",
"tryBiometricAgain": "Tente novamente com {{biometric}}",
"authCodeNote": "Nota: se você não tem acesso ao seu aparelho de verificação, você pode resetar seu 2FA com um código de recuperação fazendo login no site.",
"errors": {
"credentialsRequired": "Usuário e senha são obrigatórios",
"invalidAuthCode": "Por favor digite o código de autenticação de 6 dígitos",
"incorrectPassword": "Senha incorreta. Por favor tente novamente.",
"enterPassword": "Por favor digite sua senha",
"serverError": "Não foi possível conectar ao servidor do AliasVault. Por favor tente novamente mais tarde ou entre em contato com o suporte caso o problema persista.",
"serverErrorSelfHosted": "Não foi possível conectar à API. Para instâncias self-hosted, por favor verifique se os endpoints da API podem ser chamados através de um navegador. Ele deve mostrar 'OK'.",
"networkError": "Conexão falhou. Por favor verifique sua conexão com a internet e tente novamente.",
"networkErrorSelfHosted": "Conexão falhou. Verifique sua conexão com a rede e a disponibilidade do servidor. Para instâncias self-hosted, por favor confirme que possue um certificado SSL válido instalado. Certificados self-signed não são suportados em celulares por questões de segurança.",
"sessionExpired": "Sua sessão expirou. Por favor faça login novamente.",
"tokenRefreshFailed": "Falha ao atualizar token de autenticação",
"httpError": "Erro HTTP: {{status}}"
},
"confirmLogout": "Tem certeza que deseja sair? Você precisará fazer login novamente com sua senha mestre para acessar o cofre.",
"noAccountYet": "Não tem conta ainda?",
"createNewVault": "Criar novo cofre",
"connectingTo": "Conectando à",
"loggedInAs": "Logado como"
},
"vault": {
"syncingVault": "Sincronizando cofre",
"uploadingVaultToServer": "Fazendo upload do cofre para o servidor",
"savingChangesToVault": "Salvando mudanças no cofre",
"checkingForVaultUpdates": "Verificando atualizações do cofre",
"executingOperation": "Realizando operação...",
"checkingVaultUpdates": "Verificando atualizações do cofre",
"syncingUpdatedVault": "Sincronizando cofre atualizado",
"errors": {
"failedToGetEncryptedDatabase": "Falha ao acessar dados criptografados",
"usernameNotFound": "Usuário não encontrado",
"vaultMergeRequired": "Junção de cofres necessária. Por favor realize o login via site para adicionar as atualizações pendentes ao cofre.",
"vaultOutdated": "Seu cofre está desatualizado. Por favor realize login pelo site do AliasVault e siga as instruções.",
"failedToUploadVault": "Falha ao enviar cofre ao servidor. Por favor tente novamente reabrindo o aplicativo.",
"usernameNotFoundLoginAgain": "Usuário não encontrado. Por favor realize login novamente.",
"errorDuringPasswordChange": "Erro ao atualizar a senha. Por favor realize login novamente para recuperar seu último cofre.",
"failedToSyncVault": "Falha ao sincronizar cofre",
"operationFailed": "Operação falhou",
"versionNotSupported": "Esta versão do aplicativo AliasVault não é mais suportada pelo servidor. Por favor atualize seu aplicativo para a última versão.",
"serverNeedsUpdate": "O servidor do AliasVault precisa ser atualizado para a última versão para poder utilizar este aplicativo. Por favor entre em contato com o suporte se precisar de ajuda.",
"vaultDecryptFailed": "Cofre não pôde ser descriptografado, se o problema persistir por favor saia e realize login novamente.",
"passwordChanged": "Sua senha mudou desde o último login. Por favor realize login novamente por questões de segurança."
}
},
"credentials": {
"title": "Credenciais",
"addCredential": "Adicionar Credencial",
"editCredential": "Editar Credencial",
"deleteCredential": "Excluir Credencial",
"deleteConfirm": "Tem certeza que deseja excluir esta credencial? Essa operação não pode ser desfeita.",
"service": "Serviço",
"serviceName": "Nome do Serviço",
"serviceUrl": "URL do Serviço",
"loginCredentials": "Credenciais de login",
"username": "Usuário",
"email": "E-mail",
"alias": "Alias",
"metadata": "Metadados",
"firstName": "Primeiro Nome",
"lastName": "Sobrenome",
"nickName": "Apelido",
"fullName": "Nome Completo",
"gender": "Gênero",
"birthDate": "Data de Nascimento",
"birthDatePlaceholder": "AAAA-MM-DD",
"notes": "Notas",
"randomAlias": "Alias Aleatório",
"manual": "Manual",
"generateRandomAlias": "Gerar Alias Aleatório",
"clearAliasFields": "Limpar Campos de Alias",
"enterFullEmail": "Digite o endereço de e-mail completo",
"enterEmailPrefix": "Digite o prefixo do e-mail",
"useDomainChooser": "Utilizar escolhedor de domínio",
"enterCustomDomain": "Digitar domínio personalizado",
"selectEmailDomain": "Selecionar domínio de e-mail",
"privateEmailTitle": "E-mail privado",
"privateEmailAliasVaultServer": "Servidor AliasVault",
"privateEmailDescription": "Criptografia E2E, totalmente privado.",
"publicEmailTitle": "Provedores Públicos de E-mail Temporário",
"publicEmailDescription": "Anônimo mas com privacidade limitada. Conteúdo do e-mail pode ser lido por qualquer um que souber o endereço.",
"searchPlaceholder": "Pesquisar credenciais...",
"noMatchingCredentials": "Nenhuma credencial foi encontrada",
"noCredentialsFound": "Nenhuma credencial encontrada. Crie uma para iniciar. Dica: você também pode fazer login no site do AliasVault e importar credenciais de outros gerenciadores de senhas.",
"recentEmails": "E-mails recentes",
"loadingEmails": "Carregando emails...",
"noEmailsYet": "Nenhum e-mail recebido ainda.",
"offlineEmailsMessage": "Você está offline. Por favor reconecte à internet para carregar seus e-mails.",
"emailLoadError": "Ocorreu um erro ao carregar os e-mails. Por favor tente novamente mais tarde.",
"emailUnexpectedError": "Ocorreu um erro inesperado ao carregar e-mails. Por favor tente novamente mais tarde.",
"password": "Senha",
"passwordLength": "Tamanho da Senha",
"changePasswordComplexity": "Configurações de Senha",
"includeLowercase": "Minúsculas (a-z)",
"includeUppercase": "Maiúsculas (A-Z)",
"includeNumbers": "Números (0-9)",
"includeSpecialChars": "Caracteres Especiais (!@#)",
"avoidAmbiguousChars": "Evitar Caracteres Ambíguos",
"deletingCredential": "Excluindo credencial...",
"errorLoadingCredentials": "Erro ao carregar credenciais",
"vaultSyncFailed": "Sincronização do cofre falhou",
"vaultSyncedSuccessfully": "Cofre sincronizado com sucesso",
"vaultUpToDate": "Cofre está atualizado",
"offlineMessage": "Você está offline. Por favor conecte-se à internet para sincronizar seu cofre.",
"credentialCreated": "Credencial Criada!",
"credentialCreatedMessage": "Sua nova credencial foi adicionada ao seu cofre e está pronta para ser usada.",
"credentialDetails": "Detalhes da Credencial",
"emailPreview": "Prévia de E-mail",
"switchBackToBrowser": "Volte ao navegador para continuar.",
"twoFactorAuth": "Autenticação de dois fatores",
"totpCode": "Código TOTP",
"attachments": "Anexos",
"loadingAttachments": "Carregando anexos...",
"addAttachments": "Adicionar Anexo",
"deleteAttachment": "Excluir",
"toasts": {
"credentialUpdated": "Credencial atualizada com sucesso",
"credentialCreated": "Credencial criada com sucesso",
"credentialDeleted": "Credencial excluída com sucesso"
},
"createNewAliasFor": "Criar novo alias para",
"errors": {
"loadFailed": "Falha ao carregar crerencial",
"generateUsernameFailed": "Falha ao gerar usuário",
"generatePasswordFailed": "Falha ao gerar senha"
},
"contextMenu": {
"title": "Opções da Credencial",
"edit": "Editar",
"delete": "Excluir",
"copyUsername": "Copiar Usuário",
"copyEmail": "Copiar E-mail",
"copyPassword": "Copiar Senha"
}
},
"settings": {
"title": "Configurações",
"iosAutofill": "Autopreenchimento no iOS",
"iosAutofillSettings": {
"headerText": "Você pode configurar o AliasVault para preencher senhas nativamente pelo iOS. Siga as instruções abaixo para habilitar.",
"howToEnable": "Como habilitar:",
"step1": "1. Abra as Configurações do iOS através do botão abaixo",
"step2": "2. Vá até \"Geral\"",
"step3": "3. Clique \"Autopreenchimento & Senhas\"",
"step4": "4. Habilite \"AliasVault\"",
"step5": "5. Desabilite outros provedores de senha (ex. \"iCloud Passwords\") para evitar conflitos",
"openIosSettings": "Abrir Configurações do iOS",
"alreadyConfigured": "Já configurei",
"warningText": "Nota: Você terá que autenticar com Face ID/Touch ID ou a senha do seu dispositivo quando utilizar o autopreenchimento."
},
"androidAutofill": "Autopreenchimento no Android",
"androidAutofillSettings": {
"warningTitle": "⚠️ Funcionalidade Experimental",
"warningDescription": "Autopreenchimento no Android está atualmente em fase experimental.",
"warningLink": "Leia mais sobre isso aqui",
"headerText": "Você pode configurar o AliasVault para preencher senhas nativamente no Android. Siga as instruções abaixo para habilitar.",
"howToEnable": "Como habilitar:",
"step1": "1. Abra as Configurações do Android através do botão abaixo, e troque o \"serviço de autopreenchimento preferido\" para \"AliasVault\"",
"openAutofillSettings": "Abrir Configurações de Autopreenchimento",
"buttonTip": "Se o botão acima não funcionar pode estar bloqueado pelas configurações de segurança. Você pode ir manualmente às Configurações do Android → Configurações Gerais → Senhas e autopreenchimento.",
"step2": "2. Alguns aplicativos, ex. Google Chrome, podem solicitar configurações manuais nas suas configurações para habilitar autopreenchimento de aplicativos terceiros. Porém, a maioria dos aplicativos deve funcionar com autopreenchimento por padrão.",
"alreadyConfigured": "Já configurei"
},
"vaultUnlock": "Método de Desbloqueio do Cofre",
"autoLock": "Tempo para Bloqueio Automático",
"clipboardClear": "Limpar área de transferência",
"clipboardClearDescription": "Limpa automaticamente as senhas e dados sensíveis copiados da sua área de transferência após o tempo especificado.",
"clipboardClearAndroidWarning": "Nota: alguns dispositivos Android têm histórico de área de transferência habilitado, o que pode manter dados copiados anteriormente, mesmo após o AliasVault limpar a área de transferência. O AliasVault só pode remover o item mais recente, porém outros itens podem permanecer visíveis no histórico. Por questões de segurança, recomendamos desabilitar qualquer histórico de área de transferência nas configurações do seu dispositivo.",
"clipboardClearOptions": {
"never": "Nunca",
"5seconds": "5 segundos",
"10seconds": "10 segundos",
"15seconds": "15 segundos",
"30seconds": "30 segundos"
},
"batteryOptimizationHelpTitle": "Habilitar Limpeza de Área de Transferência no Background",
"batteryOptimizationActive": "Otimização de bateria está bloqueando tarefas em background",
"batteryOptimizationDisabled": "Limpeza da área de transferência em background habilitada",
"batteryOptimizationHelpDescription": "A otimização de bateria do Android previne a limpeza correta da área de transferência enquanto o aplicativo está em background. Desabilitar a otimização de bateria para o AliasVault permite a limpeza correta da área de transferência em background e automaticamente autoriza as permissões de alarme.",
"disableBatteryOptimization": "Desabilitar otimização de bateria",
"identityGenerator": "Gerador de Identidade",
"passwordGenerator": "Gerador de Senha",
"importExport": "Importar / Exportar",
"importSectionTitle": "Importar",
"importSectionDescription": "Importe suas senhas de outros gerenciadores de senhas ou de dados exportados anteriormente do AliasVault.",
"importWebNote": "Para importar credenciais de um gerenciador de senhas existente, por favor realize o login pelo site. Por enquanto, a função de importar só está disponível pela versão web.",
"exportSectionTitle": "Exportar",
"exportSectionDescription": "Exporte seu cofre para um arquivo CSV. Este arquivo pode ser utilizado como backup e também pode ser importado em outros gerenciadores de senhas.",
"exportCsvButton": "Exportar cofre para arquivo CSV",
"exporting": "Exportando...",
"exportConfirmTitle": "Exportar Cofre",
"exportWarning": "Aviso: Exportar seu cofre para um arquivo descriptografado vai expor todas as suas senhas e dados sensíveis em texto. Realize isso apenas em dispositivos confiados e garanta que:\n\n• Guarde o arquivo em um local seguro\n• Exclua o arquivo quando não precisar mais\n• Nunca compartilhe o arquivo exportado com outras pessoas\n\nTem certeza que deseja continuar com o exporte?",
"security": "Segurança",
"appVersion": "Versão do aplicativo {{version}} ({{url}})",
"autoLockOptions": {
"never": "Nunca",
"5seconds": "5 segundos",
"30seconds": "30 segundos",
"1minute": "1 minuto",
"15minutes": "15 minutos",
"30minutes": "30 minutos",
"1hour": "1 hora",
"4hours": "4 horas",
"8hours": "8 horas"
},
"language": "Idioma",
"languageSystemMessage": "Para mudar o idioma do aplicativo, configure o idioma preferido para o AliasVault nas configurações do seu dispositivo.",
"openSettings": "Abrir Configurações",
"vaultUnlockSettings": {
"description": "Escolha como você quer desbloquear seu cofre.",
"biometrics": "Biometria",
"faceId": "Face ID",
"touchId": "Touch ID",
"faceIdTouchId": "Face ID / Touch ID",
"biometricEnabled": "{{biometric}} foi ativado com sucesso",
"biometricNotAvailable": "{{biometric}} Não Disponível",
"biometricDisabledMessage": "{{biometric}} está desabitado para o AliasVault. Para utilizar, por favor primeiro habilite nas configurações do seu dispositivo.",
"biometricHelp": "A chave de descriptografia do seu cofre será salva seguramente no seu dispositivo local na {{keystore}} e pode ser acessada com {{biometric}}.",
"biometricUnavailableHelp": "{{biometric}} não está disponível. Clique para abrir as configurações e/ou vá às configurações do seu dispositivo para habilitar e configurar.",
"passwordHelp": "Re-digite sua senha mestre para desbloquear seu cofre. Isso sempre estará disponível como uma opção alternativa.",
"keystoreIOS": "iOS Keychain",
"keystoreAndroid": "Android Keystore"
},
"autoLockSettings": {
"description": "Escolha por quanto tempo o aplicativos pode ficar em background antes de solicitar re-autenticação. Você precisará usar o Face ID ou digitar sua senha para desbloquear seu cofre novamente."
},
"identityGeneratorSettings": {
"description": "Configure o idioma padrão e preferência de gênero para geração de novas identidades.",
"languageSection": "Idioma",
"languageDescription": "Defina o idioma que será utilizado ao gerar novas identidades.",
"genderSection": "Gênero",
"genderDescription": "Defina a preferência de gênero para geração de novas identidades.",
"languageOptions": {
"english": "Inglês",
"dutch": "Holandês"
},
"genderOptions": {
"random": "Aleatório",
"male": "Masculino",
"female": "Feminino"
},
"errors": {
"loadFailed": "Falha ao carregar configurações do gerador.",
"languageUpdateFailed": "Falha ao atualizar configurações de idioma.",
"genderUpdateFailed": "Falha ao atualizar configurações de gênero."
}
},
"passwordGeneratorSettings": {
"description": "Configure as configurações padrão usadas para gerar novas senhas. Estas configurações serão usadas para todas as novas senhas a menos que seja sobrescrito para itens específicos.",
"preview": "Prévia"
},
"securitySettings": {
"title": "Segurança",
"description": "Gerencie as configurações de sua conta e de segurança do seu cofre.",
"changeMasterPassword": "Alterar Senha Mestre",
"activeSessionsTitle": "Sessões Ativas",
"recentAuthLogs": "Logs de Autenticação Recente",
"deleteAccountTitle": "Excluir Conta",
"changePassword": {
"headerText": "Alterar sua senha mestre também irá alterar as chaves de criptografia do cofre. É aconselhável alterar sua senha mestre periodicamente para manter seu cofre seguro.",
"currentPassword": "Senha Atual",
"newPassword": "Nova Senha",
"confirmNewPassword": "Confirmar Nova Senha",
"enterCurrentPassword": "Digite a senha atual",
"enterNewPassword": "Digite a nova senha",
"changePassword": "Mudar Senha",
"fillAllFields": "Por favor preencha todos os campos",
"passwordsDoNotMatch": "Novas senhas não são iguais",
"userNotAuthenticated": "Usuário não está autenticado",
"initiatingChange": "Iniciando troca de senha...",
"currentPasswordIncorrect": "Senha atual incorreta",
"passwordChangedSuccessfully": "Senha atualizada com sucesso",
"failedToChange": "Falha ao atualizar senha. Por favor tente novamente."
},
"activeSessions": {
"headerText": "Abaixo está uma lista de dispositivos onde sua conta está atualmente logada ou tem uma sessão ativa. Você pode deslogar de qualquer uma dessas sessões por aqui.",
"noSessions": "Nenhuma sessão ativa",
"revoke": "Revogar",
"revokeSession": "Revogar Sessão",
"revokeConfirmation": "Tem certeza que quer revogar esta sessão? Isso vai deslogar sua conta do dispositivo selecionado.",
"sessionRevoked": "Sessão revogada com sucesso",
"failedToRevoke": "Falha ao revogar sessão",
"failedToLoad": "Falha ao carregar sessões ativas",
"lastActive": "Última atividade",
"expires": "Expira"
},
"authLogs": {
"headerText": "Abaixo você pode ter uma visão de tentativas de login recentes na sua conta.",
"noLogs": "No auth logs found",
"success": "Success",
"failed": "Failed",
"time": "Time",
"device": "Device",
"ipAddress": "IP Address",
"client": "Client",
"failedToLoad": "Failed to load auth logs"
},
"deleteAccount": {
"headerText": "Deleting your account will immediately and permanently delete all of your data.",
"warningText": "Warning: This action cannot be undone. All your data will be permanently deleted.",
"finalWarning": "Final warning: Enter your password to permanently delete your account.",
"warningVaults": "All encrypted vaults which includes all of your credentials will be permanently deleted",
"warningAliases": "Your email aliases will be orphaned and cannot be claimed by other users",
"warningRecovery": "Your account cannot be recovered after deletion",
"irreversibleWarning": "Account deletion is irreversible and cannot be undone. Pressing the button below will delete your account immediately and permanently.",
"enterUsername": "Enter your username to continue",
"password": "Password",
"enterPassword": "Enter password",
"deleteAccount": "Delete Account",
"confirmationMessage": "Are you absolutely sure you want to delete your account? This action cannot be undone.",
"usernameDoesNotMatch": "Username does not match",
"verifyingPassword": "Verifying password...",
"currentPasswordIncorrect": "Current password is not correct",
"initiatingDeletion": "Initiating account deletion",
"verifyingWithServer": "Verifying with server",
"deletingAccount": "Deleting account",
"accountDeleted": "Account deleted successfully",
"failedToDelete": "Failed to delete account. Please try again.",
"usernameNotFound": "Username not found. Please login again."
}
}
},
"navigation": {
"credentials": "Credentials",
"emails": "Emails",
"settings": "Settings"
},
"emails": {
"title": "Emails",
"emailDetails": "Email Details",
"subject": "Subject:",
"date": "Date:",
"from": "From:",
"to": "To:",
"attachments": "Attachments",
"deleteEmail": "Delete Email",
"deleteEmailConfirm": "Are you sure you want to delete this email? This action is permanent and cannot be undone.",
"emailNotFound": "Email not found",
"noPlainText": "This email does not contain any plain-text.",
"sizeKB": "KB",
"offlineMessage": "You are offline. Please connect to the internet to load your emails.",
"emptyMessage": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
"time": {
"justNow": "just now",
"minutesAgo_single": "{{count}} min ago",
"minutesAgo_plural": "{{count}} mins ago",
"hoursAgo_single": "{{count}} hr ago",
"hoursAgo_plural": "{{count}} hrs ago",
"yesterday": "yesterday"
},
"errors": {
"generic": "An error occurred",
"loadFailed": "Failed to load emails",
"deleteFailed": "Failed to delete email",
"dbNotAvailable": "Database context or email not available",
"decryptFailed": "Failed to decrypt attachment",
"downloadFailed": "Failed to download attachment"
}
},
"validation": {
"required": "This field is required",
"serviceNameRequired": "Service name is required",
"invalidDateFormat": "Date must be in YYYY-MM-DD format",
"invalidEmailFormat": "Invalid email format"
},
"apiErrors": {
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again.",
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
"USERNAME_REQUIRED": "Username is required.",
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
"USERNAME_AVAILABLE": "Username is available.",
"USERNAME_MISMATCH": "Username does not match the current user.",
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
"USERNAME_INVALID_EMAIL": "Invalid email address.",
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
"INTERNAL_SERVER_ERROR": "Internal server error.",
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
},
"app": {
"status": {
"unlockingVault": "Unlocking vault",
"decryptingVault": "Decrypting vault",
"openingVaultReadOnly": "Opening vault in read-only mode",
"retryingConnection": "Retrying connection..."
},
"offline": {
"banner": "Offline mode (read-only)",
"backOnline": "Back online",
"stillOffline": "Still offline"
},
"alerts": {
"syncIssue": "Sync Issue",
"syncIssueMessage": "The AliasVault server could not be reached and your vault could not be synced. Would you like to open your local vault in read-only mode or retry the connection?",
"openLocalVault": "Open Local Vault",
"retrySync": "Retry Sync"
},
"navigation": {
"login": "Login",
"loginSettings": "Login Settings",
"notFound": "Not Found"
},
"notFound": {
"title": "Page not found",
"message": "This page has been moved or deleted.",
"goHome": "Go back to the start"
},
"appName": "AliasVault",
"reinitialize": {
"vaultAutoLockedMessage": "Vault auto-locked after timeout.",
"attemptingToUnlockMessage": "Attempting to unlock."
},
"loginSettings": {
"title": "API Connection",
"aliasvaultNet": "Aliasvault.net",
"selfHosted": "Self-hosted",
"customApiUrl": "Custom API URL",
"customApiUrlPlaceholder": "https://my-aliasvault-instance.com/api",
"version": "Version: {{version}}"
}
},
"upgrade": {
"title": "Upgrade Vault",
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
"versionInformation": "Version Information",
"yourVault": "Your vault:",
"newVersion": "New version:",
"upgrade": "Upgrade",
"upgrading": "Upgrading...",
"logout": "Logout",
"whatsNew": "What's New",
"whatsNewDescription": "An upgrade is required to support the following changes:",
"noDescriptionAvailable": "No description available for this version.",
"status": {
"preparingUpgrade": "Preparing upgrade...",
"vaultAlreadyUpToDate": "Vault is already up to date",
"startingDatabaseTransaction": "Starting database transaction...",
"applyingDatabaseMigrations": "Applying database migrations...",
"applyingMigration": "Applying migration {{current}} of {{total}}...",
"committingChanges": "Committing changes..."
},
"alerts": {
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
"selfHostedServer": "Self-Hosted Server",
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
"continueUpgrade": "Continue Upgrade",
"upgradeFailed": "Upgrade Failed",
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
}
}
}

View File

@@ -1,7 +1,7 @@
{
"common": {
"cancel": "Отмена",
"close": "Close",
"close": "Закрыть",
"delete": "Удалить",
"save": "Сохранить",
"yes": "Да",
@@ -15,8 +15,8 @@
"copied": "Скопировано в буфер обмена",
"loadMore": "Загрузить ещё",
"use": "Использовать",
"confirm": "Confirm",
"unknownError": "Unknown error"
"confirm": "Подтвердить",
"unknownError": "Неизвестная ошибка"
},
"auth": {
"login": "Войти",
@@ -109,6 +109,7 @@
"randomAlias": "Случайный псевдоним",
"manual": "Инструкция",
"generateRandomAlias": "Сгенерировать случайный псевдоним",
"clearAliasFields": "Очистить поля псевдонимов",
"enterFullEmail": "Введите полный адрес электронной почты",
"enterEmailPrefix": "Введите префикс электронной почты",
"useDomainChooser": "Использовать выбор домена",
@@ -219,17 +220,17 @@
"batteryOptimizationHelpDescription": "Оптимизация заряда батареи в Android предотвращает надежную очистку буфера обмена, когда приложение работает в фоновом режиме. Отключение оптимизации заряда батареи для AliasVault обеспечивает точную очистку буфера обмена в фоновом режиме и автоматически предоставляет необходимые разрешения для оповещения.",
"disableBatteryOptimization": "Отключить оптимизацию заряда батареи",
"identityGenerator": "Генератор личности",
"passwordGenerator": "Password Generator",
"importExport": "Import / Export",
"importSectionTitle": "Import",
"importSectionDescription": "Import your passwords from other password managers or from a previous AliasVault export.",
"importWebNote": "To import credentials from existing password managers, please login to the web app. The import feature is currently only available on the web version.",
"exportSectionTitle": "Export",
"exportSectionDescription": "Export your vault data to a CSV file. This file can be used as a back-up and can also be imported into other password managers.",
"exportCsvButton": "Export vault to CSV file",
"exporting": "Exporting...",
"exportConfirmTitle": "Export Vault",
"exportWarning": "Warning: Exporting your vault to an unencrypted file will expose all of your passwords and sensitive information in plain text. Only do this on trusted devices and ensure you:\n\n• Store the exported file in a secure location\n• Delete the file when you no longer need it\n• Never share the exported file with others\n\nAre you sure you want to continue with the export?",
"passwordGenerator": "Генератор паролей",
"importExport": "Импорт / экспорт",
"importSectionTitle": "Импорт",
"importSectionDescription": "Импортируйте свои пароли из других менеджеров паролей или из предыдущего экспорта AliasVault.",
"importWebNote": "Импорт из других менеджеров паролей доступен только в веб‑версии. Пожалуйста, войдите в веб‑приложение.",
"exportSectionTitle": "Экспорт",
"exportSectionDescription": "Экспортируйте данные вашего хранилища в CSV-файл. Этот файл можно использовать как резервную копию или импортировать в другие менеджеры паролей.",
"exportCsvButton": "Экспорт хранилища в CSV-файл",
"exporting": "Экспорт...",
"exportConfirmTitle": "Экспортировать хранилище",
"exportWarning": "Предупреждение: Экспорт хранилища в незашифрованный файл приведёт к тому, что все ваши пароли и конфиденциальная информация будут доступны в открытом виде. Делайте это только на доверенных устройствах и убедитесь, что вы:\n\n• Храните экспортированный файл в безопасном месте\n• Удаляете файл, когда он больше не нужен\n• Никогда не передаёте файл другим людям\n\nВы уверены, что хотите продолжить экспорт?",
"security": "Безопасность",
"appVersion": "Версия приложения {{version}} ({{url}})",
"autoLockOptions": {
@@ -286,8 +287,8 @@
}
},
"passwordGeneratorSettings": {
"description": "Configure the default settings used when generating new passwords. These settings will be used for all new passwords unless overridden for specific entries.",
"preview": "Preview"
"description": "Настройте параметры по умолчанию, которые будут использоваться при генерации новых паролей. Эти настройки будут применяться ко всем новым паролям, если только они не будут изменены для конкретных записей.",
"preview": "Предпросмотр"
},
"securitySettings": {
"title": "Безопасность",
@@ -439,9 +440,9 @@
"retryingConnection": "Повторная попытка подключения..."
},
"offline": {
"banner": "Offline mode (read-only)",
"backOnline": "Back online",
"stillOffline": "Still offline"
"banner": "Офлайн режим (только для чтения)",
"backOnline": "Восстановить подключение",
"stillOffline": "Нет подключения"
},
"alerts": {
"syncIssue": "Проблема с синхронизацией",

View File

@@ -109,6 +109,7 @@
"randomAlias": "Random Alias",
"manual": "Manual",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"enterFullEmail": "Enter full email address",
"enterEmailPrefix": "Enter email prefix",
"useDomainChooser": "Use domain chooser",

View File

@@ -109,6 +109,7 @@
"randomAlias": "Random Alias",
"manual": "Manual",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"enterFullEmail": "E-posta adresinizi girin",
"enterEmailPrefix": "E-posta önekini girin",
"useDomainChooser": "Alan adı seçiciyi kullan",

View File

@@ -109,6 +109,7 @@
"randomAlias": "Випадковий псевдонім",
"manual": "Посібник",
"generateRandomAlias": "Генерувати випадковий псевдонім",
"clearAliasFields": "Clear Alias Fields",
"enterFullEmail": "Введіть повну електронну адресу",
"enterEmailPrefix": "Введіть префікс електронної адреси",
"useDomainChooser": "Використовувати засіб вибору домену",

View File

@@ -109,6 +109,7 @@
"randomAlias": "随机别名",
"manual": "手动输入",
"generateRandomAlias": "生成随机别名",
"clearAliasFields": "清除别名字段",
"enterFullEmail": "输入完整邮箱地址",
"enterEmailPrefix": "输入邮箱前缀",
"useDomainChooser": "使用域名选择器",

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -9,7 +9,10 @@
{
"layers" : [
{
"fill" : "automatic",
"blend-mode" : "overlay",
"fill" : {
"automatic-gradient" : "display-p3:0.90471,0.76358,0.48553,1.00000"
},
"glass" : true,
"hidden" : false,
"image-name" : "icon-1024.png",

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -22,7 +22,7 @@
CE48FD372DBE95EB00E5E3D6 /* VaultModels.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE482AA2DBE8EFE00F4A367 /* VaultModels.framework */; };
CE48FD632DBEA3B800E5E3D6 /* VaultUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE4816A2DBE8AC800F4A367 /* VaultUI.framework */; };
CE59C7632E4F47FE0024A246 /* VaultUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE4816A2DBE8AC800F4A367 /* VaultUI.framework */; };
CE73B3A02DFC8A0C0081B6CB /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = CE73B39F2DFC8A0C0081B6CB /* AppIcon.icon */; };
CE73B3A02DFC8A0C0081B6CB /* AliasVault.icon in Resources */ = {isa = PBXBuildFile; fileRef = CE73B39F2DFC8A0C0081B6CB /* AliasVault.icon */; };
CE9A58FC2DBA982100CB0A4C /* RCTNativeVaultManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = CE9A58FB2DBA982100CB0A4C /* RCTNativeVaultManager.mm */; };
CE9A5A022DBAAE5000CB0A4C /* RCTNativeVaultManager.h in Sources */ = {isa = PBXBuildFile; fileRef = CE9A58FA2DBA982100CB0A4C /* RCTNativeVaultManager.h */; };
CED3AB3C2E70CF8700F3FDEB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED3AB3B2E70CF8700F3FDEB /* AppDelegate.swift */; };
@@ -179,7 +179,7 @@
CE26D68C2DA7FCD2006DC04D /* VaultManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultManager.swift; sourceTree = "<group>"; };
CE3AE2B02DC7ACD700E7745E /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
CE59C75F2E4F47FD0024A246 /* VaultUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VaultUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
CE73B39F2DFC8A0C0081B6CB /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = AppIcon.icon; sourceTree = "<group>"; };
CE73B39F2DFC8A0C0081B6CB /* AliasVault.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AliasVault.icon; sourceTree = "<group>"; };
CE9A58FA2DBA982100CB0A4C /* RCTNativeVaultManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTNativeVaultManager.h; sourceTree = "<group>"; };
CE9A58FB2DBA982100CB0A4C /* RCTNativeVaultManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTNativeVaultManager.mm; sourceTree = "<group>"; };
CE9A5B662DBAE42B00CB0A4C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
@@ -198,7 +198,7 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */ = {
CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
@@ -208,73 +208,12 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
CE59C7602E4F47FD0024A246 /* VaultUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = VaultUITests;
sourceTree = "<group>";
};
CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = VaultStoreKit;
sourceTree = "<group>";
};
CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = VaultStoreKitTests;
sourceTree = "<group>";
};
CEE4816B2DBE8AC800F4A367 /* VaultUI */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = VaultUI;
sourceTree = "<group>";
};
CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = VaultModels;
sourceTree = "<group>";
};
CEE909812DA548C7008D568F /* Autofill */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = Autofill;
sourceTree = "<group>";
};
CE59C7602E4F47FD0024A246 /* VaultUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUITests; sourceTree = "<group>"; };
CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKit; sourceTree = "<group>"; };
CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKitTests; sourceTree = "<group>"; };
CEE4816B2DBE8AC800F4A367 /* VaultUI */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUI; sourceTree = "<group>"; };
CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultModels; sourceTree = "<group>"; };
CEE909812DA548C7008D568F /* Autofill */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Autofill; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -353,7 +292,7 @@
CED3AB3B2E70CF8700F3FDEB /* AppDelegate.swift */,
962DE0FB93C9458BAFEEED60 /* AliasVault-Bridging-Header.h */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
CE73B39F2DFC8A0C0081B6CB /* AppIcon.icon */,
CE73B39F2DFC8A0C0081B6CB /* AliasVault.icon */,
13B07FB61A68108700A75B9A /* Info.plist */,
76D5DBE3C1FA4900A7CD27FB /* noop-file.swift */,
BA88D6CB4FD656C7180FE9F7 /* PrivacyInfo.xcprivacy */,
@@ -718,8 +657,10 @@
de,
en,
fi,
he,
it,
nl,
uk,
zh,
es,
sv,
@@ -728,6 +669,7 @@
fr,
ru,
uk,
he,
);
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
@@ -751,7 +693,7 @@
buildActionMask = 2147483647;
files = (
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
CE73B3A02DFC8A0C0081B6CB /* AppIcon.icon in Resources */,
CE73B3A02DFC8A0C0081B6CB /* AliasVault.icon in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
D6E19ADE0A608FA46984C60D /* PrivacyInfo.xcprivacy in Resources */,
@@ -1198,12 +1140,13 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 5C1EA06BE7608888B8DF9CE0 /* Pods-AliasVault.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AliasVault;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -1218,7 +1161,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1240,10 +1183,11 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 82C6F986E9F08FE1179D0A40 /* Pods-AliasVault.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AliasVault;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
INFOPLIST_FILE = AliasVault/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
@@ -1253,7 +1197,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1323,10 +1267,7 @@
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -1380,10 +1321,7 @@
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
@@ -1404,7 +1342,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1440,7 +1378,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1472,7 +1410,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1525,7 +1463,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1574,7 +1512,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1609,7 +1547,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1642,7 +1580,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1695,7 +1633,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1744,7 +1682,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1796,7 +1734,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1847,7 +1785,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1863,7 +1801,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
@@ -1892,7 +1830,7 @@
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 230000;
CURRENT_PROJECT_VERSION = 230200;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1908,7 +1846,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.app.autofill;

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

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