mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-31 01:58:36 -05:00
Compare commits
243 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f48591685a | ||
|
|
cae1813084 | ||
|
|
74e18a8fb1 | ||
|
|
a89546200c | ||
|
|
a40f29d467 | ||
|
|
bcda120351 | ||
|
|
ad1ffd63d5 | ||
|
|
4b55a21d33 | ||
|
|
183548616e | ||
|
|
4938129367 | ||
|
|
984f5a2c52 | ||
|
|
5969a9d437 | ||
|
|
efbb64637d | ||
|
|
b460023911 | ||
|
|
c0e869a586 | ||
|
|
cd306ef878 | ||
|
|
1a40e31470 | ||
|
|
30f9199a7e | ||
|
|
e830b9c482 | ||
|
|
bc6b9da10b | ||
|
|
40991d879e | ||
|
|
2949978a11 | ||
|
|
9715be40f3 | ||
|
|
a1d146c517 | ||
|
|
b729efbcfb | ||
|
|
ac0b7c4be8 | ||
|
|
865d5c8fce | ||
|
|
f154d8afe7 | ||
|
|
df6bcff8b3 | ||
|
|
3fbfca6163 | ||
|
|
d86ad136f7 | ||
|
|
a3e51409cf | ||
|
|
a11052bc77 | ||
|
|
de4b102397 | ||
|
|
ec66e7c339 | ||
|
|
59b118b35d | ||
|
|
db9ba0eac3 | ||
|
|
215e7b0eff | ||
|
|
d7b97a7139 | ||
|
|
8b23bc6142 | ||
|
|
49eae07bce | ||
|
|
8a2aafacfb | ||
|
|
23c386003e | ||
|
|
16e03d4dbc | ||
|
|
3616afa625 | ||
|
|
2fd8ade738 | ||
|
|
e10a37328a | ||
|
|
4811eb9ebe | ||
|
|
ba65e0c8ff | ||
|
|
1150614722 | ||
|
|
d10cc79148 | ||
|
|
ec833cb430 | ||
|
|
751f8b6afd | ||
|
|
68f351cfc5 | ||
|
|
b2177f5d98 | ||
|
|
d43efb0273 | ||
|
|
490861016a | ||
|
|
fa6ff5153a | ||
|
|
8ddefa56af | ||
|
|
0dac97f4ff | ||
|
|
7da8189789 | ||
|
|
a674baa6d6 | ||
|
|
9e04e54b43 | ||
|
|
7cb789ce9d | ||
|
|
c0a5a7db03 | ||
|
|
ccb84780eb | ||
|
|
25acce3ae0 | ||
|
|
1d29c3338d | ||
|
|
1c95a86c51 | ||
|
|
78052e74d6 | ||
|
|
70cc2b4985 | ||
|
|
5050fdc95d | ||
|
|
4da10bbfba | ||
|
|
7844f411ef | ||
|
|
cca687b61f | ||
|
|
8e6d125700 | ||
|
|
19fe4121ad | ||
|
|
6178303418 | ||
|
|
d563bd5c02 | ||
|
|
47f55ea08f | ||
|
|
07bad37568 | ||
|
|
b0dda6cb77 | ||
|
|
7a179fcde0 | ||
|
|
53decce407 | ||
|
|
dfb8c86366 | ||
|
|
cec6e7c303 | ||
|
|
1993d08487 | ||
|
|
6c54c270fa | ||
|
|
c92c8fc663 | ||
|
|
b0d03d6bb1 | ||
|
|
68895a7834 | ||
|
|
d183a406ac | ||
|
|
55b22dcaa8 | ||
|
|
44ff1b0118 | ||
|
|
ddd7b0a4ab | ||
|
|
bd564a1cd9 | ||
|
|
c7aa98a172 | ||
|
|
553e716c31 | ||
|
|
1e50b7b6bc | ||
|
|
3fce102471 | ||
|
|
297a7b4824 | ||
|
|
9dc80be72a | ||
|
|
c585bd83d2 | ||
|
|
0b81554b38 | ||
|
|
c93884c306 | ||
|
|
e8a40ea18e | ||
|
|
80a9996a23 | ||
|
|
7a300d5a46 | ||
|
|
3985a9e5ab | ||
|
|
214c76b446 | ||
|
|
30a2b0557a | ||
|
|
e63c198cce | ||
|
|
1e33c22d32 | ||
|
|
e253646c30 | ||
|
|
6303924d01 | ||
|
|
42fff611d8 | ||
|
|
4837d3d855 | ||
|
|
0b461bd015 | ||
|
|
a2c69bf36c | ||
|
|
bfe08eada7 | ||
|
|
8361860db5 | ||
|
|
5a1e859185 | ||
|
|
f9aa9005da | ||
|
|
de785d7e82 | ||
|
|
3aac3d9088 | ||
|
|
ca2088fd7a | ||
|
|
4e08d3f01c | ||
|
|
3bc2e47d76 | ||
|
|
9546327575 | ||
|
|
9dbfd3ea2b | ||
|
|
b3101c5336 | ||
|
|
454e005127 | ||
|
|
4bde61a70f | ||
|
|
bda0b11729 | ||
|
|
4cc0e66d93 | ||
|
|
fc091c441c | ||
|
|
d6e510fad3 | ||
|
|
a3cad05cd3 | ||
|
|
b9f3995f5d | ||
|
|
1b1a5924c3 | ||
|
|
7b820ccda1 | ||
|
|
459616880e | ||
|
|
62c23d34cf | ||
|
|
74e7635705 | ||
|
|
0a943a5066 | ||
|
|
67d3519ff8 | ||
|
|
02f4b53670 | ||
|
|
3bed56231a | ||
|
|
5204726bec | ||
|
|
c5a0bad44d | ||
|
|
f74a09e4bb | ||
|
|
99e17d0792 | ||
|
|
5f7730a474 | ||
|
|
f9a4937a3a | ||
|
|
468e7c8b66 | ||
|
|
8d5d755fdf | ||
|
|
64857bcbb4 | ||
|
|
66db3e0571 | ||
|
|
4cbed21e67 | ||
|
|
16f8eced09 | ||
|
|
547fa57cb6 | ||
|
|
cbacd7486a | ||
|
|
3b413a79c9 | ||
|
|
a2b962bb44 | ||
|
|
95739f6758 | ||
|
|
cc95779f48 | ||
|
|
25ff5bf994 | ||
|
|
32e6ca597a | ||
|
|
a39340262e | ||
|
|
58ed0bf156 | ||
|
|
77994d221e | ||
|
|
519fc5fb24 | ||
|
|
accc76d8a2 | ||
|
|
0c2de27f1a | ||
|
|
53047cf3ad | ||
|
|
0b7cdbce02 | ||
|
|
a963064dc8 | ||
|
|
f4c4962cb8 | ||
|
|
3c36020812 | ||
|
|
9892430e59 | ||
|
|
1e3e542f92 | ||
|
|
c90c5a9f2f | ||
|
|
7621be4cbe | ||
|
|
31868b7099 | ||
|
|
8213a81321 | ||
|
|
df2ae22a99 | ||
|
|
9999529d60 | ||
|
|
1df4884301 | ||
|
|
185b7a0ad6 | ||
|
|
c3dd77d6f8 | ||
|
|
c3ae769d11 | ||
|
|
fc7f12471a | ||
|
|
d36a3dba42 | ||
|
|
9556e6dca9 | ||
|
|
c0a63be92b | ||
|
|
2cf1ea2065 | ||
|
|
df7d1560be | ||
|
|
a6a56ec9fb | ||
|
|
3675454737 | ||
|
|
da21565f1b | ||
|
|
5b6a80a7b1 | ||
|
|
cb5cd1006c | ||
|
|
ca9b9e465c | ||
|
|
9a6c86569d | ||
|
|
21177e9927 | ||
|
|
e7c79f2aa4 | ||
|
|
8e89673cc9 | ||
|
|
fc75532a0d | ||
|
|
9eb913c692 | ||
|
|
e1497b74aa | ||
|
|
2d85511ec5 | ||
|
|
7c26398e9c | ||
|
|
23052b375c | ||
|
|
406505035b | ||
|
|
371ed93819 | ||
|
|
e715454acb | ||
|
|
28c1869048 | ||
|
|
bde0877168 | ||
|
|
2f11b5507c | ||
|
|
149a85dde9 | ||
|
|
cdfe7c5a99 | ||
|
|
23378368fb | ||
|
|
27fad07f92 | ||
|
|
29b5501a01 | ||
|
|
988c43ae20 | ||
|
|
f9e94c3059 | ||
|
|
1969dd0b48 | ||
|
|
f7a0f3d29a | ||
|
|
2464858b4e | ||
|
|
f793510b1e | ||
|
|
e7644dc3fb | ||
|
|
67d4a0b8ff | ||
|
|
0e37616ced | ||
|
|
182e5d8d8d | ||
|
|
f19e288196 | ||
|
|
8bff55414c | ||
|
|
63b18acbac | ||
|
|
49676bf1f4 | ||
|
|
db39a18ab5 | ||
|
|
4d57f8dea3 | ||
|
|
3160ad202a | ||
|
|
946a44a9a1 | ||
|
|
4bba4c5911 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -417,6 +417,7 @@ database/postgres-dev
|
||||
|
||||
# Temp files
|
||||
temp
|
||||
*.zip
|
||||
|
||||
# Don't check in .js.map or .mjs.map files. These are generated by the build process in the shared
|
||||
# libraries and copied to the application so they can be used for debugging, but we don't need
|
||||
|
||||
14
.vscode/tasks.json
vendored
14
.vscode/tasks.json
vendored
@@ -57,6 +57,20 @@
|
||||
"cwd": "${workspaceFolder}/apps/server/Services/AliasVault.SmtpService"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch TaskRunner",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/server/Services/AliasVault.TaskRunner"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Client CSS",
|
||||
"type": "shell",
|
||||
|
||||
@@ -2,26 +2,39 @@
|
||||
|
||||
Thanks for your interest in contributing to the AliasVault project! There are a lot of ways to help out.
|
||||
|
||||
## Community Engagement
|
||||
## Table of Contents
|
||||
|
||||
Become active in AliasVault's community, helping by:
|
||||
1. [Help spread the word](#1-help-spread-the-word)
|
||||
2. [Contributing to Translations](#2-contributing-to-translations)
|
||||
3. [Contributing to the Documentation](#3-contributing-to-the-documentation)
|
||||
4. [Contributing to the Main Codebase](#4-contributing-to-the-main-codebase)
|
||||
- [4.1 Get in contact](#41-get-in-contact)
|
||||
- [4.2 Set up your local development environment](#42-set-up-your-local-development-environment)
|
||||
5. [License and Contributions](#5-license-and-contributions)
|
||||
|
||||
- **Answering questions** in our [Discord community](https://discord.gg/DsaXMTEtpF)
|
||||
- **Helping users** with self-hosting setup and troubleshooting
|
||||
- **Reporting bugs** and suggesting improvements
|
||||
- **Participating in discussions** about features and improvements
|
||||
---
|
||||
|
||||
## Spreading the Word
|
||||
## 1. Help spread the word
|
||||
|
||||
Getting the word out about AliasVault is important so we can reach and help more people to improve their privacy. You can help by:
|
||||
Help grow the AliasVault community by:
|
||||
|
||||
- **Sharing on social media** (X, Reddit, Mastodon, etc.)
|
||||
- **Writing blog posts** about your AliasVault experience
|
||||
- **Creating video tutorials** or walkthroughs
|
||||
- **Mentioning AliasVault** in privacy/self-hosting discussions
|
||||
- **Telling friends and colleagues** about the project
|
||||
- Answering questions and helping users in our [Discord](https://discord.gg/DsaXMTEtpF)
|
||||
- Reporting bugs and suggesting improvements
|
||||
- Sharing on social media and writing about your experience
|
||||
- Creating tutorials and documentation
|
||||
- Spreading the word about privacy and self-hosting
|
||||
|
||||
## Contributing to the Documentation
|
||||
## 2. Contributing to Translations
|
||||
|
||||
Help make AliasVault accessible to users worldwide by contributing translations! AliasVault is currently available in English and Dutch, but we're looking for volunteers to help translate it into other languages such as German, French, Spanish, Ukrainian, Italian, and more.
|
||||
|
||||
AliasVault translations are managed through [Crowdin](https://crowdin.com/), an online translation platform. If you’d like to help translate AliasVault into your native language, please [request access to the Crowdin project](https://crowdin.com/project/aliasvault).
|
||||
|
||||
If you're willing to help, we also encourage you to get in contact via [Discord](https://discord.gg/DsaXMTEtpF) to chat (quickest), or contact us via email at [contact@support.aliasvault.net](mailto:contact@support.aliasvault.net) to discuss the language(s) you are willing to contribute to, and so we can answer any technical questions you might have.
|
||||
|
||||
Your translation contributions will help make AliasVault more accessible to privacy-conscious users around the world!
|
||||
|
||||
## 3. Contributing to the Documentation
|
||||
|
||||
The docs are built using Jekyll and automatically deploy to GitHub Pages via GitHub Actions. You can build the docs locally by running `docker compose up` in the `./docs` folder.
|
||||
|
||||
@@ -29,15 +42,16 @@ The docs site is based on the open-source template called Just The Docs. Find mo
|
||||
|
||||
To make changes to the AliasVault documentation please make a PR that directly edits the `docs` markdown files in this repository.
|
||||
|
||||
## Contributing to the Main Codebase
|
||||
### Get in contact
|
||||
If you’re planning to work on a new feature or improvement for AliasVault, we strongly encourage you to get in touch with us first. This ensures that your proposed changes align with the project's direction and increases the likelihood of your work being accepted into the official repository. You can reach us through:
|
||||
## 4. Contributing to the Main Codebase
|
||||
|
||||
### 4.1 Get in contact
|
||||
If you're planning to work on a new feature or improvement for AliasVault, we strongly encourage you to get in touch with us first. This ensures that your proposed changes align with the project's direction and increases the likelihood of your work being accepted into the official repository. You can reach us through:
|
||||
|
||||
- Opening an issue on GitHub to discuss your proposed changes
|
||||
- Reaching out via Discord or email
|
||||
- Contacting the maintainers directly
|
||||
|
||||
### Set up your local development environment
|
||||
### 4.2 Set up your local development environment
|
||||
You can find instructions on how to get your local development environment setup for the different parts of the AliasVault codebase here:
|
||||
|
||||
https://docs.aliasvault.net/misc/dev/
|
||||
@@ -46,7 +60,7 @@ https://docs.aliasvault.net/misc/dev/
|
||||
|
||||
If you run into any issues, feel free to join our [Discord](https://discord.gg/DsaXMTEtpF) to chat with the maintainers and author.
|
||||
|
||||
## License and Contributions
|
||||
## 5. License and Contributions
|
||||
|
||||
AliasVault is licensed under the GNU Affero General Public License v3.0 (AGPLv3). By submitting code, documentation, or other contributions to this project, you agree that:
|
||||
|
||||
|
||||
13
README.md
13
README.md
@@ -4,11 +4,12 @@ AliasVault is a privacy-first password and email alias manager. Create unique id
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github&label=Release">](https://github.com/lanedirt/AliasVault/releases)
|
||||
[](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-e2e-tests.yml)
|
||||
[<img src="https://img.shields.io/sonar/quality_gate/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=Sonarcloud&logo=sonarcloud">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
|
||||
[<img src="https://badges.crowdin.net/aliasvault/localized.svg">](https://crowdin.com/project/aliasvault)
|
||||
[<img alt="Discord" src="https://img.shields.io/discord/1309300619026235422?logo=discord&logoColor=%237289da&label=Discord&color=%237289da">](https://discord.gg/DsaXMTEtpF)
|
||||
|
||||
<a href="https://app.aliasvault.net">Try the cloud version 🔥</a> | <a href="https://aliasvault.net?utm_source=gh-readme">Website </a> | <a href="https://docs.aliasvault.net?utm_source=gh-readme">Documentation </a> | <a href="#self-hosting">Self-host instructions</a>
|
||||
|
||||
⭐ Star us on GitHub — it motivates us a lot!
|
||||
⭐ Star us on GitHub, it motivates us a lot!
|
||||
|
||||
## About
|
||||
Built on 15 years of experience, AliasVault is independent, open-source, self-hostable and community-driven. It’s the response to a web that tracks everything: a way to take back control of your digital privacy and help you stay secure online.
|
||||
@@ -57,6 +58,7 @@ AliasVault is available on:
|
||||
<p>
|
||||
<a href="https://apps.apple.com/app/id6745490915" style="display: inline-block; margin-right: 20px;"><img src="https://github.com/user-attachments/assets/bad09b85-2635-4e3e-b154-9f348b88f6d6" style="height: 40px;margin-right:10px;" alt="Download on the App Store"></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=net.aliasvault.app" style="display: inline-block;"><img src="https://github.com/user-attachments/assets/b28979c9-f4b8-4090-8735-e384a7fdaa47" style="height: 40px;" alt="Get it on Google Play"></a>
|
||||
<a href="https://f-droid.org/packages/net.aliasvault.app" style="display: inline-block;"><img src="https://github.com/user-attachments/assets/0fb25df1-0ea2-46a6-bfee-a9d70f22a02a" style="height: 40px;" alt="Get it on F-Droid"></a>
|
||||
</p>
|
||||
|
||||
[<img width="700" alt="Screenshot of AliasVault" src="docs/assets/img/screenshot.png">](https://app.aliasvault.net)
|
||||
@@ -68,11 +70,9 @@ For full control over your own data you can self-host and install AliasVault on
|
||||
|
||||
This method uses pre-built Docker images and works on minimal hardware specifications:
|
||||
|
||||
- Linux VM with root access (Ubuntu/AlmaLinux recommended) or Raspberry Pi
|
||||
- 1 vCPU
|
||||
- 1GB RAM
|
||||
- 16GB disk space
|
||||
- Docker (20.10+) and Docker Compose (2.0+)
|
||||
- 64-bit Linux VM (Ubuntu/AlmaLinux) or Raspberry Pi, with root access
|
||||
- Minimum: 1 vCPU, 1GB RAM, 16GB disk
|
||||
- Docker ≥ 20.10 and Docker Compose ≥ 2.0
|
||||
|
||||
```bash
|
||||
# Download install script from latest stable release
|
||||
@@ -126,6 +126,7 @@ Core features that are being worked on:
|
||||
- [x] iOS native app
|
||||
- [x] Android native app
|
||||
- [x] Editing in browser extension
|
||||
- [x] Multi-language support across all client applications
|
||||
- [ ] Data model and usability improvements (more flexible aliases and credential types, folder support, bulk selecting etc.)
|
||||
- [ ] Support for FIDO2/WebAuthn hardware keys and passkeys
|
||||
- [ ] Adding support for family/team sharing (organization features)
|
||||
|
||||
101
apps/browser-extension/package-lock.json
generated
101
apps/browser-extension/package-lock.json
generated
@@ -1,22 +1,24 @@
|
||||
{
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.18.1",
|
||||
"version": "0.20.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.18.1",
|
||||
"version": "0.20.2",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"i18next": "^25.3.1",
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "^15.6.0",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0",
|
||||
"sql.js": "^1.12.0",
|
||||
@@ -7014,6 +7016,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
|
||||
@@ -7079,6 +7090,46 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.3.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.1.tgz",
|
||||
"integrity": "sha512-S4CPAx8LfMOnURnnJa8jFWvur+UX/LWcl6+61p9VV7SK2m0445JeBJ6tLD0D5SR0H29G4PYfWkEhivKG5p4RDg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next/node_modules/@babel/runtime": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -10790,6 +10841,41 @@
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.0.tgz",
|
||||
"integrity": "sha512-W135dB0rDfiFmbMipC17nOhGdttO5mzH8BivY+2ybsQBbXvxWIwl3cmeH3T9d+YPBSJu/ouyJKFJTtkK7rJofw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next/node_modules/@babel/runtime": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -12676,7 +12762,7 @@
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -13189,6 +13275,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.20.2",
|
||||
"version": "0.21.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "wxt -b chrome",
|
||||
@@ -30,10 +30,12 @@
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"i18next": "^25.3.1",
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "^15.6.0",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0",
|
||||
"sql.js": "^1.12.0",
|
||||
|
||||
@@ -447,7 +447,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
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.20.2;
|
||||
MARKETING_VERSION = 0.21.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 = 23;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
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.20.2;
|
||||
MARKETING_VERSION = 0.21.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 = 23;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
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.20.2;
|
||||
MARKETING_VERSION = 0.21.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 = 23;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
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.20.2;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -37,7 +37,7 @@ export default defineBackground({
|
||||
// Setup context menus
|
||||
const isContextMenuEnabled = await storage.getItem(GLOBAL_CONTEXT_MENU_ENABLED_KEY) ?? true;
|
||||
if (isContextMenuEnabled) {
|
||||
setupContextMenus();
|
||||
await setupContextMenus();
|
||||
}
|
||||
|
||||
// Listen for custom commands
|
||||
|
||||
@@ -3,12 +3,14 @@ import { sendMessage } from 'webext-bridge/background';
|
||||
|
||||
import { PasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
|
||||
import { t } from '@/i18n/StandaloneI18n';
|
||||
|
||||
import { browser } from "#imports";
|
||||
|
||||
/**
|
||||
* Setup the context menus.
|
||||
*/
|
||||
export function setupContextMenus() : void {
|
||||
export async function setupContextMenus() : Promise<void> {
|
||||
// Create root menu
|
||||
browser.contextMenus.create({
|
||||
id: "aliasvault-root",
|
||||
@@ -20,7 +22,7 @@ export function setupContextMenus() : void {
|
||||
browser.contextMenus.create({
|
||||
id: "aliasvault-activate-form",
|
||||
parentId: "aliasvault-root",
|
||||
title: "Autofill with AliasVault",
|
||||
title: await t('content.autofillWithAliasVault'),
|
||||
contexts: ["editable"],
|
||||
});
|
||||
|
||||
@@ -36,7 +38,7 @@ export function setupContextMenus() : void {
|
||||
browser.contextMenus.create({
|
||||
id: "aliasvault-generate-password",
|
||||
parentId: "aliasvault-root",
|
||||
title: "Generate random password (copy to clipboard)",
|
||||
title: await t('content.generateRandomPassword'),
|
||||
contexts: ["all"]
|
||||
});
|
||||
|
||||
@@ -56,10 +58,13 @@ export function handleContextMenuClick(info: Browser.contextMenus.OnClickData, t
|
||||
|
||||
// Use browser.scripting to write password to clipboard from active tab
|
||||
if (tab?.id) {
|
||||
browser.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: copyPasswordToClipboard,
|
||||
args: [password]
|
||||
// Get confirm text translation.
|
||||
t('content.passwordCopiedToClipboard').then((message) => {
|
||||
browser.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: copyPasswordToClipboard,
|
||||
args: [message, password]
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (info.menuItemId === "aliasvault-activate-form" && tab?.id) {
|
||||
@@ -80,9 +85,9 @@ export function handleContextMenuClick(info: Browser.contextMenus.OnClickData, t
|
||||
/**
|
||||
* Copy provided password to clipboard.
|
||||
*/
|
||||
function copyPasswordToClipboard(generatedPassword: string) : void {
|
||||
function copyPasswordToClipboard(message: string, generatedPassword: string) : void {
|
||||
navigator.clipboard.writeText(generatedPassword).then(() => {
|
||||
showToast('Password copied to clipboard');
|
||||
showToast(message);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,7 +45,7 @@ export function handleToggleContextMenu(message: any) : Promise<BoolResponse> {
|
||||
if (!message.enabled) {
|
||||
browser.contextMenus.removeAll();
|
||||
} else {
|
||||
setupContextMenus();
|
||||
await setupContextMenus();
|
||||
}
|
||||
return { success: true };
|
||||
})();
|
||||
|
||||
@@ -14,6 +14,8 @@ import { VaultResponse as messageVaultResponse } from '@/utils/types/messaging/V
|
||||
import { VaultUploadResponse as messageVaultUploadResponse } from '@/utils/types/messaging/VaultUploadResponse';
|
||||
import { WebApiService } from '@/utils/WebApiService';
|
||||
|
||||
import { t } from '@/i18n/StandaloneI18n';
|
||||
|
||||
/**
|
||||
* Check if the user is logged in and if the vault is locked, and also check for pending migrations.
|
||||
*/
|
||||
@@ -58,7 +60,7 @@ export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, i
|
||||
isLoggedIn,
|
||||
isVaultLocked,
|
||||
hasPendingMigrations: false,
|
||||
error: error instanceof Error ? error.message : 'An unknown error occurred'
|
||||
error: error instanceof Error ? error.message : await t('common.errors.unknownError')
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -100,7 +102,7 @@ export async function handleStoreVault(
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to store vault:', error);
|
||||
return { success: false, error: 'Failed to store vault' };
|
||||
return { success: false, error: await t('common.errors.failedToStoreVault') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +115,7 @@ export async function handleSyncVault(
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
return { success: false, error: statusError };
|
||||
return { success: false, error: await t('common.errors.' + statusError) };
|
||||
}
|
||||
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
|
||||
@@ -147,7 +149,7 @@ export async function handleGetVault(
|
||||
|
||||
if (!encryptedVault) {
|
||||
console.error('Vault not available');
|
||||
return { success: false, error: 'Vault not available' };
|
||||
return { success: false, error: await t('common.errors.vaultNotAvailable') };
|
||||
}
|
||||
|
||||
const decryptedVault = await EncryptionUtility.symmetricDecrypt(
|
||||
@@ -164,7 +166,7 @@ export async function handleGetVault(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get vault:', error);
|
||||
return { success: false, error: 'Failed to get vault' };
|
||||
return { success: false, error: await t('common.errors.failedToGetVault') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +194,7 @@ export async function handleGetCredentials(
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
if (!derivedKey) {
|
||||
return { success: false, error: 'Vault is locked' };
|
||||
return { success: false, error: await t('common.errors.vaultIsLocked') };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -201,7 +203,7 @@ export async function handleGetCredentials(
|
||||
return { success: true, credentials: credentials };
|
||||
} catch (error) {
|
||||
console.error('Error getting credentials:', error);
|
||||
return { success: false, error: 'Failed to get credentials' };
|
||||
return { success: false, error: await t('common.errors.failedToGetCredentials') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +216,7 @@ export async function handleCreateIdentity(
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
if (!derivedKey) {
|
||||
return { success: false, error: 'Vault is locked' };
|
||||
return { success: false, error: await t('common.errors.vaultIsLocked') };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -229,7 +231,7 @@ export async function handleCreateIdentity(
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to create identity:', error);
|
||||
return { success: false, error: 'Failed to create identity' };
|
||||
return { success: false, error: await t('common.errors.failedToCreateIdentity') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +273,7 @@ export function handleGetDefaultEmailDomain(): Promise<stringResponse> {
|
||||
return { success: true, value: defaultEmailDomain ?? undefined };
|
||||
} catch (error) {
|
||||
console.error('Error getting default email domain:', error);
|
||||
return { success: false, error: 'Failed to get default email domain' };
|
||||
return { success: false, error: await t('common.errors.failedToGetDefaultEmailDomain') };
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -295,7 +297,7 @@ export async function handleGetDefaultIdentitySettings(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting default identity settings:', error);
|
||||
return { success: false, error: 'Failed to get default identity settings' };
|
||||
return { success: false, error: await t('common.errors.failedToGetDefaultIdentitySettings') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +313,7 @@ export async function handleGetPasswordSettings(
|
||||
return { success: true, settings: passwordSettings };
|
||||
} catch (error) {
|
||||
console.error('Error getting password settings:', error);
|
||||
return { success: false, error: 'Failed to get password settings' };
|
||||
return { success: false, error: await t('common.errors.failedToGetPasswordSettings') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +344,7 @@ export async function handleUploadVault(
|
||||
return { success: true, status: response.status, newRevisionNumber: response.newRevisionNumber };
|
||||
} catch (error) {
|
||||
console.error('Failed to upload vault:', error);
|
||||
return { success: false, error: 'Failed to upload vault' };
|
||||
return { success: false, error: await t('common.errors.failedToUploadVault') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +355,7 @@ export async function handleUploadVault(
|
||||
export async function handlePersistFormValues(data: any): Promise<void> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
if (!derivedKey) {
|
||||
throw new Error('No derived key available for encryption');
|
||||
throw new Error(await t('common.errors.noDerivedKeyAvailable'));
|
||||
}
|
||||
|
||||
// Always stringify the data properly
|
||||
@@ -441,7 +443,7 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
|
||||
if (response.status === 0) {
|
||||
await storage.setItem('session:vaultRevisionNumber', response.newRevisionNumber);
|
||||
} else {
|
||||
throw new Error('Failed to upload new vault to server');
|
||||
throw new Error(await t('common.errors.failedToUploadVaultToServer'));
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -454,7 +456,7 @@ async function createVaultSqliteClient() : Promise<SqliteClient> {
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
if (!encryptedVault || !derivedKey) {
|
||||
throw new Error('No vault or derived key found');
|
||||
throw new Error(await t('common.errors.noVaultOrDerivedKeyFound'));
|
||||
}
|
||||
|
||||
// Decrypt the vault.
|
||||
|
||||
@@ -7,6 +7,8 @@ import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup, createU
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
|
||||
|
||||
import { t } from '@/i18n/StandaloneI18n';
|
||||
|
||||
import { defineContentScript } from '#imports';
|
||||
import { createShadowRootUi } from '#imports';
|
||||
|
||||
@@ -159,13 +161,13 @@ export default defineContentScript({
|
||||
|
||||
if (authStatus.hasPendingMigrations) {
|
||||
// Show upgrade required popup
|
||||
createUpgradeRequiredPopup(inputElement, container, 'Vault upgrade required.');
|
||||
await createUpgradeRequiredPopup(inputElement, container, await t('content.vaultUpgradeRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (authStatus.error) {
|
||||
// Show upgrade required popup for version-related errors
|
||||
createUpgradeRequiredPopup(inputElement, container, authStatus.error);
|
||||
await createUpgradeRequiredPopup(inputElement, container, authStatus.error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -897,4 +897,288 @@ body {
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Password Configuration Styles */
|
||||
.av-password-length-container {
|
||||
margin-top: 12px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.av-password-length-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.av-password-length-header label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.av-password-length-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.av-password-length-value {
|
||||
font-size: 0.875rem;
|
||||
color: #e5e7eb;
|
||||
font-family: 'Courier New', monospace;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.av-password-config-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
transition: color 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.av-password-config-btn:hover {
|
||||
color: #e5e7eb;
|
||||
background-color: rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
|
||||
.av-password-config-btn .av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.av-password-length-slider {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #374151;
|
||||
border-radius: 4px;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.av-password-length-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
background: #d68338;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.av-password-length-slider::-moz-range-thumb {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
background: #d68338;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Password Config Dialog */
|
||||
.av-password-config-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 2147483647;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.av-password-config-dialog {
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-password-config-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #374151;
|
||||
}
|
||||
|
||||
.av-password-config-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.av-password-config-close {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
border-radius: 4px;
|
||||
transition: color 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.av-password-config-close:hover {
|
||||
color: #e5e7eb;
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-password-config-close .av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.av-password-config-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.av-password-preview-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.av-password-config-preview {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 6px;
|
||||
background: #374151;
|
||||
color: #f8f9fa;
|
||||
font-size: 14px;
|
||||
font-family: 'Courier New', monospace;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.av-password-config-refresh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
background: #374151;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #e5e7eb;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.av-password-config-refresh:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.av-password-config-refresh .av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.av-password-config-options {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.av-password-config-toggles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.av-password-config-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
background: #374151;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
transition: all 0.2s ease;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.av-password-config-toggle:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.av-password-config-toggle.active {
|
||||
background-color: #d68338;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.av-password-config-toggle.active:hover {
|
||||
background-color: #c97731;
|
||||
}
|
||||
|
||||
.av-password-config-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.av-password-config-checkbox label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #e5e7eb;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.av-password-config-checkbox input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: #d68338;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.av-password-config-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.av-password-config-use {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 10px 16px;
|
||||
background: #6b7280;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.av-password-config-use:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.av-password-config-use .av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
|
||||
import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav';
|
||||
@@ -41,30 +42,31 @@ type RouteConfig = {
|
||||
* App component.
|
||||
*/
|
||||
const App: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const authContext = useAuth();
|
||||
const { isInitialLoading } = useLoading();
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 150);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const { headerButtons } = useHeaderButtons();
|
||||
|
||||
// Add these route configurations
|
||||
const routes: RouteConfig[] = [
|
||||
// Move routes definition to useMemo to prevent recreation on every render
|
||||
const routes: RouteConfig[] = React.useMemo(() => [
|
||||
{ path: '/', element: <Index />, showBackButton: false },
|
||||
{ path: '/reinitialize', element: <Reinitialize />, showBackButton: false },
|
||||
{ path: '/login', element: <Login />, showBackButton: false },
|
||||
{ path: '/unlock', element: <Unlock />, showBackButton: false },
|
||||
{ path: '/unlock-success', element: <UnlockSuccess />, showBackButton: false },
|
||||
{ path: '/upgrade', element: <Upgrade />, showBackButton: false },
|
||||
{ path: '/auth-settings', element: <AuthSettings />, showBackButton: true, title: 'Settings' },
|
||||
{ path: '/auth-settings', element: <AuthSettings />, showBackButton: true, title: t('settings.title') },
|
||||
{ path: '/credentials', element: <CredentialsList />, showBackButton: false },
|
||||
{ path: '/credentials/add', element: <CredentialAddEdit />, showBackButton: true, title: 'Add credential' },
|
||||
{ path: '/credentials/:id', element: <CredentialDetails />, showBackButton: true, title: 'Credential details' },
|
||||
{ path: '/credentials/:id/edit', element: <CredentialAddEdit />, showBackButton: true, title: 'Edit credential' },
|
||||
{ path: '/credentials/add', element: <CredentialAddEdit />, showBackButton: true, title: t('credentials.addCredential') },
|
||||
{ path: '/credentials/:id', element: <CredentialDetails />, showBackButton: true, title: t('credentials.credentialDetails') },
|
||||
{ path: '/credentials/:id/edit', element: <CredentialAddEdit />, showBackButton: true, title: t('credentials.editCredential') },
|
||||
{ path: '/emails', element: <EmailsList />, showBackButton: false },
|
||||
{ path: '/emails/:id', element: <EmailDetails />, showBackButton: true, title: 'Email details' },
|
||||
{ path: '/emails/:id', element: <EmailDetails />, showBackButton: true, title: t('emails.title') },
|
||||
{ path: '/settings', element: <Settings />, showBackButton: false },
|
||||
{ path: '/logout', element: <Logout />, showBackButton: false },
|
||||
];
|
||||
], [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialLoading) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/FormInputCopyToClipboard';
|
||||
|
||||
@@ -13,6 +14,7 @@ type AliasBlockProps = {
|
||||
* Render the alias block.
|
||||
*/
|
||||
const AliasBlock: React.FC<AliasBlockProps> = ({ credential }) => {
|
||||
const { t } = useTranslation();
|
||||
const hasFirstName = Boolean(credential.Alias?.FirstName?.trim());
|
||||
const hasLastName = Boolean(credential.Alias?.LastName?.trim());
|
||||
const hasNickName = Boolean(credential.Alias?.NickName?.trim());
|
||||
@@ -24,39 +26,39 @@ const AliasBlock: React.FC<AliasBlockProps> = ({ credential }) => {
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">Alias</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t('common.alias')}</h2>
|
||||
{(hasFirstName || hasLastName) && (
|
||||
<FormInputCopyToClipboard
|
||||
id="fullName"
|
||||
label="Full Name"
|
||||
label={t('common.fullName')}
|
||||
value={[credential.Alias?.FirstName, credential.Alias?.LastName].filter(Boolean).join(' ')}
|
||||
/>
|
||||
)}
|
||||
{hasFirstName && (
|
||||
<FormInputCopyToClipboard
|
||||
id="firstName"
|
||||
label="First Name"
|
||||
label={t('common.firstName')}
|
||||
value={credential.Alias?.FirstName ?? ''}
|
||||
/>
|
||||
)}
|
||||
{hasLastName && (
|
||||
<FormInputCopyToClipboard
|
||||
id="lastName"
|
||||
label="Last Name"
|
||||
label={t('common.lastName')}
|
||||
value={credential.Alias?.LastName ?? ''}
|
||||
/>
|
||||
)}
|
||||
{hasBirthDate && (
|
||||
<FormInputCopyToClipboard
|
||||
id="birthDate"
|
||||
label="Birth Date"
|
||||
label={t('common.birthDate')}
|
||||
value={IdentityHelperUtils.normalizeBirthDateForDisplay(credential.Alias?.BirthDate)}
|
||||
/>
|
||||
)}
|
||||
{hasNickName && (
|
||||
<FormInputCopyToClipboard
|
||||
id="nickName"
|
||||
label="Nickname"
|
||||
label={t('common.nickname')}
|
||||
value={credential.Alias?.NickName ?? ''}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import type { Attachment } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
type AttachmentBlockProps = {
|
||||
credentialId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows attachments for a credential.
|
||||
*/
|
||||
const AttachmentBlock: React.FC<AttachmentBlockProps> = ({ credentialId }) => {
|
||||
const { t } = useTranslation();
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Downloads an attachment file.
|
||||
*/
|
||||
const downloadAttachment = (attachment: Attachment): void => {
|
||||
try {
|
||||
// Convert Uint8Array or number[] to Uint8Array
|
||||
const byteArray = attachment.Blob instanceof Uint8Array
|
||||
? attachment.Blob
|
||||
: new Uint8Array(attachment.Blob);
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([byteArray as BlobPart]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create temporary download link
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = attachment.Filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error downloading attachment:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the attachments for the credential.
|
||||
*/
|
||||
const loadAttachments = async (): Promise<void> => {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const attachmentList = dbContext.sqliteClient.getAttachmentsForCredential(credentialId);
|
||||
setAttachments(attachmentList);
|
||||
} catch (error) {
|
||||
console.error('Error loading attachments:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadAttachments();
|
||||
}, [credentialId, dbContext?.sqliteClient]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">{t('common.attachments')}</h2>
|
||||
{t('common.loadingAttachments')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">{t('common.attachments')}</h2>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{attachments.map(attachment => (
|
||||
<button
|
||||
key={attachment.Id}
|
||||
className="w-full text-left p-2 ps-3 pe-3 rounded bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
onClick={() => downloadAttachment(attachment)}
|
||||
aria-label={`Download ${attachment.Filename}`}
|
||||
>
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="flex flex-col">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{attachment.Filename}</h4>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{new Date(attachment.CreatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttachmentBlock;
|
||||
@@ -0,0 +1,146 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { Attachment } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
type AttachmentUploaderProps = {
|
||||
attachments: Attachment[];
|
||||
onAttachmentsChange: (attachments: Attachment[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component allows uploading and managing attachments for a credential.
|
||||
*/
|
||||
const AttachmentUploader: React.FC<AttachmentUploaderProps> = ({
|
||||
attachments,
|
||||
onAttachmentsChange
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [statusMessage, setStatusMessage] = useState<string>('');
|
||||
|
||||
/**
|
||||
* Handles file selection and upload.
|
||||
*/
|
||||
const handleFileSelection = async (event: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||
const files = event.target.files;
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatusMessage('Uploading...');
|
||||
|
||||
try {
|
||||
const newAttachments = [...attachments];
|
||||
|
||||
for (const file of Array.from(files)) {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const byteArray = new Uint8Array(arrayBuffer);
|
||||
|
||||
const attachment: Attachment = {
|
||||
Id: crypto.randomUUID(),
|
||||
Filename: file.name,
|
||||
Blob: byteArray,
|
||||
CredentialId: '', // Will be set when saving credential
|
||||
CreatedAt: new Date().toISOString(),
|
||||
UpdatedAt: new Date().toISOString(),
|
||||
IsDeleted: false,
|
||||
};
|
||||
|
||||
newAttachments.push(attachment);
|
||||
}
|
||||
|
||||
onAttachmentsChange(newAttachments);
|
||||
setStatusMessage('Files uploaded successfully.');
|
||||
|
||||
// Clear status message after 3 seconds
|
||||
setTimeout(() => setStatusMessage(''), 3000);
|
||||
} catch (error) {
|
||||
console.error('Error uploading files:', error);
|
||||
setStatusMessage('Error uploading files.');
|
||||
setTimeout(() => setStatusMessage(''), 3000);
|
||||
}
|
||||
|
||||
// Reset file input
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an attachment.
|
||||
*/
|
||||
const deleteAttachment = (attachmentToDelete: Attachment): void => {
|
||||
try {
|
||||
const updatedAttachments = [...attachments];
|
||||
|
||||
// Remove attachment from array
|
||||
const index = updatedAttachments.findIndex(a => a.Id === attachmentToDelete.Id);
|
||||
if (index !== -1) {
|
||||
updatedAttachments.splice(index, 1);
|
||||
}
|
||||
|
||||
onAttachmentsChange(updatedAttachments);
|
||||
setStatusMessage('Attachment deleted successfully.');
|
||||
setTimeout(() => setStatusMessage(''), 3000);
|
||||
} catch (error) {
|
||||
console.error('Error deleting attachment:', error);
|
||||
setStatusMessage('Error deleting attachment.');
|
||||
setTimeout(() => setStatusMessage(''), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const activeAttachments = attachments.filter(a => !a.IsDeleted);
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">{t('common.attachments')}</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileSelection}
|
||||
className="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400"
|
||||
/>
|
||||
{statusMessage && (
|
||||
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">{statusMessage}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeAttachments.length > 0 && (
|
||||
<div>
|
||||
<h4 className="mb-2 text-md font-medium text-gray-900 dark:text-white">Current attachments:</h4>
|
||||
<div className="space-y-2">
|
||||
{activeAttachments.map(attachment => (
|
||||
<div
|
||||
key={attachment.Id}
|
||||
className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700 rounded border border-gray-200 dark:border-gray-600"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{attachment.Filename}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{new Date(attachment.CreatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deleteAttachment(attachment)}
|
||||
className="text-red-500 hover:text-red-700 focus:outline-none"
|
||||
aria-label={`Delete ${attachment.Filename}`}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttachmentUploader;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/FormInputCopyToClipboard';
|
||||
|
||||
@@ -12,6 +13,7 @@ type LoginCredentialsBlockProps = {
|
||||
* Render the login credentials block.
|
||||
*/
|
||||
const LoginCredentialsBlock: React.FC<LoginCredentialsBlockProps> = ({ credential }) => {
|
||||
const { t } = useTranslation();
|
||||
const email = credential.Alias?.Email?.trim();
|
||||
const username = credential.Username?.trim();
|
||||
const password = credential.Password?.trim();
|
||||
@@ -22,25 +24,25 @@ const LoginCredentialsBlock: React.FC<LoginCredentialsBlockProps> = ({ credentia
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">Login credentials</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t('common.loginCredentials')}</h2>
|
||||
{email && (
|
||||
<FormInputCopyToClipboard
|
||||
id="email"
|
||||
label="Email"
|
||||
label={t('common.email')}
|
||||
value={email}
|
||||
/>
|
||||
)}
|
||||
{username && (
|
||||
<FormInputCopyToClipboard
|
||||
id="username"
|
||||
label="Username"
|
||||
label={t('common.username')}
|
||||
value={username}
|
||||
/>
|
||||
)}
|
||||
{password && (
|
||||
<FormInputCopyToClipboard
|
||||
id="password"
|
||||
label="Password"
|
||||
label={t('common.password')}
|
||||
value={password}
|
||||
type="password"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type NotesBlockProps = {
|
||||
notes: string | undefined;
|
||||
@@ -20,6 +21,7 @@ const convertUrlsToLinks = (text: string): string => {
|
||||
* Render the notes block.
|
||||
*/
|
||||
const NotesBlock: React.FC<NotesBlockProps> = ({ notes }) => {
|
||||
const { t } = useTranslation();
|
||||
if (!notes) {
|
||||
return null;
|
||||
}
|
||||
@@ -28,7 +30,7 @@ const NotesBlock: React.FC<NotesBlockProps> = ({ notes }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">Notes</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t('common.notes')}</h2>
|
||||
<div className="p-4 bg-gray-50 rounded-lg dark:bg-gray-700">
|
||||
<p
|
||||
className="text-gray-900 dark:text-gray-100 whitespace-pre-wrap"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as OTPAuth from 'otpauth';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
@@ -13,6 +14,7 @@ type TotpBlockProps = {
|
||||
* This component shows TOTP codes for a credential.
|
||||
*/
|
||||
const TotpBlock: React.FC<TotpBlockProps> = ({ credentialId }) => {
|
||||
const { t } = useTranslation();
|
||||
const [totpCodes, setTotpCodes] = useState<TotpCode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentCodes, setCurrentCodes] = useState<Record<string, string>>({});
|
||||
@@ -138,8 +140,8 @@ const TotpBlock: React.FC<TotpBlockProps> = ({ credentialId }) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Two-factor authentication</h2>
|
||||
Loading TOTP codes...
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">{t('common.twoFactorAuthentication')}</h2>
|
||||
{t('common.loadingTotpCodes')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -151,7 +153,7 @@ const TotpBlock: React.FC<TotpBlockProps> = ({ credentialId }) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Two-factor authentication</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">{t('common.twoFactorAuthentication')}</h2>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{totpCodes.map(totpCode => (
|
||||
<button
|
||||
@@ -171,7 +173,7 @@ const TotpBlock: React.FC<TotpBlockProps> = ({ credentialId }) => {
|
||||
</span>
|
||||
<div className="text-xs">
|
||||
{copiedId === totpCode.Id ? (
|
||||
<span className="text-green-600 dark:text-green-400">Copied!</span>
|
||||
<span className="text-green-600 dark:text-green-400">{t('common.copied')}</span>
|
||||
) : (
|
||||
<span className="text-gray-500 dark:text-gray-400">{getRemainingSeconds()}s</span>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import AliasBlock from './AliasBlock';
|
||||
import AttachmentBlock from './AttachmentBlock';
|
||||
import EmailBlock from './EmailBlock';
|
||||
import HeaderBlock from './HeaderBlock';
|
||||
import LoginCredentialsBlock from './LoginCredentialsBlock';
|
||||
@@ -11,5 +12,6 @@ export {
|
||||
TotpBlock,
|
||||
LoginCredentialsBlock,
|
||||
AliasBlock,
|
||||
NotesBlock
|
||||
NotesBlock,
|
||||
AttachmentBlock
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
@@ -18,15 +19,38 @@ type EmailPreviewProps = {
|
||||
* This component shows a preview of the latest emails in the inbox.
|
||||
*/
|
||||
export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
const { t } = useTranslation();
|
||||
const [emails, setEmails] = useState<MailboxEmail[]>([]);
|
||||
const [displayedEmails, setDisplayedEmails] = useState<MailboxEmail[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastEmailId, setLastEmailId] = useState<number>(0);
|
||||
const [isSpamOk, setIsSpamOk] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSupportedDomain, setIsSupportedDomain] = useState(false);
|
||||
const [displayedCount, setDisplayedCount] = useState(2);
|
||||
const webApi = useWebApi();
|
||||
const dbContext = useDb();
|
||||
|
||||
const emailsPerLoad = 3;
|
||||
const canLoadMore = displayedCount < emails.length;
|
||||
|
||||
/**
|
||||
* Updates the displayed emails based on the current count.
|
||||
*/
|
||||
const updateDisplayedEmails = (allEmails: MailboxEmail[], count: number) : void => {
|
||||
const displayed = allEmails.slice(0, count);
|
||||
setDisplayedEmails(displayed);
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads more emails.
|
||||
*/
|
||||
const loadMoreEmails = (): void => {
|
||||
const newCount = Math.min(displayedCount + emailsPerLoad, emails.length);
|
||||
setDisplayedCount(newCount);
|
||||
updateDisplayedEmails(emails, newCount);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the email is a public domain.
|
||||
*/
|
||||
@@ -74,23 +98,30 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
setError('An error occurred while loading emails. Please try again later.');
|
||||
setError(t('emails.errors.emailLoadError'));
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Only show the latest 2 emails to save space in UI
|
||||
const latestMails = data?.mails
|
||||
// Store all emails, sorted by date
|
||||
const allMails = data?.mails
|
||||
?.toSorted((a: MailboxEmail, b: MailboxEmail) =>
|
||||
new Date(b.dateSystem).getTime() - new Date(a.dateSystem).getTime())
|
||||
?.slice(0, 2) ?? [];
|
||||
new Date(b.dateSystem).getTime() - new Date(a.dateSystem).getTime()) ?? [];
|
||||
|
||||
if (loading && latestMails.length > 0) {
|
||||
setLastEmailId(latestMails[0].id);
|
||||
if (loading && allMails.length > 0) {
|
||||
setLastEmailId(allMails[0].id);
|
||||
}
|
||||
|
||||
setEmails(latestMails);
|
||||
// Only update emails if they actually changed to preserve displayedCount
|
||||
setEmails(prevEmails => {
|
||||
const emailsChanged = JSON.stringify(prevEmails.map(e => e.id)) !== JSON.stringify(allMails.map(e => e.id));
|
||||
if (emailsChanged) {
|
||||
updateDisplayedEmails(allMails, displayedCount);
|
||||
return allMails;
|
||||
}
|
||||
return prevEmails;
|
||||
});
|
||||
} else if (isPrivate) {
|
||||
// For private domains, use existing encrypted email logic
|
||||
try {
|
||||
@@ -102,15 +133,14 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
try {
|
||||
const data = response as { mails: MailboxEmail[] };
|
||||
|
||||
// Only show the latest 2 emails to save space in UI
|
||||
const latestMails = data.mails
|
||||
.toSorted((a, b) => new Date(b.dateSystem).getTime() - new Date(a.dateSystem).getTime())
|
||||
.slice(0, 2);
|
||||
// Store all emails, sorted by date
|
||||
const allMails = data.mails
|
||||
.toSorted((a, b) => new Date(b.dateSystem).getTime() - new Date(a.dateSystem).getTime());
|
||||
|
||||
if (latestMails) {
|
||||
if (allMails) {
|
||||
// Loop through all emails and decrypt them locally
|
||||
const decryptedEmails: MailboxEmail[] = await EncryptionUtility.decryptEmailList(
|
||||
latestMails,
|
||||
allMails,
|
||||
dbContext.sqliteClient!.getAllEncryptionKeys()
|
||||
);
|
||||
|
||||
@@ -118,30 +148,30 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
setLastEmailId(decryptedEmails[0].id);
|
||||
}
|
||||
|
||||
setEmails(decryptedEmails);
|
||||
// Only update emails if they actually changed to preserve displayedCount
|
||||
setEmails(prevEmails => {
|
||||
const emailsChanged = JSON.stringify(prevEmails.map(e => e.id)) !== JSON.stringify(decryptedEmails.map(e => e.id));
|
||||
if (emailsChanged) {
|
||||
updateDisplayedEmails(decryptedEmails, displayedCount);
|
||||
return decryptedEmails;
|
||||
}
|
||||
return prevEmails;
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Try to parse as error response instead
|
||||
const apiErrorResponse = response as ApiErrorResponse;
|
||||
|
||||
if (apiErrorResponse?.code === 'CLAIM_DOES_NOT_MATCH_USER') {
|
||||
setError('The current chosen email address is already in use. Please change the email address by editing this credential.');
|
||||
} else if (apiErrorResponse?.code === 'CLAIM_DOES_NOT_EXIST') {
|
||||
setError('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.');
|
||||
} else {
|
||||
setError('An error occurred while loading emails. Please try again later.');
|
||||
}
|
||||
|
||||
setError(t('emails.apiErrors.' + apiErrorResponse?.code));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
setError('An error occurred while loading emails. Please try again later.');
|
||||
setError(t('emails.errors.emailLoadError'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading emails:', err);
|
||||
setError('An unexpected error occurred while loading emails. Please try again later.');
|
||||
setError(t('emails.errors.emailUnexpectedError'));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
@@ -150,7 +180,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
// Set up auto-refresh interval
|
||||
const interval = setInterval(loadEmails, 2000);
|
||||
return () : void => clearInterval(interval);
|
||||
}, [email, loading, webApi, dbContext]);
|
||||
}, [email, loading, webApi, dbContext, t, displayedCount]);
|
||||
|
||||
// Don't render anything if the domain is not supported
|
||||
if (!isSupportedDomain) {
|
||||
@@ -161,7 +191,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Recent emails</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">{t('common.recentEmails')}</h2>
|
||||
</div>
|
||||
<div className="p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded">
|
||||
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
@@ -174,10 +204,10 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Recent emails</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">{t('common.recentEmails')}</h2>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
</div>
|
||||
Loading emails...
|
||||
{t('common.loadingEmails')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -185,10 +215,10 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Recent emails</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">{t('common.recentEmails')}</h2>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
</div>
|
||||
No emails received yet.
|
||||
{t('emails.noEmails')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -196,11 +226,11 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
return (
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Recent emails</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">{t('common.recentEmails')}</h2>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
</div>
|
||||
|
||||
{emails.map((mail) => (
|
||||
{displayedEmails.map((mail) => (
|
||||
isSpamOk ? (
|
||||
<a
|
||||
key={mail.id}
|
||||
@@ -239,6 +269,18 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
</Link>
|
||||
)
|
||||
))}
|
||||
|
||||
{canLoadMore && (
|
||||
<button
|
||||
onClick={loadMoreEmails}
|
||||
className="w-full mt-2 py-1 px-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-md transition-colors duration-200 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100 flex items-center justify-center gap-1"
|
||||
>
|
||||
<span>{t('common.loadMore')}</span>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* Button configuration for form input.
|
||||
@@ -36,6 +37,13 @@ const Icon: React.FC<{ name: string }> = ({ name }) => {
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||
</>
|
||||
);
|
||||
case 'settings':
|
||||
return (
|
||||
<>
|
||||
<path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -78,6 +86,7 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(({
|
||||
showPassword: controlledShowPassword,
|
||||
onShowPasswordChange
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [internalShowPassword, setInternalShowPassword] = React.useState(false);
|
||||
|
||||
/**
|
||||
@@ -101,7 +110,7 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(({
|
||||
};
|
||||
|
||||
const inputClasses = `mt-1 block w-full rounded-md ${
|
||||
error ? 'border-red-500' : 'border-gray-300 dark:border-gray-700'
|
||||
error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm border focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 py-2 px-3`;
|
||||
|
||||
// Add password visibility button if type is password
|
||||
@@ -112,7 +121,7 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(({
|
||||
* Toggle password visibility.
|
||||
*/
|
||||
onClick: (): void => setShowPassword(!showPassword),
|
||||
title: showPassword ? 'Hide password' : 'Show password'
|
||||
title: showPassword ? t('common.hidePassword') : t('common.showPassword')
|
||||
}]
|
||||
: buttons;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ClipboardCopyService } from '@/entrypoints/popup/utils/ClipboardCopyService';
|
||||
|
||||
@@ -60,6 +61,7 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
|
||||
value,
|
||||
type = 'text'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@@ -112,7 +114,7 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-green-500 dark:text-green-400 transition-colors duration-200"
|
||||
title="Copied!"
|
||||
title={t('common.copied')}
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Icon name="check" />
|
||||
@@ -123,7 +125,7 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
|
||||
type="button"
|
||||
onClick={copyToClipboard}
|
||||
className="p-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
|
||||
title="Copy to clipboard"
|
||||
title={t('common.copyToClipboard')}
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Icon name="copy" />
|
||||
@@ -135,7 +137,7 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="p-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
|
||||
title={showPassword ? 'Hide password' : 'Show password'}
|
||||
title={showPassword ? t('common.hidePassword') : t('common.showPassword')}
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Icon name={showPassword ? 'visibility-off' : 'visibility'} />
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { AVAILABLE_LANGUAGES, getLanguageConfig, ILanguageConfig } from '../../../i18n/config';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
type LanguageSwitcherProps = {
|
||||
variant?: 'dropdown' | 'buttons';
|
||||
size?: 'sm' | 'md';
|
||||
};
|
||||
|
||||
/**
|
||||
* Language switcher component that allows users to switch between supported languages
|
||||
* @param props - Component props including variant and size
|
||||
* @returns JSX element for the language switcher
|
||||
*/
|
||||
const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({
|
||||
variant = 'dropdown',
|
||||
size = 'md'
|
||||
}): React.JSX.Element => {
|
||||
const { i18n } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const currentLanguage = getLanguageConfig(i18n.language) || AVAILABLE_LANGUAGES[0];
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect((): (() => void) => {
|
||||
/**
|
||||
* Handle clicks outside the dropdown to close it
|
||||
* @param event - Mouse event
|
||||
*/
|
||||
const handleClickOutside = (event: MouseEvent): void => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Change the application language
|
||||
* @param lng - Language code to switch to
|
||||
*/
|
||||
const changeLanguage = async (lng: string): Promise<void> => {
|
||||
await i18n.changeLanguage(lng);
|
||||
await storage.setItem('local:language', lng);
|
||||
|
||||
setIsOpen(false);
|
||||
|
||||
// Force immediate re-render by dispatching the event that react-i18next listens to
|
||||
i18n.emit('languageChanged', lng);
|
||||
};
|
||||
|
||||
if (variant === 'buttons') {
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
{AVAILABLE_LANGUAGES.map((lang: ILanguageConfig) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
className={`flex items-center space-x-1 px-2 py-1 text-xs rounded transition-colors ${
|
||||
i18n.language === lang.code
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-500'
|
||||
}`}
|
||||
title={lang.nativeName}
|
||||
>
|
||||
<span className="text-sm">{lang.flag}</span>
|
||||
<span>{lang.code.toUpperCase()}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`w-full flex items-center justify-between px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors ${
|
||||
size === 'sm' ? 'text-sm' : 'text-base'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-lg">{currentLanguage.flag}</span>
|
||||
<span>{currentLanguage.nativeName}</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute top-full left-0 mt-1 w-full bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg z-50">
|
||||
{AVAILABLE_LANGUAGES.map((lang: ILanguageConfig) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
className={`w-full flex items-center justify-between px-3 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors first:rounded-t-lg last:rounded-b-lg ${
|
||||
size === 'sm' ? 'text-sm' : 'text-base'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-lg">{lang.flag}</span>
|
||||
<span className="text-gray-700 dark:text-gray-200">{lang.nativeName}</span>
|
||||
</div>
|
||||
{i18n.language === lang.code && (
|
||||
<svg className="w-4 h-4 text-primary-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSwitcher;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
type TabName = 'credentials' | 'emails' | 'settings';
|
||||
@@ -7,6 +8,7 @@ type TabName = 'credentials' | 'emails' | 'settings';
|
||||
* Bottom nav component.
|
||||
*/
|
||||
const BottomNav: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [currentTab, setCurrentTab] = useState<TabName>('credentials');
|
||||
@@ -60,7 +62,7 @@ const BottomNav: React.FC = () => {
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">Credentials</span>
|
||||
<span className="text-xs mt-1">{t('menu.credentials')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('emails')}
|
||||
@@ -71,7 +73,7 @@ const BottomNav: React.FC = () => {
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">Emails</span>
|
||||
<span className="text-xs mt-1">{t('menu.emails')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('settings')}
|
||||
@@ -83,7 +85,7 @@ const BottomNav: React.FC = () => {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">Settings</span>
|
||||
<span className="text-xs mt-1">{t('menu.settings')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
@@ -22,6 +23,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
routes = [],
|
||||
rightButtons
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const authContext = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@@ -86,7 +88,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
className="flex items-center hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img src="/assets/images/logo.svg" alt="AliasVault" className="h-8 w-8 mr-2" />
|
||||
<h1 className="text-gray-900 dark:text-white text-xl font-bold">AliasVault</h1>
|
||||
<h1 className="text-gray-900 dark:text-white text-xl font-bold">{t('common.appName')}</h1>
|
||||
{/* Hide beta badge on Safari as it's not allowed to show non-production badges */}
|
||||
{!import.meta.env.SAFARI && (
|
||||
<span className="text-primary-500 text-[10px] ml-1 font-normal">BETA</span>
|
||||
@@ -106,7 +108,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onClick={(handleSettings)}
|
||||
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className="sr-only">Settings</span>
|
||||
<span className="sr-only">{t('common.settings')}</span>
|
||||
<svg className="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useApiUrl } from '@/entrypoints/popup/utils/ApiUrlUtility';
|
||||
@@ -9,6 +10,7 @@ import { useApiUrl } from '@/entrypoints/popup/utils/ApiUrlUtility';
|
||||
const LoginServerInfo: React.FC = () => {
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -26,7 +28,7 @@ const LoginServerInfo: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 mb-4">
|
||||
(Connecting to{' '}
|
||||
({t('auth.connectingTo')}{' '}
|
||||
<button
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -24,6 +25,7 @@ const Modal: React.FC<IModalProps> = ({
|
||||
cancelText = '',
|
||||
variant = 'default'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
@@ -46,7 +48,7 @@ const Modal: React.FC<IModalProps> = ({
|
||||
className="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">{t('common.close')}</span>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { PasswordSettings } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
|
||||
interface IPasswordConfigDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (password: string) => void;
|
||||
onSettingsChange?: (settings: PasswordSettings) => void;
|
||||
initialSettings: PasswordSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Password configuration dialog component.
|
||||
*/
|
||||
const PasswordConfigDialog: React.FC<IPasswordConfigDialogProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
onSettingsChange,
|
||||
initialSettings
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [settings, setSettings] = useState<PasswordSettings>(initialSettings);
|
||||
const [previewPassword, setPreviewPassword] = useState<string>('');
|
||||
|
||||
const generatePreview = useCallback((currentSettings: PasswordSettings) => {
|
||||
try {
|
||||
const passwordGenerator = CreatePasswordGenerator(currentSettings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
setPreviewPassword(password);
|
||||
} catch (error) {
|
||||
console.error('Error generating preview password:', error);
|
||||
setPreviewPassword('');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initialize settings when dialog opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSettings({ ...initialSettings });
|
||||
generatePreview({ ...initialSettings });
|
||||
}
|
||||
}, [isOpen, initialSettings, generatePreview]);
|
||||
|
||||
const handleSettingChange = useCallback((key: keyof PasswordSettings, value: boolean | number) => {
|
||||
const newSettings = { ...settings, [key]: value };
|
||||
setSettings(newSettings);
|
||||
generatePreview(newSettings);
|
||||
onSettingsChange?.(newSettings);
|
||||
}, [settings, generatePreview, onSettingsChange]);
|
||||
|
||||
const handleRefreshPreview = useCallback(() => {
|
||||
generatePreview(settings);
|
||||
}, [settings, generatePreview]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onSave(previewPassword);
|
||||
onClose();
|
||||
}, [previewPassword, onSave, onClose]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 bg-black bg-opacity-60 transition-opacity" onClick={handleCancel} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<div className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all w-full max-w-lg">
|
||||
{/* Close button */}
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Modal content */}
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="w-full mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-white mb-4">
|
||||
{t('credentials.changePasswordComplexity')}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Password Preview */}
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={previewPassword}
|
||||
readOnly
|
||||
className="flex-1 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:text-white font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefreshPreview}
|
||||
className="px-3 py-2 text-sm text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
|
||||
title={t('credentials.generateNewPreview')}
|
||||
>
|
||||
<svg className="w-4 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Character Type Toggle Buttons */}
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* Lowercase Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseLowercase', !settings.UseLowercase)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseLowercase
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeLowercase')}
|
||||
>
|
||||
<span className="font-mono text-base">a-z</span>
|
||||
</button>
|
||||
|
||||
{/* Uppercase Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseUppercase', !settings.UseUppercase)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseUppercase
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeUppercase')}
|
||||
>
|
||||
<span className="font-mono text-base">A-Z</span>
|
||||
</button>
|
||||
|
||||
{/* Numbers Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseNumbers', !settings.UseNumbers)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseNumbers
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeNumbers')}
|
||||
>
|
||||
<span className="font-mono text-base">0-9</span>
|
||||
</button>
|
||||
|
||||
{/* Special Characters Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseSpecialChars', !settings.UseSpecialChars)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseSpecialChars
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeSpecialChars')}
|
||||
>
|
||||
<span className="font-mono text-base">!@#</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Avoid Ambiguous Characters - Checkbox */}
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="use-non-ambiguous"
|
||||
type="checkbox"
|
||||
checked={settings.UseNonAmbiguousChars}
|
||||
onChange={(e) => handleSettingChange('UseNonAmbiguousChars', e.target.checked)}
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label htmlFor="use-non-ambiguous" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('credentials.avoidAmbiguousChars')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="mt-5 sm:mt-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center items-center gap-1 rounded-md bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 sm:ml-3 sm:w-auto"
|
||||
onClick={handleSave}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 13l-3 3m0 0l-3-3m3 3V8m0 13a9 9 0 110-18 9 9 0 010 18z" />
|
||||
</svg>
|
||||
{t('common.use')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordConfigDialog;
|
||||
@@ -0,0 +1,205 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { PasswordSettings } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
|
||||
import PasswordConfigDialog from './PasswordConfigDialog';
|
||||
|
||||
interface IPasswordFieldProps {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
error?: string;
|
||||
showPassword?: boolean;
|
||||
onShowPasswordChange?: (show: boolean) => void;
|
||||
initialSettings: PasswordSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Password field component with inline length slider and advanced configuration.
|
||||
*/
|
||||
const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
error,
|
||||
showPassword: controlledShowPassword,
|
||||
onShowPasswordChange,
|
||||
initialSettings
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [internalShowPassword, setInternalShowPassword] = useState(false);
|
||||
const [showConfigDialog, setShowConfigDialog] = useState(false);
|
||||
const [currentSettings, setCurrentSettings] = useState<PasswordSettings>(initialSettings);
|
||||
|
||||
// Use controlled or uncontrolled showPassword state
|
||||
const showPassword = controlledShowPassword !== undefined ? controlledShowPassword : internalShowPassword;
|
||||
|
||||
/**
|
||||
* Set the showPassword state.
|
||||
*/
|
||||
const setShowPassword = useCallback((show: boolean): void => {
|
||||
if (controlledShowPassword !== undefined) {
|
||||
onShowPasswordChange?.(show);
|
||||
} else {
|
||||
setInternalShowPassword(show);
|
||||
}
|
||||
}, [controlledShowPassword, onShowPasswordChange]);
|
||||
|
||||
// Initialize settings only once when component mounts
|
||||
useEffect(() => {
|
||||
setCurrentSettings({ ...initialSettings });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // Only run on mount to avoid resetting user changes
|
||||
|
||||
const generatePassword = useCallback((settings: PasswordSettings) => {
|
||||
try {
|
||||
const passwordGenerator = CreatePasswordGenerator(settings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
onChange(password);
|
||||
setShowPassword(true);
|
||||
} catch (error) {
|
||||
console.error('Error generating password:', error);
|
||||
}
|
||||
}, [onChange, setShowPassword]);
|
||||
|
||||
const handleLengthChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const length = parseInt(e.target.value, 10);
|
||||
const newSettings = { ...currentSettings, Length: length };
|
||||
setCurrentSettings(newSettings);
|
||||
|
||||
// Always generate password when length changes
|
||||
generatePassword(newSettings);
|
||||
}, [currentSettings, generatePassword]);
|
||||
|
||||
const handleRegeneratePassword = useCallback(() => {
|
||||
generatePassword(currentSettings);
|
||||
}, [generatePassword, currentSettings]);
|
||||
|
||||
const handleConfiguredPassword = useCallback((password: string) => {
|
||||
onChange(password);
|
||||
setShowPassword(true);
|
||||
}, [onChange, setShowPassword]);
|
||||
|
||||
const handleAdvancedSettingsChange = useCallback((newSettings: PasswordSettings) => {
|
||||
setCurrentSettings(newSettings);
|
||||
}, []);
|
||||
|
||||
const togglePasswordVisibility = useCallback(() => {
|
||||
setShowPassword(!showPassword);
|
||||
}, [showPassword, setShowPassword]);
|
||||
|
||||
const openConfigDialog = useCallback(() => {
|
||||
setShowConfigDialog(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Label */}
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{label}
|
||||
</label>
|
||||
|
||||
{/* Password Input with Buttons */}
|
||||
<div className="flex">
|
||||
<div className="relative flex-grow">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="outline-0 shadow-sm border border-gray-300 bg-gray-50 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{/* Show/Hide Password Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePasswordVisibility}
|
||||
className="px-3 text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium text-sm dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
|
||||
title={showPassword ? t('common.hidePassword') : t('common.showPassword')}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{showPassword ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||
) : (
|
||||
<>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Generate Password Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRegeneratePassword}
|
||||
className="px-3 text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-r-lg text-sm border-l border-gray-300 dark:border-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
|
||||
title={t('credentials.generateRandomPassword')}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inline Password Length Slider */}
|
||||
<div className="pt-2">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label htmlFor={`${id}-length`} className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{t('credentials.passwordLength')}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
||||
{currentSettings.Length}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openConfigDialog}
|
||||
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
title={t('credentials.changePasswordComplexity')}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
id={`${id}-length`}
|
||||
min="8"
|
||||
max="64"
|
||||
value={currentSettings.Length}
|
||||
onChange={handleLengthChange}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
)}
|
||||
|
||||
{/* Advanced Configuration Dialog */}
|
||||
<PasswordConfigDialog
|
||||
isOpen={showConfigDialog}
|
||||
onClose={() => setShowConfigDialog(false)}
|
||||
onSave={handleConfiguredPassword}
|
||||
onSettingsChange={handleAdvancedSettingsChange}
|
||||
initialSettings={currentSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordField;
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IUsernameFieldProps {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
error?: string;
|
||||
onRegenerate: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Username field component with regenerate functionality.
|
||||
*/
|
||||
const UsernameField: React.FC<IUsernameFieldProps> = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
error,
|
||||
onRegenerate
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value);
|
||||
}, [onChange]);
|
||||
|
||||
const handleRegenerate = useCallback(() => {
|
||||
onRegenerate();
|
||||
}, [onRegenerate]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Label */}
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{label}
|
||||
</label>
|
||||
|
||||
{/* Username Input with Button */}
|
||||
<div className="flex">
|
||||
<div className="relative flex-grow">
|
||||
<input
|
||||
type="text"
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder}
|
||||
className="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{/* Generate Username Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRegenerate}
|
||||
className="px-3 text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-r-lg text-sm dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
|
||||
title={t('credentials.generateRandomUsername')}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsernameField;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
@@ -22,8 +23,9 @@ export function useVaultMutate() : {
|
||||
isLoading: boolean;
|
||||
syncStatus: string;
|
||||
} {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [syncStatus, setSyncStatus] = useState('Syncing vault');
|
||||
const [syncStatus, setSyncStatus] = useState(t('common.syncingVault'));
|
||||
const dbContext = useDb();
|
||||
const { syncVault } = useVaultSync();
|
||||
|
||||
@@ -34,12 +36,12 @@ export function useVaultMutate() : {
|
||||
operation: () => Promise<void>,
|
||||
options: VaultMutationOptions
|
||||
) : Promise<void> => {
|
||||
setSyncStatus('Saving changes to vault');
|
||||
setSyncStatus(t('common.savingChangesToVault'));
|
||||
|
||||
// Execute the provided operation (e.g. create/update/delete credential)
|
||||
await operation();
|
||||
|
||||
setSyncStatus('Uploading vault to server');
|
||||
setSyncStatus(t('common.uploadingVaultToServer'));
|
||||
|
||||
try {
|
||||
// Upload the updated vault to the server.
|
||||
@@ -90,7 +92,7 @@ export function useVaultMutate() : {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}, [dbContext]);
|
||||
}, [dbContext, t]);
|
||||
|
||||
/**
|
||||
* Hook to execute a vault mutation which uploads a new encrypted vault to the server
|
||||
@@ -101,11 +103,11 @@ export function useVaultMutate() : {
|
||||
) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setSyncStatus('Checking for vault updates');
|
||||
setSyncStatus(t('common.checkingVaultUpdates'));
|
||||
|
||||
// Skip sync check if requested (e.g., during upgrade operations)
|
||||
if (options.skipSyncCheck) {
|
||||
setSyncStatus('Executing operation...');
|
||||
setSyncStatus(t('common.executingOperation'));
|
||||
await executeMutateOperation(operation, options);
|
||||
return;
|
||||
}
|
||||
@@ -154,7 +156,7 @@ export function useVaultMutate() : {
|
||||
setIsLoading(false);
|
||||
setSyncStatus('');
|
||||
}
|
||||
}, [syncVault, executeMutateOperation]);
|
||||
}, [syncVault, executeMutateOperation, t]);
|
||||
|
||||
return {
|
||||
executeVaultMutation,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
@@ -46,6 +47,7 @@ type VaultSyncOptions = {
|
||||
export const useVaultSync = () : {
|
||||
syncVault: (options?: VaultSyncOptions) => Promise<boolean>;
|
||||
} => {
|
||||
const { t } = useTranslation();
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
@@ -65,7 +67,7 @@ export const useVaultSync = () : {
|
||||
}
|
||||
|
||||
// Check app status and vault revision
|
||||
onStatus?.('Checking vault updates');
|
||||
onStatus?.(t('common.checkingVaultUpdates'));
|
||||
const statusResponse = await withMinimumDelay(() => webApi.getStatus(), 300, enableDelay);
|
||||
|
||||
// Check if server is actually available, 0.0.0 indicates connection error which triggers offline mode.
|
||||
@@ -75,7 +77,7 @@ export const useVaultSync = () : {
|
||||
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError) {
|
||||
onError?.(statusError);
|
||||
onError?.(t('common.errors.' + statusError));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -90,10 +92,10 @@ export const useVaultSync = () : {
|
||||
const vaultRevisionNumber = vaultMetadata?.vaultRevisionNumber ?? 0;
|
||||
|
||||
if (statusResponse.vaultRevision > vaultRevisionNumber) {
|
||||
onStatus?.('Syncing updated vault');
|
||||
onStatus?.(t('common.syncingUpdatedVault'));
|
||||
const vaultResponseJson = await withMinimumDelay(() => webApi.get<VaultResponse>('Vault'), 1000, enableDelay);
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson as VaultResponse);
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson as VaultResponse, t);
|
||||
if (vaultError) {
|
||||
// Only logout if it's an authentication error, not a network error
|
||||
if (vaultError.includes('authentication') || vaultError.includes('unauthorized')) {
|
||||
@@ -169,7 +171,7 @@ export const useVaultSync = () : {
|
||||
onError?.(errorMessage);
|
||||
return false;
|
||||
}
|
||||
}, [authContext, dbContext, webApi]);
|
||||
}, [authContext, dbContext, webApi, t]);
|
||||
|
||||
return { syncVault };
|
||||
};
|
||||
@@ -8,19 +8,33 @@ import { LoadingProvider } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { ThemeProvider } from '@/entrypoints/popup/context/ThemeContext';
|
||||
import { WebApiProvider } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
<DbProvider>
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<LoadingProvider>
|
||||
<HeaderButtonsProvider>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</HeaderButtonsProvider>
|
||||
</LoadingProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
</DbProvider>
|
||||
);
|
||||
import i18n from '@/i18n/i18n';
|
||||
|
||||
/**
|
||||
* Renders the main application.
|
||||
*/
|
||||
const renderApp = (): void => {
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
<DbProvider>
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<LoadingProvider>
|
||||
<HeaderButtonsProvider>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</HeaderButtonsProvider>
|
||||
</LoadingProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
</DbProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// Wait for i18n to be ready before rendering React. Not waiting can cause issues on some browsers, Firefox on Windows specifically.
|
||||
if (i18n.isInitialized) {
|
||||
renderApp();
|
||||
} else {
|
||||
i18n.on('initialized', renderApp);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import LanguageSwitcher from '@/entrypoints/popup/components/LanguageSwitcher';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
@@ -19,10 +21,13 @@ const DEFAULT_OPTIONS: ApiOption[] = [
|
||||
];
|
||||
|
||||
// Validation schema for URLs
|
||||
const urlSchema = Yup.object().shape({
|
||||
/**
|
||||
* Creates a URL validation schema with localized error messages.
|
||||
*/
|
||||
const createUrlSchema = (t: (key: string) => string): Yup.ObjectSchema<{apiUrl: string; clientUrl: string}> => Yup.object().shape({
|
||||
apiUrl: Yup.string()
|
||||
.required('API URL is required')
|
||||
.test('is-valid-api-url', 'Please enter a valid API URL', (value: string | undefined) => {
|
||||
.required(t('validation.apiUrlRequired'))
|
||||
.test('is-valid-api-url', t('settings.validation.apiUrlInvalid'), (value: string | undefined) => {
|
||||
if (!value) {
|
||||
return true; // Allow empty for non-custom option
|
||||
}
|
||||
@@ -34,8 +39,8 @@ const urlSchema = Yup.object().shape({
|
||||
}
|
||||
}),
|
||||
clientUrl: Yup.string()
|
||||
.required('Client URL is required')
|
||||
.test('is-valid-client-url', 'Please enter a valid client URL', (value: string | undefined) => {
|
||||
.required(t('validation.clientUrlRequired'))
|
||||
.test('is-valid-client-url', t('settings.validation.clientUrlInvalid'), (value: string | undefined) => {
|
||||
if (!value) {
|
||||
return true; // Allow empty for non-custom option
|
||||
}
|
||||
@@ -52,6 +57,7 @@ const urlSchema = Yup.object().shape({
|
||||
* Auth settings page only shown when user is not logged in.
|
||||
*/
|
||||
const AuthSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedOption, setSelectedOption] = useState<string>('');
|
||||
const [customUrl, setCustomUrl] = useState<string>('');
|
||||
const [customClientUrl, setCustomClientUrl] = useState<string>('');
|
||||
@@ -59,6 +65,8 @@ const AuthSettings: React.FC = () => {
|
||||
const [errors, setErrors] = useState<{ apiUrl?: string; clientUrl?: string }>({});
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
const urlSchema = createUrlSchema(t);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load the stored settings from the storage.
|
||||
@@ -165,9 +173,17 @@ const AuthSettings: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
{/* Language Settings Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('common.language')}</p>
|
||||
<LanguageSwitcher variant="dropdown" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="api-connection" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
API Connection
|
||||
{t('settings.serverUrl')}
|
||||
</label>
|
||||
<select
|
||||
value={selectedOption}
|
||||
@@ -222,7 +238,7 @@ const AuthSettings: React.FC = () => {
|
||||
{/* Autofill Popup Settings Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Autofill popup</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillEnabled')}</p>
|
||||
<button
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
@@ -231,13 +247,13 @@ const AuthSettings: React.FC = () => {
|
||||
: 'bg-red-200 text-red-800 hover:bg-red-300 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50'
|
||||
}`}
|
||||
>
|
||||
{isGloballyEnabled ? 'Enabled' : 'Disabled'}
|
||||
{isGloballyEnabled ? t('common.enabled', 'Enabled') : t('common.disabled', 'Disabled')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version: {AppInfo.VERSION}
|
||||
{t('settings.version')}: {AppInfo.VERSION}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import AttachmentUploader from '@/entrypoints/popup/components/CredentialDetails/AttachmentUploader';
|
||||
import { FormInput } from '@/entrypoints/popup/components/FormInput';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import Modal from '@/entrypoints/popup/components/Modal';
|
||||
import PasswordField from '@/entrypoints/popup/components/PasswordField';
|
||||
import UsernameField from '@/entrypoints/popup/components/UsernameField';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import { IdentityHelperUtils, CreateIdentityGenerator, CreateUsernameEmailGenerator, Identity, Gender } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import type { Attachment, Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
|
||||
type CredentialMode = 'random' | 'manual';
|
||||
|
||||
// Persisted form data type used for JSON serialization.
|
||||
@@ -32,52 +35,58 @@ type PersistedFormData = {
|
||||
formValues: Omit<Credential, 'Logo'> & { Logo?: string | null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema for the credential form.
|
||||
*/
|
||||
const credentialSchema = Yup.object().shape({
|
||||
Id: Yup.string(),
|
||||
ServiceName: Yup.string().required('Service name is required'),
|
||||
ServiceUrl: Yup.string().url('Invalid URL format').nullable().optional(),
|
||||
Alias: Yup.object().shape({
|
||||
FirstName: Yup.string().nullable().optional(),
|
||||
LastName: Yup.string().nullable().optional(),
|
||||
NickName: Yup.string().nullable().optional(),
|
||||
BirthDate: Yup.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
.test(
|
||||
'is-valid-date-format',
|
||||
'Date must be in YYYY-MM-DD format',
|
||||
value => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(value);
|
||||
},
|
||||
),
|
||||
Gender: Yup.string().nullable().optional(),
|
||||
Email: Yup.string().email('Invalid email format').nullable().optional()
|
||||
}),
|
||||
Username: Yup.string().nullable().optional(),
|
||||
Password: Yup.string().nullable().optional(),
|
||||
Notes: Yup.string().nullable().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* Add or edit credential page.
|
||||
*/
|
||||
const CredentialAddEdit: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const dbContext = useDb();
|
||||
// If we received an ID, we're in edit mode
|
||||
const isEditMode = id !== undefined && id.length > 0;
|
||||
|
||||
/**
|
||||
* Validation schema for the credential form with translatable messages.
|
||||
*/
|
||||
const credentialSchema = useMemo(() => Yup.object().shape({
|
||||
Id: Yup.string(),
|
||||
ServiceName: Yup.string().required(t('credentials.validation.serviceNameRequired')),
|
||||
ServiceUrl: Yup.string().url(t('credentials.validation.invalidUrl')).nullable().optional(),
|
||||
Alias: Yup.object().shape({
|
||||
FirstName: Yup.string().nullable().optional(),
|
||||
LastName: Yup.string().nullable().optional(),
|
||||
NickName: Yup.string().nullable().optional(),
|
||||
BirthDate: Yup.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
.test(
|
||||
'is-valid-date-format',
|
||||
t('credentials.validation.invalidDateFormat'),
|
||||
value => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(value);
|
||||
},
|
||||
),
|
||||
Gender: Yup.string().nullable().optional(),
|
||||
Email: Yup.string().email(t('credentials.validation.invalidEmail')).nullable().optional()
|
||||
}),
|
||||
Username: Yup.string().nullable().optional(),
|
||||
Password: Yup.string().nullable().optional(),
|
||||
Notes: Yup.string().nullable().optional()
|
||||
}), [t]);
|
||||
|
||||
const { executeVaultMutation, isLoading, syncStatus } = useVaultMutate();
|
||||
const [mode, setMode] = useState<CredentialMode>('random');
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [localLoading, setLocalLoading] = useState(true);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(!isEditMode);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
|
||||
const webApi = useWebApi();
|
||||
|
||||
const serviceNameRef = useRef<HTMLInputElement>(null);
|
||||
@@ -141,9 +150,6 @@ const CredentialAddEdit: React.FC = () => {
|
||||
return (): void => subscription.unsubscribe();
|
||||
}, [watch, persistFormValues]);
|
||||
|
||||
// If we received an ID, we're in edit mode
|
||||
const isEditMode = id !== undefined && id.length > 0;
|
||||
|
||||
/**
|
||||
* Loads persisted form values from storage. This is used to keep track of form changes
|
||||
* and restore them when the page is reloaded. The browser extension popup will close
|
||||
@@ -223,7 +229,15 @@ const CredentialAddEdit: React.FC = () => {
|
||||
setIsInitialLoading(false);
|
||||
|
||||
// Load persisted form values if they exist.
|
||||
loadPersistedValues();
|
||||
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);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -238,6 +252,11 @@ const CredentialAddEdit: React.FC = () => {
|
||||
setValue(key as keyof Credential, value);
|
||||
});
|
||||
|
||||
// Load attachments for this credential
|
||||
const credentialAttachments = dbContext.sqliteClient.getAttachmentsForCredential(id);
|
||||
setAttachments(credentialAttachments);
|
||||
setOriginalAttachmentIds(credentialAttachments.map(a => a.Id));
|
||||
|
||||
setMode('manual');
|
||||
setIsInitialLoading(false);
|
||||
|
||||
@@ -251,7 +270,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
console.error('Error loading credential:', err);
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues]);
|
||||
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues, watch]);
|
||||
|
||||
/**
|
||||
* Handle the delete button click.
|
||||
@@ -368,16 +387,9 @@ const CredentialAddEdit: React.FC = () => {
|
||||
}
|
||||
}, [setValue, watch]);
|
||||
|
||||
const generateRandomPassword = useCallback(async () => {
|
||||
try {
|
||||
const { passwordGenerator } = await initializeGenerators();
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
setValue('Password', password);
|
||||
setShowPassword(true);
|
||||
} catch (error) {
|
||||
console.error('Error generating random password:', error);
|
||||
}
|
||||
}, [initializeGenerators, setValue]);
|
||||
const initialPasswordSettings = useMemo(() => {
|
||||
return dbContext.sqliteClient?.getPasswordSettings();
|
||||
}, [dbContext.sqliteClient]);
|
||||
|
||||
/**
|
||||
* Handle form submission.
|
||||
@@ -427,9 +439,9 @@ const CredentialAddEdit: React.FC = () => {
|
||||
setLocalLoading(false);
|
||||
|
||||
if (isEditMode) {
|
||||
await dbContext.sqliteClient!.updateCredentialById(data);
|
||||
await dbContext.sqliteClient!.updateCredentialById(data, originalAttachmentIds, attachments);
|
||||
} else {
|
||||
const credentialId = await dbContext.sqliteClient!.createCredential(data);
|
||||
const credentialId = await dbContext.sqliteClient!.createCredential(data, attachments);
|
||||
data.Id = credentialId.toString();
|
||||
}
|
||||
}, {
|
||||
@@ -448,7 +460,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues]);
|
||||
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues, originalAttachmentIds, attachments]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
@@ -458,14 +470,14 @@ const CredentialAddEdit: React.FC = () => {
|
||||
{isEditMode && (
|
||||
<HeaderButton
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
title="Delete credential"
|
||||
title={t('credentials.deleteCredential')}
|
||||
iconType={HeaderIconType.DELETE}
|
||||
variant="danger"
|
||||
/>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
title="Save credential"
|
||||
title={t('credentials.saveCredential')}
|
||||
iconType={HeaderIconType.SAVE}
|
||||
/>
|
||||
</div>
|
||||
@@ -473,7 +485,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => {};
|
||||
}, [setHeaderButtons, handleSubmit, onSubmit, isEditMode]);
|
||||
}, [setHeaderButtons, handleSubmit, onSubmit, isEditMode, t]);
|
||||
|
||||
// Clear header buttons on unmount
|
||||
useEffect((): (() => void) => {
|
||||
@@ -481,7 +493,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
if (isEditMode && !watch('ServiceName')) {
|
||||
return <div>Loading...</div>;
|
||||
return <div>{t('common.loading')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -503,10 +515,10 @@ const CredentialAddEdit: React.FC = () => {
|
||||
setShowDeleteModal(false);
|
||||
void handleDelete();
|
||||
}}
|
||||
title="Delete Credential"
|
||||
message="Are you sure you want to delete this credential? This action cannot be undone."
|
||||
confirmText="Delete"
|
||||
cancelText="Cancel"
|
||||
title={t('credentials.deleteCredentialTitle')}
|
||||
message={t('credentials.deleteCredentialConfirm')}
|
||||
confirmText={t('common.delete')}
|
||||
cancelText={t('common.cancel')}
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
@@ -527,7 +539,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<circle cx="8" cy="16" r="1"/>
|
||||
<circle cx="16" cy="16" r="1"/>
|
||||
</svg>
|
||||
Random Alias
|
||||
{t('credentials.randomAlias')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -540,18 +552,18 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
<path d="M5.5 20a6.5 6.5 0 0 1 13 0"/>
|
||||
</svg>
|
||||
Manual
|
||||
{t('credentials.manual')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Service</h2>
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">{t('credentials.service')}</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
id="serviceName"
|
||||
label="Service Name"
|
||||
label={t('credentials.serviceName')}
|
||||
ref={serviceNameRef}
|
||||
value={watch('ServiceName') ?? ''}
|
||||
onChange={(value) => setValue('ServiceName', value)}
|
||||
@@ -560,7 +572,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
/>
|
||||
<FormInput
|
||||
id="serviceUrl"
|
||||
label="Service URL"
|
||||
label={t('credentials.serviceUrl')}
|
||||
value={watch('ServiceUrl') ?? ''}
|
||||
onChange={(value) => setValue('ServiceUrl', value)}
|
||||
error={errors.ServiceUrl?.message}
|
||||
@@ -571,91 +583,88 @@ const CredentialAddEdit: React.FC = () => {
|
||||
{(mode === 'manual' || isEditMode) && (
|
||||
<>
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Login Credentials</h2>
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">{t('credentials.loginCredentials')}</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
id="username"
|
||||
label="Username"
|
||||
value={watch('Username') ?? ''}
|
||||
onChange={(value) => setValue('Username', value)}
|
||||
error={errors.Username?.message}
|
||||
buttons={[
|
||||
{
|
||||
icon: 'refresh',
|
||||
onClick: generateRandomUsername,
|
||||
title: 'Generate random username'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<FormInput
|
||||
id="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
value={watch('Password') ?? ''}
|
||||
onChange={(value) => setValue('Password', value)}
|
||||
error={errors.Password?.message}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
buttons={[
|
||||
{
|
||||
icon: 'refresh',
|
||||
onClick: generateRandomPassword,
|
||||
title: 'Generate random password'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateRandomAlias}
|
||||
className="w-full 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"
|
||||
>
|
||||
Generate Random Alias
|
||||
</button>
|
||||
<FormInput
|
||||
id="email"
|
||||
label="Email"
|
||||
label={t('common.email')}
|
||||
value={watch('Alias.Email') ?? ''}
|
||||
onChange={(value) => setValue('Alias.Email', value)}
|
||||
error={errors.Alias?.Email?.message}
|
||||
/>
|
||||
<UsernameField
|
||||
id="username"
|
||||
label={t('common.username')}
|
||||
value={watch('Username') ?? ''}
|
||||
onChange={(value) => setValue('Username', value)}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Alias</h2>
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">{t('credentials.alias')}</h2>
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateRandomAlias}
|
||||
className="w-full 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"
|
||||
>
|
||||
<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"
|
||||
label="First Name"
|
||||
label={t('credentials.firstName')}
|
||||
value={watch('Alias.FirstName') ?? ''}
|
||||
onChange={(value) => setValue('Alias.FirstName', value)}
|
||||
error={errors.Alias?.FirstName?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="lastName"
|
||||
label="Last Name"
|
||||
label={t('credentials.lastName')}
|
||||
value={watch('Alias.LastName') ?? ''}
|
||||
onChange={(value) => setValue('Alias.LastName', value)}
|
||||
error={errors.Alias?.LastName?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="nickName"
|
||||
label="Nick Name"
|
||||
label={t('credentials.nickName')}
|
||||
value={watch('Alias.NickName') ?? ''}
|
||||
onChange={(value) => setValue('Alias.NickName', value)}
|
||||
error={errors.Alias?.NickName?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="gender"
|
||||
label="Gender"
|
||||
label={t('credentials.gender')}
|
||||
value={watch('Alias.Gender') ?? ''}
|
||||
onChange={(value) => setValue('Alias.Gender', value)}
|
||||
error={errors.Alias?.Gender?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="birthDate"
|
||||
label="Birth Date"
|
||||
placeholder="YYYY-MM-DD"
|
||||
label={t('credentials.birthDate')}
|
||||
placeholder={t('credentials.birthDatePlaceholder')}
|
||||
value={watch('Alias.BirthDate') ?? ''}
|
||||
onChange={(value) => setValue('Alias.BirthDate', value)}
|
||||
error={errors.Alias?.BirthDate?.message}
|
||||
@@ -664,11 +673,11 @@ const CredentialAddEdit: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Metadata</h2>
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">{t('credentials.metadata')}</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
id="notes"
|
||||
label="Notes"
|
||||
label={t('credentials.notes')}
|
||||
value={watch('Notes') ?? ''}
|
||||
onChange={(value) => setValue('Notes', value)}
|
||||
multiline
|
||||
@@ -677,6 +686,12 @@ const CredentialAddEdit: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AttachmentUploader
|
||||
attachments={attachments}
|
||||
onAttachmentsChange={setAttachments}
|
||||
originalAttachmentIds={originalAttachmentIds}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
@@ -7,7 +8,8 @@ import {
|
||||
TotpBlock,
|
||||
LoginCredentialsBlock,
|
||||
AliasBlock,
|
||||
NotesBlock
|
||||
NotesBlock,
|
||||
AttachmentBlock
|
||||
} from '@/entrypoints/popup/components/CredentialDetails';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
@@ -22,6 +24,7 @@ import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
* Credential details page.
|
||||
*/
|
||||
const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const dbContext = useDb();
|
||||
@@ -74,20 +77,20 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<HeaderButton
|
||||
onClick={openInNewPopup}
|
||||
title="Open in new window"
|
||||
title={t('common.openInNewWindow')}
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={handleEdit}
|
||||
title="Edit credential"
|
||||
title={t('credentials.editCredential')}
|
||||
iconType={HeaderIconType.EDIT}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => {};
|
||||
}, [setHeaderButtons, handleEdit, openInNewPopup]);
|
||||
}, [setHeaderButtons, handleEdit, openInNewPopup, t]);
|
||||
|
||||
// Clear header buttons on unmount
|
||||
useEffect((): (() => void) => {
|
||||
@@ -95,7 +98,7 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
if (!credential) {
|
||||
return <div>Loading...</div>;
|
||||
return <div>{t('common.loading')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -112,6 +115,7 @@ const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
<LoginCredentialsBlock credential={credential} />
|
||||
<AliasBlock credential={credential} />
|
||||
<NotesBlock notes={credential.Notes} />
|
||||
<AttachmentBlock credentialId={credential.Id} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import CredentialCard from '@/entrypoints/popup/components/CredentialCard';
|
||||
@@ -21,6 +22,7 @@ import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
* Credentials list page.
|
||||
*/
|
||||
const CredentialsList: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const navigate = useNavigate();
|
||||
@@ -133,14 +135,23 @@ const CredentialsList: React.FC = () => {
|
||||
refreshCredentials();
|
||||
}, [dbContext?.sqliteClient, setIsLoading, setIsInitialLoading]);
|
||||
|
||||
// Add this function to filter credentials
|
||||
const filteredCredentials = credentials.filter(cred => {
|
||||
const filteredCredentials = credentials.filter(credential => {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
|
||||
/**
|
||||
* We filter credentials by searching in the following fields:
|
||||
* - Service name
|
||||
* - Username
|
||||
* - Alias email
|
||||
* - Service URL
|
||||
* - Notes
|
||||
*/
|
||||
const searchableFields = [
|
||||
cred.ServiceName?.toLowerCase(),
|
||||
cred.Username?.toLowerCase(),
|
||||
cred.Alias?.Email?.toLowerCase(),
|
||||
cred.ServiceUrl?.toLowerCase()
|
||||
credential.ServiceName?.toLowerCase(),
|
||||
credential.Username?.toLowerCase(),
|
||||
credential.Alias?.Email?.toLowerCase(),
|
||||
credential.ServiceUrl?.toLowerCase(),
|
||||
credential.Notes?.toLowerCase(),
|
||||
];
|
||||
return searchableFields.some(field => field?.includes(searchLower));
|
||||
});
|
||||
@@ -156,14 +167,14 @@ const CredentialsList: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">Credentials</h2>
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">{t('credentials.title')}</h2>
|
||||
<ReloadButton onClick={syncVaultAndRefresh} />
|
||||
</div>
|
||||
|
||||
{credentials.length > 0 ? (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search credentials..."
|
||||
placeholder={t('credentials.searchPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
autoFocus
|
||||
@@ -176,13 +187,10 @@ const CredentialsList: React.FC = () => {
|
||||
{credentials.length === 0 ? (
|
||||
<div className="text-gray-500 dark:text-gray-400 space-y-2 mb-10">
|
||||
<p className="text-sm">
|
||||
Welcome to AliasVault!
|
||||
{t('credentials.welcomeTitle')}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
If you want to create manual identities, open the full AliasVault app via the popout icon in the top right corner.
|
||||
{t('credentials.welcomeDescription')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
@@ -22,6 +23,7 @@ import { HeaderIconType } from '../components/Icons/HeaderIcons';
|
||||
* Email details page.
|
||||
*/
|
||||
const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const dbContext = useDb();
|
||||
@@ -149,13 +151,13 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<HeaderButton
|
||||
onClick={openInNewPopup}
|
||||
title="Open in new window"
|
||||
title={t('common.openInNewWindow')}
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
title="Delete email"
|
||||
title={t('emails.deleteEmail')}
|
||||
iconType={HeaderIconType.DELETE}
|
||||
variant="danger"
|
||||
/>
|
||||
@@ -166,7 +168,7 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
setHeaderButtonsConfigured(true);
|
||||
}
|
||||
return () => {};
|
||||
}, [setHeaderButtons, headerButtonsConfigured, openInNewPopup]);
|
||||
}, [setHeaderButtons, headerButtonsConfigured, openInNewPopup, t]);
|
||||
|
||||
// Clear header buttons on unmount
|
||||
useEffect((): (() => void) => {
|
||||
@@ -182,11 +184,11 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-500">Error: {error}</div>;
|
||||
return <div className="text-red-500">{t('common.error')} {error}</div>;
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
return <div className="text-gray-500">Email not found</div>;
|
||||
return <div className="text-gray-500">{t('emails.emailNotFound')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -198,10 +200,10 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
setShowDeleteModal(false);
|
||||
void handleDelete();
|
||||
}}
|
||||
title="Delete Email"
|
||||
message="Are you sure you want to delete this email? This action cannot be undone."
|
||||
confirmText="Delete"
|
||||
cancelText="Cancel"
|
||||
title={t('emails.deleteEmailTitle')}
|
||||
message={t('emails.deleteEmailConfirm')}
|
||||
confirmText={t('common.delete')}
|
||||
cancelText={t('common.cancel')}
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
@@ -212,9 +214,9 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{email.subject}</h1>
|
||||
</div>
|
||||
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>From: {email.fromDisplay} ({email.fromLocal}@{email.fromDomain})</p>
|
||||
<p>To: {email.toLocal}@{email.toDomain}</p>
|
||||
<p>Date: {new Date(email.dateSystem).toLocaleString()}</p>
|
||||
<p>{t('emails.from')} {email.fromDisplay} ({email.fromLocal}@{email.fromDomain})</p>
|
||||
<p>{t('emails.to')} {email.toLocal}@{email.toDomain}</p>
|
||||
<p>{t('emails.date')} {new Date(email.dateSystem).toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -224,10 +226,10 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
<iframe
|
||||
srcDoc={ConversionUtility.convertAnchorTagsToOpenInNewTab(email.messageHtml)}
|
||||
className="w-full min-h-[500px] border-0"
|
||||
title="Email content"
|
||||
title={t('emails.emailContent')}
|
||||
/>
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap text-gray-700 dark:text-gray-300">
|
||||
<pre className="whitespace-pre-wrap text-gray-800 p-3">
|
||||
{email.messagePlain}
|
||||
</pre>
|
||||
)}
|
||||
@@ -237,7 +239,7 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
{email.attachments && email.attachments.length > 0 && (
|
||||
<div className="p-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
Attachments
|
||||
{t('emails.attachments')}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{email.attachments.map((attachment) => (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
@@ -20,6 +21,7 @@ import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
* Emails list page.
|
||||
*/
|
||||
const EmailsList: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
@@ -64,15 +66,15 @@ const EmailsList: React.FC = () => {
|
||||
setEmails(decryptedEmails);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error('Failed to load emails');
|
||||
throw new Error(t('emails.errors.emailLoadError'));
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
setError(err instanceof Error ? err.message : t('common.errors.unknownError'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
}, [dbContext?.sqliteClient, webApi, setIsLoading, setIsInitialLoading]);
|
||||
}, [dbContext?.sqliteClient, webApi, setIsLoading, setIsInitialLoading, t]);
|
||||
|
||||
useEffect(() => {
|
||||
loadEmails();
|
||||
@@ -83,7 +85,7 @@ const EmailsList: React.FC = () => {
|
||||
const headerButtonsJSX = !PopoutUtility.isPopup() ? (
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
title={t('common.openInNewWindow')}
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
) : null;
|
||||
@@ -93,7 +95,7 @@ const EmailsList: React.FC = () => {
|
||||
return () => {
|
||||
setHeaderButtons(null);
|
||||
};
|
||||
}, [setHeaderButtons]);
|
||||
}, [setHeaderButtons, t]);
|
||||
|
||||
/**
|
||||
* Formats the date display for emails
|
||||
@@ -104,18 +106,26 @@ const EmailsList: React.FC = () => {
|
||||
const secondsAgo = Math.floor((now.getTime() - emailDate.getTime()) / 1000);
|
||||
|
||||
if (secondsAgo < 60) {
|
||||
return 'just now';
|
||||
return t('emails.dateFormat.justNow');
|
||||
} else if (secondsAgo < 3600) {
|
||||
// Less than 1 hour ago
|
||||
const minutes = Math.floor(secondsAgo / 60);
|
||||
return `${minutes} ${minutes === 1 ? 'min' : 'mins'} ago`;
|
||||
if (minutes === 1) {
|
||||
return t('emails.dateFormat.minutesAgo_single', { count: minutes });
|
||||
} else {
|
||||
return t('emails.dateFormat.minutesAgo_plural', { count: minutes });
|
||||
}
|
||||
} else if (secondsAgo < 86400) {
|
||||
// Less than 24 hours ago
|
||||
const hours = Math.floor(secondsAgo / 3600);
|
||||
return `${hours} ${hours === 1 ? 'hr' : 'hrs'} ago`;
|
||||
if (hours === 1) {
|
||||
return t('emails.dateFormat.hoursAgo_single', { count: hours });
|
||||
} else {
|
||||
return t('emails.dateFormat.hoursAgo_plural', { count: hours });
|
||||
}
|
||||
} else if (secondsAgo < 172800) {
|
||||
// Less than 48 hours ago
|
||||
return 'yesterday';
|
||||
return t('emails.dateFormat.yesterday');
|
||||
} else {
|
||||
// Older than 48 hours
|
||||
return emailDate.toLocaleDateString('en-GB', {
|
||||
@@ -134,19 +144,19 @@ const EmailsList: React.FC = () => {
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-500">Error: {error}</div>;
|
||||
return <div className="text-red-500">{t('common.error')}: {error}</div>;
|
||||
}
|
||||
|
||||
if (emails.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">Emails</h2>
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">{t('emails.title')}</h2>
|
||||
<ReloadButton onClick={loadEmails} />
|
||||
</div>
|
||||
<div className="text-gray-500 dark:text-gray-400 space-y-2">
|
||||
<p className="text-sm">
|
||||
You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.
|
||||
{t('emails.noEmailsDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,7 +166,7 @@ const EmailsList: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">Emails</h2>
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">{t('emails.title')}</h2>
|
||||
<ReloadButton onClick={loadEmails} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
@@ -28,6 +29,7 @@ import { storage } from '#imports';
|
||||
* Login page
|
||||
*/
|
||||
const Login: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
@@ -138,7 +140,7 @@ const Login: React.FC = () => {
|
||||
|
||||
// Check if token was returned.
|
||||
if (!validationResponse.token) {
|
||||
throw new Error('Login failed -- no token returned');
|
||||
throw new Error(t('auth.errors.noToken'));
|
||||
}
|
||||
|
||||
// Try to get latest vault manually providing auth token.
|
||||
@@ -146,7 +148,7 @@ const Login: React.FC = () => {
|
||||
'Authorization': `Bearer ${validationResponse.token.token}`
|
||||
} });
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson, t);
|
||||
if (vaultError) {
|
||||
setError(vaultError);
|
||||
hideLoading();
|
||||
@@ -171,7 +173,7 @@ const Login: React.FC = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
await authContext.logout();
|
||||
setError(err instanceof Error ? err.message : 'An error occurred while checking for pending migrations.');
|
||||
setError(err instanceof Error ? err.message : t('auth.errors.migrationError'));
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
@@ -184,9 +186,9 @@ const Login: React.FC = () => {
|
||||
} catch (err) {
|
||||
// Show API authentication errors as-is.
|
||||
if (err instanceof ApiAuthError) {
|
||||
setError(err.message);
|
||||
setError(t('common.apiErrors.' + err.message));
|
||||
} else {
|
||||
setError('Could not reach AliasVault server. Please try again later or contact support if the problem persists.');
|
||||
setError(t('auth.errors.serverError'));
|
||||
}
|
||||
hideLoading();
|
||||
}
|
||||
@@ -203,13 +205,13 @@ const Login: React.FC = () => {
|
||||
showLoading();
|
||||
|
||||
if (!passwordHashString || !passwordHashBase64 || !loginResponse) {
|
||||
throw new Error('Required login data not found');
|
||||
throw new Error(t('auth.errors.loginDataMissing'));
|
||||
}
|
||||
|
||||
// Validate that 2FA code is a 6-digit number
|
||||
const code = twoFactorCode.trim();
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
throw new ApiAuthError('Please enter a valid 6-digit authentication code.');
|
||||
throw new Error(t('auth.errors.invalidCode'));
|
||||
}
|
||||
|
||||
const validationResponse = await srpUtil.validateLogin2Fa(
|
||||
@@ -222,7 +224,7 @@ const Login: React.FC = () => {
|
||||
|
||||
// Check if token was returned.
|
||||
if (!validationResponse.token) {
|
||||
throw new Error('Login failed -- no token returned');
|
||||
throw new Error(t('auth.errors.noToken'));
|
||||
}
|
||||
|
||||
// Try to get latest vault manually providing auth token.
|
||||
@@ -230,7 +232,7 @@ const Login: React.FC = () => {
|
||||
'Authorization': `Bearer ${validationResponse.token.token}`
|
||||
} });
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson, t);
|
||||
if (vaultError) {
|
||||
setError(vaultError);
|
||||
hideLoading();
|
||||
@@ -255,7 +257,7 @@ const Login: React.FC = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
await authContext.logout();
|
||||
setError(err instanceof Error ? err.message : 'An error occurred while checking for pending migrations.');
|
||||
setError(err instanceof Error ? err.message : t('auth.errors.migrationError'));
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
@@ -274,9 +276,9 @@ const Login: React.FC = () => {
|
||||
// Show API authentication errors as-is.
|
||||
console.error('2FA error:', err);
|
||||
if (err instanceof ApiAuthError) {
|
||||
setError(err.message);
|
||||
setError(t('common.apiErrors.' + err.message));
|
||||
} else {
|
||||
setError('Could not reach AliasVault server. Please try again later or contact support if the problem persists.');
|
||||
setError(t('auth.errors.serverError'));
|
||||
}
|
||||
hideLoading();
|
||||
}
|
||||
@@ -304,10 +306,10 @@ const Login: React.FC = () => {
|
||||
)}
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-700 dark:text-gray-200 mb-4">
|
||||
Please enter the authentication code from your authenticator app.
|
||||
{t('auth.twoFactorTitle')}
|
||||
</p>
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="twoFactorCode">
|
||||
Authentication Code
|
||||
{t('auth.authCode')}
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 leading-tight focus:outline-none focus:shadow-outline"
|
||||
@@ -315,13 +317,13 @@ const Login: React.FC = () => {
|
||||
type="text"
|
||||
value={twoFactorCode}
|
||||
onChange={(e) => setTwoFactorCode(e.target.value)}
|
||||
placeholder="Enter 6-digit code"
|
||||
placeholder={t('auth.authCodePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-full space-y-2">
|
||||
<Button type="submit">
|
||||
Verify
|
||||
{t('auth.verify')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -340,11 +342,11 @@ const Login: React.FC = () => {
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
Cancel
|
||||
{t('auth.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-4 text-center">
|
||||
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.
|
||||
{t('auth.twoFactorNote')}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
@@ -359,18 +361,18 @@ const Login: React.FC = () => {
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-xl font-bold dark:text-gray-200">Log in to AliasVault</h2>
|
||||
<h2 className="text-xl font-bold dark:text-gray-200">{t('auth.loginTitle')}</h2>
|
||||
<LoginServerInfo />
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="username">
|
||||
Username or email
|
||||
{t('auth.username')}
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="username"
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="name / name@company.com"
|
||||
placeholder={t('auth.usernamePlaceholder')}
|
||||
value={credentials.username}
|
||||
onChange={handleChange}
|
||||
required
|
||||
@@ -378,14 +380,14 @@ const Login: React.FC = () => {
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="password">
|
||||
Password
|
||||
{t('auth.password')}
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Enter your password"
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
value={credentials.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
@@ -399,24 +401,24 @@ const Login: React.FC = () => {
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200">Remember me</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200">{t('auth.rememberMe')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Button type="submit">
|
||||
Login
|
||||
{t('auth.loginButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
No account yet?{' '}
|
||||
{t('auth.noAccount')}{' '}
|
||||
<a
|
||||
href={clientUrl ?? ''}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-orange-500 hover:text-orange-600 dark:text-orange-400 dark:hover:text-orange-500"
|
||||
>
|
||||
Create new vault
|
||||
{t('auth.createVault')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LanguageSwitcher from '@/entrypoints/popup/components/LanguageSwitcher';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
@@ -32,6 +34,7 @@ type PopupSettings = {
|
||||
* Settings page component.
|
||||
*/
|
||||
const Settings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const authContext = useAuth();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
@@ -77,14 +80,14 @@ const Settings: React.FC = () => {
|
||||
<>
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
title={t('settings.openInNewWindow')}
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={openClientTab}
|
||||
title="Open web app"
|
||||
title={t('settings.openWebApp')}
|
||||
iconType={HeaderIconType.EXTERNAL_LINK}
|
||||
/>
|
||||
</div>
|
||||
@@ -92,7 +95,7 @@ const Settings: React.FC = () => {
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons]);
|
||||
}, [setHeaderButtons, t]);
|
||||
|
||||
/**
|
||||
* Load settings.
|
||||
@@ -255,7 +258,7 @@ const Settings: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">Settings</h2>
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">{t('settings.title')}</h2>
|
||||
</div>
|
||||
|
||||
{/* User Menu Section */}
|
||||
@@ -276,7 +279,7 @@ const Settings: React.FC = () => {
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Logged in
|
||||
{t('settings.loggedIn')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,7 +287,7 @@ const Settings: React.FC = () => {
|
||||
onClick={handleLogout}
|
||||
className="px-4 py-2 text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
Logout
|
||||
{t('settings.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -293,14 +296,14 @@ const Settings: React.FC = () => {
|
||||
|
||||
{/* Global Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">Global Settings</h3>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.globalSettings')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Autofill popup</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillPopup')}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isGloballyEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isGloballyEnabled ? 'Active on all sites (unless disabled below)' : 'Disabled on all sites'}
|
||||
{settings.isGloballyEnabled ? t('settings.activeOnAllSites') : t('settings.disabledOnAllSites')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -311,15 +314,15 @@ const Settings: React.FC = () => {
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isGloballyEnabled ? 'Enabled' : 'Disabled'}
|
||||
{settings.isGloballyEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Right-click context menu</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.rightClickContextMenu')}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isContextMenuEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isContextMenuEnabled ? 'Enabled' : 'Disabled'}
|
||||
{settings.isContextMenuEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -330,7 +333,7 @@ const Settings: React.FC = () => {
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isContextMenuEnabled ? 'Enabled' : 'Disabled'}
|
||||
{settings.isContextMenuEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -340,18 +343,18 @@ const Settings: React.FC = () => {
|
||||
{/* Site-Specific Settings Section */}
|
||||
{settings.isGloballyEnabled && (
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">Site-Specific Settings</h3>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.siteSpecificSettings')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Autofill popup on: {settings.currentUrl}</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillPopupOn')}{settings.currentUrl}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isEnabled ? 'Enabled for this site' : 'Disabled for this site'}
|
||||
{settings.isEnabled ? t('settings.enabledForThisSite') : t('settings.disabledForThisSite')}
|
||||
</p>
|
||||
{!settings.isEnabled && settings.temporaryDisabledUrls[settings.currentUrl] && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Temporarily disabled until {new Date(settings.temporaryDisabledUrls[settings.currentUrl]).toLocaleTimeString()}
|
||||
{t('settings.temporarilyDisabledUntil')}{new Date(settings.temporaryDisabledUrls[settings.currentUrl]).toLocaleTimeString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -364,7 +367,7 @@ const Settings: React.FC = () => {
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isEnabled ? 'Enabled' : 'Disabled'}
|
||||
{settings.isEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -374,7 +377,7 @@ const Settings: React.FC = () => {
|
||||
onClick={resetSettings}
|
||||
className="w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md text-gray-700 dark:text-gray-300 transition-colors text-sm"
|
||||
>
|
||||
Reset all site-specific settings
|
||||
{t('settings.resetAllSiteSettings')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -384,11 +387,17 @@ const Settings: React.FC = () => {
|
||||
|
||||
{/* Appearance Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">Appearance</h3>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.appearance')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-3">{t('settings.language')}</p>
|
||||
<LanguageSwitcher variant="dropdown" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">Theme</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">{t('settings.theme')}</p>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
@@ -399,7 +408,7 @@ const Settings: React.FC = () => {
|
||||
onChange={() => setThemePreference('system')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Use default</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.useDefault')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
@@ -410,7 +419,7 @@ const Settings: React.FC = () => {
|
||||
onChange={() => setThemePreference('light')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Light</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.light')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
@@ -421,7 +430,7 @@ const Settings: React.FC = () => {
|
||||
onChange={() => setThemePreference('dark')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Dark</span>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.dark')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -432,18 +441,18 @@ const Settings: React.FC = () => {
|
||||
{/* Keyboard Shortcuts Section */}
|
||||
{import.meta.env.CHROME && (
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">Keyboard Shortcuts</h3>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.keyboardShortcuts')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Configure keyboard shortcuts</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.configureKeyboardShortcuts')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={openKeyboardShortcuts}
|
||||
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors"
|
||||
>
|
||||
Configure
|
||||
{t('settings.configure')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -452,7 +461,7 @@ const Settings: React.FC = () => {
|
||||
)}
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version {AppInfo.VERSION} ({getDisplayUrl()})
|
||||
{t('settings.versionPrefix')}{AppInfo.VERSION} ({getDisplayUrl()})
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
@@ -24,6 +25,7 @@ import { storage } from '#imports';
|
||||
* Unlock page
|
||||
*/
|
||||
const Unlock: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
@@ -44,21 +46,21 @@ const Unlock: React.FC = () => {
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
await webApi.logout(statusError);
|
||||
await webApi.logout(t('common.apiErrors.' + statusError));
|
||||
navigate('/logout');
|
||||
}
|
||||
setIsInitialLoading(false);
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
}, [webApi, authContext, setIsInitialLoading, navigate]);
|
||||
}, [webApi, authContext, setIsInitialLoading, navigate, t]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = !PopoutUtility.isPopup() ? (
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
title={t('common.openInNewWindow')}
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
) : null;
|
||||
@@ -68,7 +70,7 @@ const Unlock: React.FC = () => {
|
||||
return () => {
|
||||
setHeaderButtons(null);
|
||||
};
|
||||
}, [setHeaderButtons]);
|
||||
}, [setHeaderButtons, t]);
|
||||
|
||||
/**
|
||||
* Handle submit
|
||||
@@ -93,9 +95,9 @@ const Unlock: React.FC = () => {
|
||||
// Make API call to get latest vault
|
||||
const vaultResponseJson = await webApi.get<VaultResponse>('Vault');
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson, t);
|
||||
if (vaultError) {
|
||||
setError(vaultError);
|
||||
setError(t('common.apiErrors.' + vaultError));
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
@@ -112,7 +114,7 @@ const Unlock: React.FC = () => {
|
||||
// Redirect to reinitialize page
|
||||
navigate('/reinitialize', { replace: true });
|
||||
} catch (err) {
|
||||
setError('Failed to unlock vault. Please check your password and try again.');
|
||||
setError(t('auth.errors.wrongPassword'));
|
||||
console.error('Unlock error:', err);
|
||||
} finally {
|
||||
hideLoading();
|
||||
@@ -143,14 +145,14 @@ const Unlock: React.FC = () => {
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Logged in
|
||||
{t('auth.loggedIn')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instruction Title */}
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Unlock your vault
|
||||
{t('auth.unlockTitle')}
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
@@ -161,7 +163,7 @@ const Unlock: React.FC = () => {
|
||||
|
||||
<div className="mb-2">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="password">
|
||||
Password
|
||||
{t('auth.masterPassword')}
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
||||
@@ -169,17 +171,17 @@ const Unlock: React.FC = () => {
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter your password"
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit">
|
||||
Unlock
|
||||
{t('auth.unlockVault')}
|
||||
</Button>
|
||||
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-200 mt-6">
|
||||
Switch accounts? <button onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">Log out</button>
|
||||
{t('auth.switchAccounts')} <button onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">{t('auth.logout')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
@@ -7,6 +8,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
*/
|
||||
const UnlockSuccess: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
* Handle browsing vault contents - navigate to credentials page and reset mode parameter
|
||||
@@ -29,23 +31,23 @@ const UnlockSuccess: React.FC = () => {
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
Your vault is successfully unlocked
|
||||
{t('auth.unlockSuccessTitle')}
|
||||
</h2>
|
||||
<p className="mb-6 text-gray-600 dark:text-gray-400">
|
||||
You can now use autofill in login forms in your browser.
|
||||
{t('auth.unlockSuccessDescription')}
|
||||
</p>
|
||||
<div className="space-y-3 w-full">
|
||||
<button
|
||||
onClick={() => window.close()}
|
||||
className="w-full px-4 py-2 text-white bg-primary-600 rounded hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
>
|
||||
Close this popup
|
||||
{t('auth.closePopup')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBrowseVaultContents}
|
||||
className="w-full px-4 py-2 text-gray-700 bg-gray-100 rounded hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
>
|
||||
Browse vault contents
|
||||
{t('auth.browseVault')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
@@ -22,6 +23,7 @@ import { VaultSqlGenerator } from '@/utils/dist/shared/vault-sql';
|
||||
* Upgrade page for handling vault version upgrades.
|
||||
*/
|
||||
const Upgrade: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { username } = useAuth();
|
||||
const dbContext = useDb();
|
||||
const { sqliteClient } = dbContext;
|
||||
@@ -44,7 +46,7 @@ const Upgrade: React.FC = () => {
|
||||
<>
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
title={t('common.openInNewWindow')}
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
</>
|
||||
@@ -55,7 +57,7 @@ const Upgrade: React.FC = () => {
|
||||
return () => {
|
||||
setHeaderButtons(null);
|
||||
};
|
||||
}, [setHeaderButtons]);
|
||||
}, [setHeaderButtons, t]);
|
||||
|
||||
/**
|
||||
* Load version information from the database.
|
||||
@@ -71,9 +73,9 @@ const Upgrade: React.FC = () => {
|
||||
setIsInitialLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to load version information:', error);
|
||||
setError('Failed to load version information. Please try again.');
|
||||
setError(t('upgrade.alerts.unableToGetVersionInfo'));
|
||||
}
|
||||
}, [sqliteClient, setIsInitialLoading]);
|
||||
}, [sqliteClient, setIsInitialLoading, t]);
|
||||
|
||||
useEffect(() => {
|
||||
loadVersionInfo();
|
||||
@@ -84,7 +86,7 @@ const Upgrade: React.FC = () => {
|
||||
*/
|
||||
const handleUpgrade = async (): Promise<void> => {
|
||||
if (!sqliteClient || !currentVersion || !latestVersion) {
|
||||
setError('Unable to get version information. Please try again.');
|
||||
setError(t('upgrade.alerts.unableToGetVersionInfo'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,7 +104,7 @@ const Upgrade: React.FC = () => {
|
||||
*/
|
||||
const performUpgrade = async (): Promise<void> => {
|
||||
if (!sqliteClient || !currentVersion || !latestVersion) {
|
||||
setError('Unable to get version information. Please try again.');
|
||||
setError(t('upgrade.alerts.unableToGetVersionInfo'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,7 +117,7 @@ const Upgrade: React.FC = () => {
|
||||
const upgradeResult = vaultSqlGenerator.getUpgradeVaultSql(currentVersion.revision, latestVersion.revision);
|
||||
|
||||
if (!upgradeResult.success) {
|
||||
throw new Error(upgradeResult.error ?? 'Failed to generate upgrade SQL');
|
||||
throw new Error(upgradeResult.error ?? t('upgrade.alerts.upgradeFailed'));
|
||||
}
|
||||
|
||||
if (upgradeResult.sqlCommands.length === 0) {
|
||||
@@ -125,30 +127,24 @@ const Upgrade: React.FC = () => {
|
||||
}
|
||||
|
||||
// Use the useVaultMutate hook to handle the upgrade and vault upload
|
||||
console.debug('executeVaultMutation');
|
||||
await executeVaultMutation(async () => {
|
||||
// Begin transaction
|
||||
console.debug('beginTransaction');
|
||||
sqliteClient.beginTransaction();
|
||||
|
||||
// Execute each SQL command
|
||||
console.debug('executeRaw', upgradeResult.sqlCommands.length);
|
||||
for (let i = 0; i < upgradeResult.sqlCommands.length; i++) {
|
||||
const sqlCommand = upgradeResult.sqlCommands[i];
|
||||
|
||||
try {
|
||||
console.debug('executeRaw', sqlCommand);
|
||||
sqliteClient.executeRaw(sqlCommand);
|
||||
} catch (error) {
|
||||
console.debug('error', error);
|
||||
console.error(`Error executing SQL command ${i + 1}:`, sqlCommand, error);
|
||||
sqliteClient.rollbackTransaction();
|
||||
throw new Error(`Failed to apply migration ${i + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw new Error(t('upgrade.alerts.failedToApplyMigration', { current: i + 1, total: upgradeResult.sqlCommands.length }));
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
console.debug('commitTransaction');
|
||||
sqliteClient.commitTransaction();
|
||||
}, {
|
||||
skipSyncCheck: true, // Skip sync check during upgrade to prevent loop
|
||||
@@ -156,14 +152,12 @@ const Upgrade: React.FC = () => {
|
||||
* Handle successful upgrade completion.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
console.debug('onSuccess');
|
||||
void handleUpgradeSuccess();
|
||||
},
|
||||
/**
|
||||
* Handle upgrade error.
|
||||
*/
|
||||
onError: (error: Error) => {
|
||||
console.debug('onError');
|
||||
console.error('Upgrade failed:', error);
|
||||
setError(error.message);
|
||||
}
|
||||
@@ -171,7 +165,7 @@ const Upgrade: React.FC = () => {
|
||||
console.debug('executeVaultMutation done?');
|
||||
} catch (error) {
|
||||
console.error('Upgrade failed:', error);
|
||||
setError(error instanceof Error ? error.message : 'An unknown error occurred during the upgrade. Please try again.');
|
||||
setError(error instanceof Error ? error.message : t('upgrade.alerts.unknownErrorDuringUpgrade'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -229,7 +223,7 @@ const Upgrade: React.FC = () => {
|
||||
<div className="fixed inset-0 flex flex-col justify-center items-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90 z-50">
|
||||
<LoadingSpinner />
|
||||
<div className="text-sm text-gray-500 mt-2">
|
||||
{syncStatus || 'Upgrading vault...'}
|
||||
{syncStatus || t('upgrade.upgrading')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -242,10 +236,10 @@ const Upgrade: React.FC = () => {
|
||||
setShowSelfHostedWarning(false);
|
||||
void performUpgrade();
|
||||
}}
|
||||
title="Self-Hosted Server"
|
||||
message="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. Do you want to continue with the upgrade?"
|
||||
confirmText="Continue"
|
||||
cancelText="Cancel"
|
||||
title={t('upgrade.alerts.selfHostedServer')}
|
||||
message={t('upgrade.alerts.selfHostedWarning')}
|
||||
confirmText={t('upgrade.alerts.continueUpgrade')}
|
||||
cancelText={t('upgrade.alerts.cancel')}
|
||||
/>
|
||||
|
||||
{/* Version info modal */}
|
||||
@@ -253,8 +247,8 @@ const Upgrade: React.FC = () => {
|
||||
isOpen={showVersionInfo}
|
||||
onClose={() => setShowVersionInfo(false)}
|
||||
onConfirm={() => setShowVersionInfo(false)}
|
||||
title="What's New"
|
||||
message={`An upgrade is required to support the following changes:\n\n${latestVersion?.description ?? 'No description available for this version.'}`}
|
||||
title={t('upgrade.whatsNew')}
|
||||
message={`${t('upgrade.whatsNewDescription')}\n\n${latestVersion?.description ?? t('upgrade.noDescriptionAvailable')}`}
|
||||
/>
|
||||
|
||||
<form className="w-full px-2 pt-2 pb-2 mb-4">
|
||||
@@ -280,33 +274,33 @@ const Upgrade: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-bold dark:text-gray-200 mb-4">Upgrade Vault</h2>
|
||||
<h2 className="text-xl font-bold dark:text-gray-200 mb-4">{t('upgrade.title')}</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-700 dark:text-gray-200 text-sm mb-4">
|
||||
AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.
|
||||
{t('upgrade.subtitle')}
|
||||
</p>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded p-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">Version Information</span>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">{t('upgrade.versionInformation')}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={showVersionDialog}
|
||||
className="bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold hover:bg-gray-300 dark:hover:bg-gray-500"
|
||||
title="Show version details"
|
||||
title={t('upgrade.whatsNew')}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Your vault:</span>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">{t('upgrade.yourVault')}</span>
|
||||
<span className="text-sm font-bold text-orange-600 dark:text-orange-400">
|
||||
{currentVersion?.releaseVersion ?? '...'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">New version:</span>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">{t('upgrade.newVersion')}</span>
|
||||
<span className="text-sm font-bold text-green-600 dark:text-green-400">
|
||||
{latestVersion?.releaseVersion ?? '...'}
|
||||
</span>
|
||||
@@ -320,7 +314,7 @@ const Upgrade: React.FC = () => {
|
||||
type="button"
|
||||
onClick={handleUpgrade}
|
||||
>
|
||||
{isLoading || isVaultMutationLoading ? (syncStatus || 'Upgrading...') : 'Upgrade Vault'}
|
||||
{isLoading || isVaultMutationLoading ? (syncStatus || t('upgrade.upgrading')) : t('upgrade.upgrade')}
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -328,7 +322,7 @@ const Upgrade: React.FC = () => {
|
||||
className="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm font-medium py-2"
|
||||
disabled={isLoading || isVaultMutationLoading}
|
||||
>
|
||||
Logout
|
||||
{t('upgrade.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
78
apps/browser-extension/src/i18n/StandaloneI18n.ts
Normal file
78
apps/browser-extension/src/i18n/StandaloneI18n.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Standalone i18n for non-React contexts.
|
||||
* This is used to translate strings in non-React contexts, such as the background and content scripts.
|
||||
*/
|
||||
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
LANGUAGE_CODES,
|
||||
loadTranslations,
|
||||
getNestedValue
|
||||
} from './config';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* Get current language from storage
|
||||
*/
|
||||
export async function getCurrentLanguage(): Promise<string> {
|
||||
try {
|
||||
// Use extension storage API exclusively (reliable across all contexts)
|
||||
const langFromStorage = await storage.getItem('local:language') as string;
|
||||
if (langFromStorage && LANGUAGE_CODES.includes(langFromStorage)) {
|
||||
return langFromStorage;
|
||||
}
|
||||
|
||||
// If no language is set in storage, detect browser language and save it
|
||||
const browserLang = navigator.language.split('-')[0];
|
||||
const detectedLanguage = LANGUAGE_CODES.includes(browserLang) ? browserLang : DEFAULT_LANGUAGE;
|
||||
|
||||
// Save the detected language to storage for future use
|
||||
await storage.setItem('local:language', detectedLanguage);
|
||||
|
||||
return detectedLanguage;
|
||||
} catch (error) {
|
||||
console.error('Failed to get current language:', error);
|
||||
return DEFAULT_LANGUAGE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translation function for non-React contexts
|
||||
*
|
||||
* @param key - Translation key (supports nested keys like 'auth.loginButton' or 'common.errors.networkError')
|
||||
* @param fallback - Fallback text if translation is not found
|
||||
* @returns Promise<string> - Translated text
|
||||
*/
|
||||
export async function t(
|
||||
key: string,
|
||||
fallback?: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const language = await getCurrentLanguage();
|
||||
const translations = await loadTranslations(language);
|
||||
|
||||
// Support nested keys like 'auth.loginButton' or 'common.errors.networkError'
|
||||
const value = getNestedValue(translations, key);
|
||||
|
||||
if (value && typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// If translation not found and we're not using English, try English fallback
|
||||
if (language !== DEFAULT_LANGUAGE) {
|
||||
const englishTranslations = await loadTranslations(DEFAULT_LANGUAGE);
|
||||
const englishValue = getNestedValue(englishTranslations, key);
|
||||
|
||||
if (englishValue && typeof englishValue === 'string') {
|
||||
return englishValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Return fallback or key if no translation found
|
||||
return fallback || key;
|
||||
} catch (error) {
|
||||
console.error('Translation error:', error);
|
||||
return fallback || key;
|
||||
}
|
||||
}
|
||||
158
apps/browser-extension/src/i18n/config.ts
Normal file
158
apps/browser-extension/src/i18n/config.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Central configuration for i18n languages
|
||||
* Add new languages here to make them available throughout the application
|
||||
*/
|
||||
|
||||
import enTranslations from './locales/en.json';
|
||||
import nlTranslations from './locales/nl.json';
|
||||
|
||||
/**
|
||||
* Create a map of all available languages and their resources for i18n.
|
||||
* When adding a new language, add the translation JSON file to the locales folder and add the language to the map here.
|
||||
*/
|
||||
export const LANGUAGE_RESOURCES = {
|
||||
en: {
|
||||
translation: enTranslations
|
||||
},
|
||||
nl: {
|
||||
translation: nlTranslations
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* List of all available languages with their code, name, native name and flag.
|
||||
* When adding a new language, add the language to the map here.
|
||||
*/
|
||||
export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
|
||||
{
|
||||
code: 'en',
|
||||
name: 'English',
|
||||
nativeName: 'English',
|
||||
flag: '🇺🇸'
|
||||
},
|
||||
{
|
||||
code: 'nl',
|
||||
name: 'Dutch',
|
||||
nativeName: 'Nederlands',
|
||||
flag: '🇳🇱'
|
||||
},
|
||||
/*
|
||||
* {
|
||||
* code: 'de',
|
||||
* name: 'German',
|
||||
* nativeName: 'Deutsch',
|
||||
* flag: '🇩🇪'
|
||||
* },
|
||||
* {
|
||||
* code: 'es',
|
||||
* name: 'Spanish',
|
||||
* nativeName: 'Español',
|
||||
* flag: '🇪🇸'
|
||||
* },
|
||||
* {
|
||||
* code: 'fr',
|
||||
* name: 'French',
|
||||
* nativeName: 'Français',
|
||||
* flag: '🇫🇷'
|
||||
* },
|
||||
* {
|
||||
* code: 'uk',
|
||||
* name: 'Ukrainian',
|
||||
* nativeName: 'Українська',
|
||||
* flag: '🇺🇦'
|
||||
* }
|
||||
*/
|
||||
];
|
||||
|
||||
/**
|
||||
* Default language that is used when no language is set in the browser or when a localized string is not found for the current language.
|
||||
*/
|
||||
export const DEFAULT_LANGUAGE = 'en';
|
||||
|
||||
export const LANGUAGE_CODES = AVAILABLE_LANGUAGES.map(lang => lang.code);
|
||||
|
||||
export interface ILanguageConfig {
|
||||
code: string;
|
||||
name: string;
|
||||
nativeName: string;
|
||||
flag?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for content translations
|
||||
*/
|
||||
export type ContentTranslations = {
|
||||
[key: string]: string | ContentTranslations;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache for loaded translations to avoid repeated file reads
|
||||
*/
|
||||
const translationCache = new Map<string, ContentTranslations>();
|
||||
|
||||
/**
|
||||
* Load translations for a specific language
|
||||
*/
|
||||
export async function loadTranslations(language: string): Promise<ContentTranslations> {
|
||||
const cacheKey = `all:${language}`;
|
||||
|
||||
// Check cache first
|
||||
if (translationCache.has(cacheKey)) {
|
||||
return translationCache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
// Get translations from pre-loaded resources
|
||||
if (LANGUAGE_RESOURCES[language as keyof typeof LANGUAGE_RESOURCES]) {
|
||||
const translationData = LANGUAGE_RESOURCES[language as keyof typeof LANGUAGE_RESOURCES].translation;
|
||||
translationCache.set(cacheKey, translationData);
|
||||
return translationData;
|
||||
}
|
||||
|
||||
// Fallback to English if available
|
||||
if (language !== DEFAULT_LANGUAGE && LANGUAGE_RESOURCES[DEFAULT_LANGUAGE]) {
|
||||
console.warn(`Translations not found for ${language}, falling back to ${DEFAULT_LANGUAGE}`);
|
||||
const fallbackData = LANGUAGE_RESOURCES[DEFAULT_LANGUAGE].translation;
|
||||
translationCache.set(cacheKey, fallbackData);
|
||||
return fallbackData;
|
||||
}
|
||||
|
||||
// Return empty object as last resort
|
||||
console.warn(`No translations found for ${language} and no fallback available`);
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all available translations for i18next
|
||||
*/
|
||||
export async function loadAllTranslations(): Promise<Record<string, { translation: ContentTranslations }>> {
|
||||
const resources: Record<string, { translation: ContentTranslations }> = {};
|
||||
|
||||
for (const language of AVAILABLE_LANGUAGES) {
|
||||
try {
|
||||
const translations = await loadTranslations(language.code);
|
||||
resources[language.code] = { translation: translations };
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load translations for ${language.code}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get language config by code
|
||||
*/
|
||||
export function getLanguageConfig(code: string): ILanguageConfig | undefined {
|
||||
return AVAILABLE_LANGUAGES.find(lang => lang.code === code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nested value from object using dot notation
|
||||
*/
|
||||
export function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
||||
return path.split('.').reduce((current: unknown, key: string) => {
|
||||
return current && typeof current === 'object' && current !== null && key in current
|
||||
? (current as Record<string, unknown>)[key]
|
||||
: undefined;
|
||||
}, obj);
|
||||
}
|
||||
62
apps/browser-extension/src/i18n/i18n.ts
Normal file
62
apps/browser-extension/src/i18n/i18n.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
LANGUAGE_CODES,
|
||||
LANGUAGE_RESOURCES
|
||||
} from './config';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
// Detect browser language
|
||||
/**
|
||||
* Detect the user's preferred language from localStorage or browser settings
|
||||
*/
|
||||
const detectLanguage = async (): Promise<string> => {
|
||||
// Check localStorage first
|
||||
const stored = await storage.getItem('local:language') as string;
|
||||
if (stored && LANGUAGE_CODES.includes(stored)) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
// Fall back to browser language
|
||||
const browserLang = navigator.language.split('-')[0];
|
||||
return LANGUAGE_CODES.includes(browserLang) ? browserLang : DEFAULT_LANGUAGE;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize i18n with async language detection
|
||||
*/
|
||||
const initI18n = async (): Promise<void> => {
|
||||
const language = await detectLanguage();
|
||||
|
||||
await i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: LANGUAGE_RESOURCES,
|
||||
lng: language,
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
|
||||
debug: false, // Set to true for development debugging
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false // React already escapes
|
||||
},
|
||||
|
||||
react: {
|
||||
useSuspense: false, // Important for browser extensions
|
||||
bindI18n: 'languageChanged loaded', // Bind to language change and loaded events
|
||||
bindI18nStore: '' // Don't bind to resource store changes
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize immediately and handle potential errors
|
||||
initI18n().catch((error) => {
|
||||
console.error('Failed to initialize i18n:', error);
|
||||
// Even if initialization fails, emit initialized event to prevent app from hanging
|
||||
i18n.emit('initialized');
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
375
apps/browser-extension/src/i18n/locales/de.json
Normal file
375
apps/browser-extension/src/i18n/locales/de.json
Normal file
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"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": {
|
||||
"VaultMergeRequired": "Your vault needs to be updated. Please login on the AliasVault website and follow the steps.",
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"NoVaultFound": "Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.",
|
||||
"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",
|
||||
"failedToGetVault": "Failed to get vault",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToGetCredentials": "Failed to get credentials",
|
||||
"failedToCreateIdentity": "Failed to create identity",
|
||||
"failedToGetDefaultEmailDomain": "Failed to get default email domain",
|
||||
"failedToGetDefaultIdentitySettings": "Failed to get default identity settings",
|
||||
"failedToGetPasswordSettings": "Failed to get password settings",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"noDerivedKeyAvailable": "No derived key available for encryption",
|
||||
"failedToUploadVaultToServer": "Failed to upload new vault to server",
|
||||
"noVaultOrDerivedKeyFound": "No vault or derived key found"
|
||||
},
|
||||
"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.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "User not found in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "User not found in database.",
|
||||
"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",
|
||||
"searchCredentials": "Search credentials...",
|
||||
"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.",
|
||||
"lastUsed": "Last used",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"copyUsername": "Copy Username",
|
||||
"openWebsite": "Open Website",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Remove from Favorites",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"deleteSuccess": "Credential deleted successfully",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"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",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"saveError": "Failed to save credential",
|
||||
"loadError": "Failed to load credentials",
|
||||
"deleteError": "Failed to delete credential",
|
||||
"copyError": "Failed to copy to clipboard"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidUrl": "Invalid URL format",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"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",
|
||||
"versionPrefix": "Version ",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
375
apps/browser-extension/src/i18n/locales/en.json
Normal file
375
apps/browser-extension/src/i18n/locales/en.json
Normal file
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"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": {
|
||||
"VaultMergeRequired": "Your vault needs to be updated. Please login on the AliasVault website and follow the steps.",
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"NoVaultFound": "Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.",
|
||||
"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",
|
||||
"failedToGetVault": "Failed to get vault",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToGetCredentials": "Failed to get credentials",
|
||||
"failedToCreateIdentity": "Failed to create identity",
|
||||
"failedToGetDefaultEmailDomain": "Failed to get default email domain",
|
||||
"failedToGetDefaultIdentitySettings": "Failed to get default identity settings",
|
||||
"failedToGetPasswordSettings": "Failed to get password settings",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"noDerivedKeyAvailable": "No derived key available for encryption",
|
||||
"failedToUploadVaultToServer": "Failed to upload new vault to server",
|
||||
"noVaultOrDerivedKeyFound": "No vault or derived key found"
|
||||
},
|
||||
"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.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "User not found in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "User not found in database.",
|
||||
"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",
|
||||
"searchCredentials": "Search credentials...",
|
||||
"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.",
|
||||
"lastUsed": "Last used",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"copyUsername": "Copy Username",
|
||||
"openWebsite": "Open Website",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Remove from Favorites",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"deleteSuccess": "Credential deleted successfully",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"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",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"saveError": "Failed to save credential",
|
||||
"loadError": "Failed to load credentials",
|
||||
"deleteError": "Failed to delete credential",
|
||||
"copyError": "Failed to copy to clipboard"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidUrl": "Invalid URL format",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"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",
|
||||
"versionPrefix": "Version ",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
375
apps/browser-extension/src/i18n/locales/es.json
Normal file
375
apps/browser-extension/src/i18n/locales/es.json
Normal file
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"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": {
|
||||
"VaultMergeRequired": "Your vault needs to be updated. Please login on the AliasVault website and follow the steps.",
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"NoVaultFound": "Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.",
|
||||
"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",
|
||||
"failedToGetVault": "Failed to get vault",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToGetCredentials": "Failed to get credentials",
|
||||
"failedToCreateIdentity": "Failed to create identity",
|
||||
"failedToGetDefaultEmailDomain": "Failed to get default email domain",
|
||||
"failedToGetDefaultIdentitySettings": "Failed to get default identity settings",
|
||||
"failedToGetPasswordSettings": "Failed to get password settings",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"noDerivedKeyAvailable": "No derived key available for encryption",
|
||||
"failedToUploadVaultToServer": "Failed to upload new vault to server",
|
||||
"noVaultOrDerivedKeyFound": "No vault or derived key found"
|
||||
},
|
||||
"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.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "User not found in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "User not found in database.",
|
||||
"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",
|
||||
"searchCredentials": "Search credentials...",
|
||||
"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.",
|
||||
"lastUsed": "Last used",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"copyUsername": "Copy Username",
|
||||
"openWebsite": "Open Website",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Remove from Favorites",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"deleteSuccess": "Credential deleted successfully",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"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",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"saveError": "Failed to save credential",
|
||||
"loadError": "Failed to load credentials",
|
||||
"deleteError": "Failed to delete credential",
|
||||
"copyError": "Failed to copy to clipboard"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidUrl": "Invalid URL format",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"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",
|
||||
"versionPrefix": "Version ",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
375
apps/browser-extension/src/i18n/locales/fr.json
Normal file
375
apps/browser-extension/src/i18n/locales/fr.json
Normal file
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"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": "Voir plus",
|
||||
"errors": {
|
||||
"VaultMergeRequired": "Your vault needs to be updated. Please login on the AliasVault website and follow the steps.",
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"NoVaultFound": "Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.",
|
||||
"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",
|
||||
"failedToGetVault": "Failed to get vault",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToGetCredentials": "Failed to get credentials",
|
||||
"failedToCreateIdentity": "Failed to create identity",
|
||||
"failedToGetDefaultEmailDomain": "Failed to get default email domain",
|
||||
"failedToGetDefaultIdentitySettings": "Failed to get default identity settings",
|
||||
"failedToGetPasswordSettings": "Failed to get password settings",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"noDerivedKeyAvailable": "No derived key available for encryption",
|
||||
"failedToUploadVaultToServer": "Failed to upload new vault to server",
|
||||
"noVaultOrDerivedKeyFound": "No vault or derived key found"
|
||||
},
|
||||
"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.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "User not found in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "User not found in database.",
|
||||
"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",
|
||||
"searchCredentials": "Search credentials...",
|
||||
"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.",
|
||||
"lastUsed": "Last used",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"copyUsername": "Copy Username",
|
||||
"openWebsite": "Open Website",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Remove from Favorites",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"deleteSuccess": "Credential deleted successfully",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"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",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"saveError": "Failed to save credential",
|
||||
"loadError": "Failed to load credentials",
|
||||
"deleteError": "Failed to delete credential",
|
||||
"copyError": "Failed to copy to clipboard"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidUrl": "Invalid URL format",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"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",
|
||||
"versionPrefix": "Version ",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
375
apps/browser-extension/src/i18n/locales/nl.json
Normal file
375
apps/browser-extension/src/i18n/locales/nl.json
Normal file
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Inloggen bij AliasVault",
|
||||
"username": "Gebruikersnaam of e-mail",
|
||||
"usernamePlaceholder": "naam / naam@bedrijf.com",
|
||||
"password": "Wachtwoord",
|
||||
"passwordPlaceholder": "Voer je wachtwoord in",
|
||||
"rememberMe": "Onthoud mij",
|
||||
"loginButton": "Inloggen",
|
||||
"noAccount": "Nog geen account?",
|
||||
"createVault": "Nieuwe vault aanmaken",
|
||||
"twoFactorTitle": "Voer de authenticatiecode van je authenticator-app in.",
|
||||
"authCode": "Authenticatiecode",
|
||||
"authCodePlaceholder": "Voer 6-cijferige code in",
|
||||
"verify": "Verifiëren",
|
||||
"cancel": "Annuleren",
|
||||
"twoFactorNote": "Opmerking: als je geen toegang hebt tot je authenticator, kunt je je 2FA resetten door met een in te loggen via de website.",
|
||||
"masterPassword": "Hoofdwachtwoord",
|
||||
"unlockVault": "Vault ontgrendelen",
|
||||
"unlockTitle": "Ontgrendel je vault",
|
||||
"unlockDescription": "Voer je hoofdwachtwoord in om je vault te ontgrendelen.",
|
||||
"logout": "Uitloggen",
|
||||
"logoutConfirm": "Weet je zeker dat je wilt uitloggen?",
|
||||
"sessionExpired": "Je sessie is verlopen. Log opnieuw in.",
|
||||
"unlockSuccess": "Vault succesvol ontgrendeld!",
|
||||
"unlockSuccessTitle": "Je vault is succesvol ontgrendeld",
|
||||
"unlockSuccessDescription": "Je kunt nu automatisch invullen gebruiken in inlogformulieren in je browser.",
|
||||
"closePopup": "Sluit deze popup",
|
||||
"browseVault": "Bekijk vault inhoud",
|
||||
"connectingTo": "Verbinden met",
|
||||
"switchAccounts": "Wisselen van account?",
|
||||
"loggedIn": "Ingelogd",
|
||||
"errors": {
|
||||
"invalidCode": "Voer een geldige 6-cijferige code in.",
|
||||
"serverError": "Kon de AliasVault server niet bereiken. Probeer het later opnieuw of neem contact op met support als het probleem aanhoudt.",
|
||||
"noToken": "Inloggen mislukt -- geen token ontvangen",
|
||||
"migrationError": "Er is een fout opgetreden bij het controleren op updates.",
|
||||
"wrongPassword": "Onjuist wachtwoord. Probeer het opnieuw.",
|
||||
"accountLocked": "Account tijdelijk vergrendeld vanwege te veel mislukte pogingen.",
|
||||
"networkError": "Netwerkfout. Controleer de verbinding en probeer het opnieuw.",
|
||||
"loginDataMissing": "Sessie verlopen. Probeer het opnieuw."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credentials",
|
||||
"emails": "E-mails",
|
||||
"settings": "Instellingen"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Laden...",
|
||||
"error": "Fout",
|
||||
"success": "Succes",
|
||||
"cancel": "Annuleren",
|
||||
"use": "Gebruik",
|
||||
"delete": "Verwijderen",
|
||||
"close": "Sluiten",
|
||||
"copied": "Gekopieerd!",
|
||||
"openInNewWindow": "Openen in nieuw venster",
|
||||
"language": "Taal",
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"showPassword": "Wachtwoord tonen",
|
||||
"hidePassword": "Wachtwoord verbergen",
|
||||
"copyToClipboard": "Naar klembord kopiëren",
|
||||
"loadingEmails": "E-mails laden...",
|
||||
"loadingTotpCodes": "TOTP-codes laden...",
|
||||
"attachments": "Bijlagen",
|
||||
"loadingAttachments": "Bijlagen laden...",
|
||||
"settings": "Instellingen",
|
||||
"recentEmails": "Recente e-mails",
|
||||
"loginCredentials": "Inloggegevens",
|
||||
"twoFactorAuthentication": "Tweestapsverificatie",
|
||||
"alias": "Alias",
|
||||
"notes": "Notities",
|
||||
"fullName": "Volledige naam",
|
||||
"firstName": "Voornaam",
|
||||
"lastName": "Achternaam",
|
||||
"birthDate": "Geboortedatum",
|
||||
"nickname": "Bijnaam",
|
||||
"email": "E-mail",
|
||||
"username": "Gebruikersnaam",
|
||||
"password": "Wachtwoord",
|
||||
"syncingVault": "Vault synchroniseren",
|
||||
"savingChangesToVault": "Wijzigingen opslaan in vault",
|
||||
"uploadingVaultToServer": "Vault uploaden naar server",
|
||||
"checkingVaultUpdates": "Controleren op vault updates",
|
||||
"syncingUpdatedVault": "Bijgewerkte vault synchroniseren",
|
||||
"executingOperation": "Actie uitvoeren...",
|
||||
"loadMore": "Laad meer",
|
||||
"errors": {
|
||||
"VaultMergeRequired": "Je vault moet worden bijgewerkt. Log in op de AliasVault website en volg de stappen.",
|
||||
"VaultOutdated": "Je vault is verouderd. Log in op de AliasVault website en volg de stappen.",
|
||||
"NoVaultFound": "Je account heeft nog geen vault. Voltooi eerst de tutorial in de AliasVault webclient voordat je de browserextensie gebruikt.",
|
||||
"serverNotAvailable": "De AliasVault server is niet beschikbaar. Probeer het later opnieuw of neem contact op met de ondersteuning als het probleem aanhoudt.",
|
||||
"clientVersionNotSupported": "Deze versie van de AliasVault browserextensie wordt niet meer ondersteund door de server. Update je browserextensie naar de nieuwste versie.",
|
||||
"serverVersionNotSupported": "De AliasVault server moet worden bijgewerkt naar een nieuwere versie om deze browserextensie te kunnen gebruiken. Neem contact op met support als je hulp nodig hebt.",
|
||||
"unknownError": "Er is een onbekende fout opgetreden",
|
||||
"failedToStoreVault": "Vault opslaan mislukt",
|
||||
"vaultNotAvailable": "Vault niet beschikbaar",
|
||||
"failedToGetVault": "Vault ophalen mislukt",
|
||||
"vaultIsLocked": "Vault is vergrendeld",
|
||||
"failedToGetCredentials": "Credentials ophalen mislukt",
|
||||
"failedToCreateIdentity": "Identiteit aanmaken mislukt",
|
||||
"failedToGetDefaultEmailDomain": "Standaard e-maildomein ophalen mislukt",
|
||||
"failedToGetDefaultIdentitySettings": "Standaard identiteit instellingen ophalen mislukt",
|
||||
"failedToGetPasswordSettings": "Wachtwoordinstellingen ophalen mislukt",
|
||||
"failedToUploadVault": "Vault uploaden mislukt",
|
||||
"noDerivedKeyAvailable": "Geen afgeleide sleutel beschikbaar voor versleuteling",
|
||||
"failedToUploadVaultToServer": "Nieuwe vault uploaden naar server mislukt",
|
||||
"noVaultOrDerivedKeyFound": "Geen vault of afgeleide sleutel gevonden"
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "Er is een onbekende fout opgetreden. Probeer het opnieuw.",
|
||||
"ACCOUNT_LOCKED": "Account tijdelijk vergrendeld vanwege te veel mislukte pogingen. Probeer het later opnieuw.",
|
||||
"ACCOUNT_BLOCKED": "Je account is uitgeschakeld. Als je denkt dat dit een vergissing is, neem dan contact op met support.",
|
||||
"USER_NOT_FOUND": "Gebruikersnaam of wachtwoord is onjuist. Probeer het opnieuw.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Ongeldige authenticator code. Probeer het opnieuw.",
|
||||
"INVALID_RECOVERY_CODE": "Ongeldige herstelcode. Probeer het opnieuw.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is vereist.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "Gebruiker niet gevonden in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "Gebruiker niet gevonden in database.",
|
||||
"INVALID_REFRESH_TOKEN": "Ongeldig refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token succesvol ingetrokken.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "Registratie van nieuwe accounts is momenteel uitgeschakeld op deze server. Neem contact op met de beheerder.",
|
||||
"USERNAME_REQUIRED": "Gebruikersnaam is vereist.",
|
||||
"USERNAME_ALREADY_IN_USE": "Gebruikersnaam is al in gebruik.",
|
||||
"USERNAME_AVAILABLE": "Gebruikersnaam is beschikbaar.",
|
||||
"USERNAME_MISMATCH": "Gebruikersnaam komt niet overeen met de huidige gebruiker.",
|
||||
"PASSWORD_MISMATCH": "Het opgegeven wachtwoord komt niet overeen met je huidige wachtwoord.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account succesvol verwijderd.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Gebruikersnaam mag niet leeg zijn of alleen uit spaties bestaan.",
|
||||
"USERNAME_TOO_SHORT": "Gebruikersnaam te kort: moet minimaal 3 tekens lang zijn.",
|
||||
"USERNAME_TOO_LONG": "Gebruikersnaam te lang: mag niet langer zijn dan 40 tekens.",
|
||||
"USERNAME_INVALID_EMAIL": "Ongeldig e-mailadres.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Gebruikersnaam is ongeldig, mag alleen letters of cijfers bevatten.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Je vault is niet up-to-date. Synchroniseer je vault en probeer het opnieuw.",
|
||||
"INTERNAL_SERVER_ERROR": "Interne serverfout.",
|
||||
"VAULT_ERROR": "Je lokale vault is niet up-to-date. Synchroniseer je vault door de pagina te vernieuwen en probeer het opnieuw."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "of",
|
||||
"new": "Nieuw",
|
||||
"cancel": "Annuleren",
|
||||
"search": "Zoeken",
|
||||
"vaultLocked": "AliasVault is vergrendeld.",
|
||||
"creatingNewAlias": "Nieuwe alias aanmaken...",
|
||||
"noMatchesFound": "Geen resultaten gevonden",
|
||||
"searchVault": "Vault doorzoeken...",
|
||||
"serviceName": "Servicenaam",
|
||||
"email": "E-mail",
|
||||
"username": "Gebruikersnaam",
|
||||
"password": "Wachtwoord",
|
||||
"enterServiceName": "Voer servicenaam in",
|
||||
"enterEmailAddress": "Voer e-mailadres in",
|
||||
"enterUsername": "Voer gebruikersnaam in",
|
||||
"hideFor1Hour": "Verberg voor 1 uur (huidige site)",
|
||||
"hidePermanently": "Permanent verbergen (huidige site)",
|
||||
"createRandomAlias": "Willekeurige alias aanmaken",
|
||||
"createUsernamePassword": "Gebruikersnaam/wachtwoord aanmaken",
|
||||
"randomAlias": "Alias",
|
||||
"usernamePassword": "Gebruikersnaam/wachtwoord",
|
||||
"createAndSaveAlias": "Alias aanmaken",
|
||||
"createAndSaveCredential": "Credential aanmaken",
|
||||
"randomIdentityDescription": "Genereer een willekeurige identiteit met een willekeurig e-mailadres toegankelijk in AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Willekeurige identiteit met willekeurige e-mail",
|
||||
"manualCredentialDescription": "Specificeer je eigen e-mailadres en/of gebruikersnaam.",
|
||||
"manualCredentialDescriptionDropdown": "Handmatige gebruikersnaam en wachtwoord",
|
||||
"failedToCreateIdentity": "Identiteit aanmaken mislukt. Probeer opnieuw.",
|
||||
"enterEmailAndOrUsername": "Voer e-mail en/of gebruikersnaam in",
|
||||
"autofillWithAliasVault": "Autofill met AliasVault",
|
||||
"generateRandomPassword": "Willekeurig wachtwoord genereren (kopiëren naar klembord)",
|
||||
"generateNewPassword": "Genereer nieuw wachtwoord",
|
||||
"togglePasswordVisibility": "Schakel zichtbaarheid van wachtwoord in/uit",
|
||||
"passwordCopiedToClipboard": "Wachtwoord gekopieerd naar klembord",
|
||||
"enterEmailAndOrUsernameError": "Voer e-mail en/of gebruikersnaam in",
|
||||
"openAliasVaultToUpgrade": "Open AliasVault om te upgraden",
|
||||
"vaultUpgradeRequired": "Update is vereist.",
|
||||
"dismissPopup": "Pop-up sluiten"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credentials",
|
||||
"addCredential": "Credential toevoegen",
|
||||
"editCredential": "Credential bewerken",
|
||||
"deleteCredential": "Credential verwijderen",
|
||||
"credentialDetails": "Credential details",
|
||||
"serviceName": "Naam",
|
||||
"serviceNamePlaceholder": "bijv. Gmail, Facebook, Bank",
|
||||
"website": "Website",
|
||||
"websitePlaceholder": "https://voorbeeld.nl",
|
||||
"username": "Gebruikersnaam",
|
||||
"usernamePlaceholder": "Voer gebruikersnaam in",
|
||||
"password": "Wachtwoord",
|
||||
"passwordPlaceholder": "Voer wachtwoord in",
|
||||
"generatePassword": "Wachtwoord genereren",
|
||||
"copyPassword": "Wachtwoord kopiëren",
|
||||
"showPassword": "Wachtwoord tonen",
|
||||
"hidePassword": "Wachtwoord verbergen",
|
||||
"notes": "Notities",
|
||||
"notesPlaceholder": "Aanvullende notities...",
|
||||
"totp": "Tweestapsverificatie",
|
||||
"totpCode": "TOTP-code",
|
||||
"copyTotp": "Kopiëren",
|
||||
"totpSecret": "TOTP secret",
|
||||
"totpSecretPlaceholder": "Voer TOTP secret in",
|
||||
"noCredentials": "Geen credentials gevonden",
|
||||
"noCredentialsDescription": "Voeg je eerste credentials toe om te beginnen",
|
||||
"searchCredentials": "Zoek credentials...",
|
||||
"searchPlaceholder": "Credentials zoeken...",
|
||||
"welcomeTitle": "Welkom bij AliasVault!",
|
||||
"welcomeDescription": "Om de AliasVault browser extensie te gebruiken: navigeer naar een website en gebruik de AliasVault autofill popup om nieuwe credentials aan te maken.",
|
||||
"lastUsed": "Laatst gebruikt",
|
||||
"createdAt": "Aangemaakt",
|
||||
"updatedAt": "Laatst bijgewerkt",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Formulier invullen",
|
||||
"copyUsername": "Gebruikersnaam kopiëren",
|
||||
"openWebsite": "Website openen",
|
||||
"favorite": "Favoriet",
|
||||
"unfavorite": "Uit favorieten verwijderen",
|
||||
"deleteConfirm": "Weet je zeker dat je deze credential wilt verwijderen?",
|
||||
"deleteSuccess": "Credential succesvol verwijderd",
|
||||
"saveSuccess": "Credential succesvol opgeslagen",
|
||||
"copySuccess": "Gekopieerd naar klembord",
|
||||
"tags": "Labels",
|
||||
"addTag": "Label toevoegen",
|
||||
"removeTag": "Label verwijderen",
|
||||
"folder": "Map",
|
||||
"selectFolder": "Map selecteren",
|
||||
"createFolder": "Map aanmaken",
|
||||
"saveCredential": "Credential opslaan",
|
||||
"deleteCredentialTitle": "Credential verwijderen",
|
||||
"deleteCredentialConfirm": "Weet je zeker dat je deze credential wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"randomAlias": "Alias",
|
||||
"manual": "Handmatig",
|
||||
"service": "Naam",
|
||||
"serviceUrl": "URL",
|
||||
"loginCredentials": "Inloggegevens",
|
||||
"generateRandomUsername": "Gebruikersnaam genereren",
|
||||
"generateRandomPassword": "Wachtwoord genereren",
|
||||
"changePasswordComplexity": "Wijzig wachtwoord complexiteit",
|
||||
"passwordLength": "Wachtwoordlengte",
|
||||
"includeLowercase": "Inclusief kleine letters",
|
||||
"includeUppercase": "Inclusief hoofdletters",
|
||||
"includeNumbers": "Inclusief cijfers",
|
||||
"includeSpecialChars": "Inclusief speciale karakters",
|
||||
"avoidAmbiguousChars": "Onduidelijke tekens vermijden (o, 0, etc.)",
|
||||
"generateNewPreview": "Genereer nieuw voorbeeld",
|
||||
"generateRandomAlias": "Alias genereren",
|
||||
"alias": "Alias",
|
||||
"firstName": "Voornaam",
|
||||
"lastName": "Achternaam",
|
||||
"nickName": "Bijnaam",
|
||||
"gender": "Geslacht",
|
||||
"birthDate": "Geboortedatum",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Voer een geldige URL in",
|
||||
"saveError": "Credential opslaan mislukt",
|
||||
"loadError": "Credential laden mislukt",
|
||||
"deleteError": "Credential verwijderen mislukt",
|
||||
"copyError": "Kopiëren naar klembord mislukt"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Dit veld is verplicht",
|
||||
"serviceNameRequired": "Servicenaam is verplicht",
|
||||
"invalidUrl": "Ongeldig URL-formaat",
|
||||
"invalidEmail": "Ongeldig e-mailformaat",
|
||||
"invalidDateFormat": "Datum moet in YYYY-MM-DD formaat zijn"
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "E-mails",
|
||||
"deleteEmailTitle": "E-mail verwijderen",
|
||||
"deleteEmailConfirm": "Weet je zeker dat je deze e-mail definitief wilt verwijderen?",
|
||||
"from": "Van",
|
||||
"to": "Naar",
|
||||
"date": "Datum",
|
||||
"emailContent": "E-mailinhoud",
|
||||
"attachments": "Bijlagen",
|
||||
"emailNotFound": "E-mail niet gevonden",
|
||||
"noEmails": "Geen e-mails gevonden",
|
||||
"noEmailsDescription": "Je hebt nog geen e-mails ontvangen op je privé e-mailadressen. Wanneer je een nieuwe e-mail ontvangt, zal deze hier verschijnen.",
|
||||
"dateFormat": {
|
||||
"justNow": "zojuist",
|
||||
"minutesAgo_single": "{{count}} min geleden",
|
||||
"minutesAgo_plural": "{{count}} min. geleden",
|
||||
"hoursAgo_single": "{{count}} uur geleden",
|
||||
"hoursAgo_plural": "{{count}} uur geleden",
|
||||
"yesterday": "gisteren"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "Er is een fout opgetreden bij het laden van e-mails. Probeer het later opnieuw.",
|
||||
"emailUnexpectedError": "Er is een onverwachte fout opgetreden bij het laden van e-mails. Probeer het later opnieuw."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "Het huidige gekozen e-mailadres is al in gebruik. Wijzig het e-mailadres door deze credential te bewerken.",
|
||||
"CLAIM_DOES_NOT_EXIST": "Er is een fout opgetreden bij het laden van e-mails. Probeer de credential te bewerken en op te slaan om de database te synchroniseren, en probeer het opnieuw."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Instellingen",
|
||||
"serverUrl": "Server URL",
|
||||
"language": "Taal",
|
||||
"autofillEnabled": "Autofill",
|
||||
"version": "Versie",
|
||||
"openInNewWindow": "Openen in nieuw venster",
|
||||
"openWebApp": "Web-app openen",
|
||||
"loggedIn": "Ingelogd",
|
||||
"logout": "Uitloggen",
|
||||
"globalSettings": "Globale Instellingen",
|
||||
"autofillPopup": "Autofill popup",
|
||||
"activeOnAllSites": "Actief voor alle sites (tenzij hieronder uitgeschakeld)",
|
||||
"disabledOnAllSites": "Uitgeschakeld op alle sites",
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"rightClickContextMenu": "Rechtermuisknop menu",
|
||||
"siteSpecificSettings": "Site-specifieke Instellingen",
|
||||
"autofillPopupOn": "Autofill popup op: ",
|
||||
"enabledForThisSite": "Ingeschakeld voor deze site",
|
||||
"disabledForThisSite": "Uitgeschakeld voor deze site",
|
||||
"temporarilyDisabledUntil": "Tijdelijk uitgeschakeld tot ",
|
||||
"resetAllSiteSettings": "Alle site-specifieke instellingen resetten",
|
||||
"appearance": "Uiterlijk",
|
||||
"theme": "Thema",
|
||||
"useDefault": "Standaard gebruiken",
|
||||
"light": "Licht",
|
||||
"dark": "Donker",
|
||||
"keyboardShortcuts": "Snelkoppelingen",
|
||||
"configureKeyboardShortcuts": "Snelkoppelingen configureren",
|
||||
"configure": "Configureren",
|
||||
"versionPrefix": "Versie ",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is vereist",
|
||||
"apiUrlInvalid": "Voer een geldige API URL in",
|
||||
"clientUrlRequired": "Client URL is vereist",
|
||||
"clientUrlInvalid": "Voer een geldige client URL in"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Vault upgraden",
|
||||
"subtitle": "AliasVault is vernieuwd en je vault moet worden bijgewerkt. Dit kan enkele seconden duren.",
|
||||
"versionInformation": "Versie-informatie",
|
||||
"yourVault": "Jouw vault:",
|
||||
"newVersion": "Nieuwe versie:",
|
||||
"upgrade": "Vault upgraden",
|
||||
"upgrading": "Aan het upgraden...",
|
||||
"logout": "Uitloggen",
|
||||
"whatsNew": "Wat is er nieuw",
|
||||
"whatsNewDescription": "Een upgrade is vereist vanwege de volgende wijzigingen:",
|
||||
"noDescriptionAvailable": "Voor deze versie is geen beschrijving beschikbaar.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Upgrade voorbereiden...",
|
||||
"vaultAlreadyUpToDate": "De vault is al bijgewerkt",
|
||||
"startingDatabaseTransaction": "Starten van database transactie...",
|
||||
"applyingDatabaseMigrations": "Databasemigratie toepassen...",
|
||||
"applyingMigration": "Toepassen van migratie {{current}} van {{total}}...",
|
||||
"committingChanges": "Wijzigingen doorvoeren..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Fout",
|
||||
"unableToGetVersionInfo": "Kan versie-informatie niet ophalen. Probeer het opnieuw.",
|
||||
"selfHostedServer": "Self-hosted server",
|
||||
"selfHostedWarning": "Als je een self-hosted server gebruikt, zorg er dan voor dat je ook je eigen self-hosted instantie bijwerkt, omdat anders het inloggen via de web client niet meer zal werken.",
|
||||
"cancel": "Annuleren",
|
||||
"continueUpgrade": "Verdergaan",
|
||||
"upgradeFailed": "Upgrade mislukt",
|
||||
"failedToApplyMigration": "Kon migratie niet toepassen ({{current}} van {{total}})",
|
||||
"unknownErrorDuringUpgrade": "Er is een onbekende fout opgetreden tijdens de upgrade. Probeer het opnieuw."
|
||||
}
|
||||
}
|
||||
}
|
||||
375
apps/browser-extension/src/i18n/locales/uk.json
Normal file
375
apps/browser-extension/src/i18n/locales/uk.json
Normal file
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"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": {
|
||||
"VaultMergeRequired": "Your vault needs to be updated. Please login on the AliasVault website and follow the steps.",
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"NoVaultFound": "Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.",
|
||||
"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",
|
||||
"failedToGetVault": "Failed to get vault",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToGetCredentials": "Failed to get credentials",
|
||||
"failedToCreateIdentity": "Failed to create identity",
|
||||
"failedToGetDefaultEmailDomain": "Failed to get default email domain",
|
||||
"failedToGetDefaultIdentitySettings": "Failed to get default identity settings",
|
||||
"failedToGetPasswordSettings": "Failed to get password settings",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"noDerivedKeyAvailable": "No derived key available for encryption",
|
||||
"failedToUploadVaultToServer": "Failed to upload new vault to server",
|
||||
"noVaultOrDerivedKeyFound": "No vault or derived key found"
|
||||
},
|
||||
"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.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "User not found in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "User not found in database.",
|
||||
"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",
|
||||
"searchCredentials": "Search credentials...",
|
||||
"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.",
|
||||
"lastUsed": "Last used",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"copyUsername": "Copy Username",
|
||||
"openWebsite": "Open Website",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Remove from Favorites",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"deleteSuccess": "Credential deleted successfully",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"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",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"saveError": "Failed to save credential",
|
||||
"loadError": "Failed to load credentials",
|
||||
"deleteError": "Failed to delete credential",
|
||||
"copyError": "Failed to copy to clipboard"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidUrl": "Invalid URL format",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"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",
|
||||
"versionPrefix": "Version ",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.20.2';
|
||||
public static readonly VERSION = '0.21.2';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import initSqlJs, { Database } from 'sql.js';
|
||||
|
||||
import type { Credential, EncryptionKey, PasswordSettings, TotpCode } from '@/utils/dist/shared/models/vault';
|
||||
import type { Attachment } from '@/utils/dist/shared/models/vault';
|
||||
import type { VaultVersion } from '@/utils/dist/shared/vault-sql';
|
||||
import { VaultSqlGenerator } from '@/utils/dist/shared/vault-sql';
|
||||
|
||||
@@ -244,7 +245,7 @@ export class SqliteClient {
|
||||
BirthDate: row.BirthDate,
|
||||
Gender: row.Gender,
|
||||
Email: row.Email
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -424,9 +425,10 @@ export class SqliteClient {
|
||||
/**
|
||||
* Create a new credential with associated entities
|
||||
* @param credential The credential object to insert
|
||||
* @param attachments The attachments to insert
|
||||
* @returns The ID of the created credential
|
||||
*/
|
||||
public async createCredential(credential: Credential): Promise<string> {
|
||||
public async createCredential(credential: Credential, attachments: Attachment[]): Promise<string> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
@@ -520,6 +522,26 @@ export class SqliteClient {
|
||||
]);
|
||||
}
|
||||
|
||||
// 5. Insert Attachment
|
||||
if (attachments) {
|
||||
for (const attachment of attachments) {
|
||||
const attachmentQuery = `
|
||||
INSERT INTO Attachments (Id, Filename, Blob, CredentialId, CreatedAt, UpdatedAt, IsDeleted)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
const attachmentId = crypto.randomUUID().toUpperCase();
|
||||
this.executeUpdate(attachmentQuery, [
|
||||
attachmentId,
|
||||
attachment.Filename,
|
||||
attachment.Blob as Uint8Array,
|
||||
credentialId,
|
||||
currentDateTime,
|
||||
currentDateTime,
|
||||
0
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
await this.commitTransaction();
|
||||
return credentialId;
|
||||
|
||||
@@ -641,6 +663,39 @@ export class SqliteClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachments for a specific credential
|
||||
* @param credentialId - The ID of the credential
|
||||
* @returns Array of attachments for the credential
|
||||
*/
|
||||
public getAttachmentsForCredential(credentialId: string): Attachment[] {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.tableExists('Attachments')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
Id,
|
||||
Filename,
|
||||
Blob,
|
||||
CredentialId,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
IsDeleted
|
||||
FROM Attachments
|
||||
WHERE CredentialId = ? AND IsDeleted = 0`;
|
||||
return this.executeQuery<Attachment>(query, [credentialId]);
|
||||
} catch (error) {
|
||||
console.error('Error getting attachments:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a credential by ID
|
||||
* @param credentialId - The ID of the credential to delete
|
||||
@@ -702,9 +757,11 @@ export class SqliteClient {
|
||||
/**
|
||||
* Update an existing credential with associated entities
|
||||
* @param credential The credential object to update
|
||||
* @param originalAttachmentIds The IDs of the original attachments
|
||||
* @param attachments The attachments to update
|
||||
* @returns The number of rows modified
|
||||
*/
|
||||
public async updateCredentialById(credential: Credential): Promise<number> {
|
||||
public async updateCredentialById(credential: Credential, originalAttachmentIds: string[], attachments: Attachment[]): Promise<number> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
@@ -850,6 +907,44 @@ export class SqliteClient {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Handle Attachments
|
||||
if (attachments) {
|
||||
// Get current attachment IDs to track what needs to be deleted
|
||||
const currentAttachmentIds = attachments.map(a => a.Id);
|
||||
|
||||
// Delete attachments that were removed (in originalAttachmentIds but not in current attachments)
|
||||
const attachmentsToDelete = originalAttachmentIds.filter(id => !currentAttachmentIds.includes(id));
|
||||
for (const attachmentId of attachmentsToDelete) {
|
||||
const deleteQuery = `
|
||||
UPDATE Attachments
|
||||
SET IsDeleted = 1,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = ?`;
|
||||
this.executeUpdate(deleteQuery, [currentDateTime, attachmentId]);
|
||||
}
|
||||
|
||||
// Process each attachment
|
||||
for (const attachment of attachments) {
|
||||
const isExistingAttachment = originalAttachmentIds.includes(attachment.Id);
|
||||
|
||||
if (!isExistingAttachment) {
|
||||
// Insert new attachment
|
||||
const insertQuery = `
|
||||
INSERT INTO Attachments (Id, Filename, Blob, CredentialId, CreatedAt, UpdatedAt, IsDeleted)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`;
|
||||
this.executeUpdate(insertQuery, [
|
||||
attachment.Id,
|
||||
attachment.Filename,
|
||||
attachment.Blob as Uint8Array,
|
||||
credential.Id,
|
||||
currentDateTime,
|
||||
currentDateTime,
|
||||
0
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.commitTransaction();
|
||||
return 1;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { StatusResponse, VaultResponse } from '@/utils/dist/shared/models/w
|
||||
|
||||
import { AppInfo } from "./AppInfo";
|
||||
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
type RequestInit = globalThis.RequestInit;
|
||||
@@ -266,19 +268,19 @@ export class WebApiService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the status response and returns an error message if validation fails.
|
||||
* Validates the status response and returns an error message (as translation key) if validation fails.
|
||||
*/
|
||||
public validateStatusResponse(statusResponse: StatusResponse): string | null {
|
||||
if (statusResponse.serverVersion === '0.0.0') {
|
||||
return 'The AliasVault server is not available. Please try again later or contact support if the problem persists.';
|
||||
return 'errors.serverNotAvailable';
|
||||
}
|
||||
|
||||
if (!statusResponse.clientVersionSupported) {
|
||||
return 'This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.';
|
||||
return 'errors.clientVersionNotSupported';
|
||||
}
|
||||
|
||||
if (!AppInfo.isServerVersionSupported(statusResponse.serverVersion)) {
|
||||
return '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.';
|
||||
return 'errors.serverVersionNotSupported';
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -287,22 +289,22 @@ export class WebApiService {
|
||||
/**
|
||||
* Validates the vault response and returns an error message if validation fails
|
||||
*/
|
||||
public validateVaultResponse(vaultResponseJson: VaultResponse): string | null {
|
||||
public validateVaultResponse(vaultResponseJson: VaultResponse, t: TFunction): string | null {
|
||||
/**
|
||||
* Status 0 = OK, vault is ready.
|
||||
* Status 1 = Merge required, which only the web client supports.
|
||||
*/
|
||||
if (vaultResponseJson.status === 1) {
|
||||
// Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
|
||||
return 'Your vault needs to be updated. Please login on the AliasVault website and follow the steps.';
|
||||
return t('errors.VaultMergeRequired');
|
||||
}
|
||||
|
||||
if (vaultResponseJson.status === 2) {
|
||||
return 'Your vault is outdated. Please login on the AliasVault website and follow the steps.';
|
||||
return t('errors.VaultOutdated');
|
||||
}
|
||||
|
||||
if (!vaultResponseJson.vault?.blob) {
|
||||
return 'Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.';
|
||||
return t('errors.NoVaultFound');
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -137,7 +137,7 @@ declare class UsernameEmailGenerator {
|
||||
* @param language - The language to use for generating the identity (e.g. "en", "nl").
|
||||
* @returns A new identity generator instance.
|
||||
*/
|
||||
declare const CreateIdentityGenerator: (language: string) => IIdentityGenerator;
|
||||
declare const CreateIdentityGenerator: (language: string) => IdentityGenerator;
|
||||
|
||||
/**
|
||||
* Creates a new username email generator. This is used by the .NET Blazor WASM JSinterop
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -77,4 +77,17 @@ type Alias = {
|
||||
Email?: string;
|
||||
};
|
||||
|
||||
export type { Alias, Credential, EncryptionKey, PasswordSettings, TotpCode };
|
||||
/**
|
||||
* Attachment SQLite database type.
|
||||
*/
|
||||
type Attachment = {
|
||||
Id: string;
|
||||
Filename: string;
|
||||
Blob: Uint8Array | number[];
|
||||
CredentialId: string;
|
||||
CreatedAt: string;
|
||||
UpdatedAt: string;
|
||||
IsDeleted?: boolean;
|
||||
};
|
||||
|
||||
export type { Alias, Attachment, Credential, EncryptionKey, PasswordSettings, TotpCode };
|
||||
|
||||
@@ -39,7 +39,7 @@ var PasswordGenerator = class {
|
||||
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
this.numberChars = "0123456789";
|
||||
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
this.ambiguousChars = "Il1O0";
|
||||
this.ambiguousChars = "Il1O0o";
|
||||
this.length = 18;
|
||||
this.useLowercase = true;
|
||||
this.useUppercase = true;
|
||||
|
||||
@@ -13,7 +13,7 @@ var PasswordGenerator = class {
|
||||
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
this.numberChars = "0123456789";
|
||||
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
this.ambiguousChars = "Il1O0";
|
||||
this.ambiguousChars = "Il1O0o";
|
||||
this.length = 18;
|
||||
this.useLowercase = true;
|
||||
this.useUppercase = true;
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
manifest: {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.20.2",
|
||||
version: "0.21.2",
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
|
||||
@@ -93,8 +93,8 @@ android {
|
||||
applicationId 'net.aliasvault.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10
|
||||
versionName "0.20.2"
|
||||
versionCode 12
|
||||
versionName "0.21.2"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:usesCleartextTraffic="true">
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:usesCleartextTraffic="true" android:localeConfig="@xml/locales_config">
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
|
||||
@@ -87,7 +87,7 @@ class AutofillService : AutofillService() {
|
||||
try {
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
val presentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
|
||||
presentation.setTextViewText(R.id.text, "Failed to retrieve, open app")
|
||||
presentation.setTextViewText(R.id.text, getString(R.string.autofill_failed_to_retrieve))
|
||||
|
||||
val dataSetBuilder = Dataset.Builder(presentation)
|
||||
|
||||
@@ -347,7 +347,7 @@ class AutofillService : AutofillService() {
|
||||
val presentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
|
||||
presentation.setTextViewText(
|
||||
R.id.text,
|
||||
"No match found, create new?",
|
||||
getString(R.string.autofill_no_match_found),
|
||||
)
|
||||
|
||||
val dataSetBuilder = Dataset.Builder(presentation)
|
||||
@@ -391,7 +391,7 @@ class AutofillService : AutofillService() {
|
||||
val openAppPresentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
|
||||
openAppPresentation.setTextViewText(
|
||||
R.id.text,
|
||||
"Open app",
|
||||
getString(R.string.autofill_open_app),
|
||||
)
|
||||
|
||||
val dataSetBuilder = Dataset.Builder(openAppPresentation)
|
||||
@@ -436,7 +436,7 @@ class AutofillService : AutofillService() {
|
||||
val presentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
|
||||
presentation.setTextViewText(
|
||||
R.id.text,
|
||||
"Vault locked",
|
||||
getString(R.string.autofill_vault_locked),
|
||||
)
|
||||
|
||||
val dataSetBuilder = Dataset.Builder(presentation)
|
||||
@@ -474,7 +474,7 @@ class AutofillService : AutofillService() {
|
||||
val presentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
|
||||
presentation.setTextViewText(
|
||||
R.id.text,
|
||||
"Failed to retrieve, open app",
|
||||
getString(R.string.autofill_failed_to_retrieve),
|
||||
)
|
||||
|
||||
val dataSetBuilder = Dataset.Builder(presentation)
|
||||
|
||||
@@ -65,38 +65,46 @@ object CredentialMatcher {
|
||||
)
|
||||
}
|
||||
// 2. Base URL match
|
||||
if (matches.isEmpty()) {
|
||||
matches += credentials.filter { cred ->
|
||||
cred.service.url?.trim()?.lowercase()?.let { url ->
|
||||
url.startsWith("https://$host") || url.startsWith("http://$host")
|
||||
} == true
|
||||
}
|
||||
matches += credentials.filter { cred ->
|
||||
cred.service.url?.trim()?.lowercase()?.let { url ->
|
||||
url.startsWith("https://$host") || url.startsWith("http://$host")
|
||||
} == true
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.isEmpty() && rootDomain != null) {
|
||||
// 3. Root domain match
|
||||
// 3. Root domain fuzzy match on both URL and service name
|
||||
if (rootDomain != null) {
|
||||
val rootDomainNoTld = rootDomain.substringBefore('.') // e.g., "coolblue" from "coolblue.nl"
|
||||
|
||||
matches += credentials.filter { cred ->
|
||||
cred.service.url?.trim()?.lowercase()?.let { url ->
|
||||
val urlMatches = cred.service.url?.trim()?.lowercase()?.takeIf { it.isNotEmpty() }?.let { url ->
|
||||
val u = url.removePrefix("https://")
|
||||
.removePrefix("http://")
|
||||
.removePrefix("www.")
|
||||
.substringBefore("/")
|
||||
extractRootDomain(u) == rootDomain
|
||||
val base = extractRootDomain(u)
|
||||
base.contains(rootDomainNoTld) || rootDomainNoTld.contains(base)
|
||||
} == true
|
||||
|
||||
val nameMatches = cred.service.name?.trim()?.lowercase()?.let { name ->
|
||||
name.contains(rootDomainNoTld) || rootDomainNoTld.contains(name)
|
||||
} == true
|
||||
|
||||
urlMatches || nameMatches
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Domain key match against service name
|
||||
if (matches.isEmpty()) {
|
||||
matches += credentials.filter { cred ->
|
||||
cred.service.name?.lowercase()?.let { name ->
|
||||
name.contains(domainKey) || domainKey.contains(name)
|
||||
} == true
|
||||
}
|
||||
// 4. Domain key match against service name, URL, and notes
|
||||
matches += credentials.filter { cred ->
|
||||
val nameMatches = cred.service.name?.trim()?.lowercase()?.contains(domainKey) == true
|
||||
val urlMatches = cred.service.url?.trim()?.lowercase()?.contains(domainKey) == true
|
||||
val notesMatches = cred.notes?.lowercase()?.contains(domainKey) == true
|
||||
|
||||
nameMatches || urlMatches || notesMatches
|
||||
}
|
||||
|
||||
return matches
|
||||
// Deduplicate matches based on credential ID to avoid duplicates from different matching strategies
|
||||
return matches.distinctBy { it.id }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">AliasVault</string>
|
||||
<string name="autofill_service_description" translatable="true">Remplissage automatique AliasVault</string>
|
||||
<string name="aliasvault_icon">Icône AliasVault</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Échec de la récupération, ouvrez l\'application</string>
|
||||
<string name="autofill_no_match_found">Aucune correspondance trouvée, créer un nouveau ?</string>
|
||||
<string name="autofill_open_app">Ouvrir l’application</string>
|
||||
<string name="autofill_vault_locked">Coffre-fort verrouillé</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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 pictogram</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Ophalen mislukt, open app</string>
|
||||
<string name="autofill_no_match_found">Geen match gevonden, nieuwe maken?</string>
|
||||
<string name="autofill_open_app">Open de app</string>
|
||||
<string name="autofill_vault_locked">Vault vergrendeld</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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>
|
||||
</resources>
|
||||
@@ -5,4 +5,10 @@
|
||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||
<string name="autofill_service_description" translatable="true">AliasVault AutoFill</string>
|
||||
<string name="aliasvault_icon">AliasVault icon</string>
|
||||
|
||||
<!-- 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>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<locale android:name="en" />
|
||||
<locale android:name="nl" />
|
||||
</locale-config>
|
||||
@@ -58,8 +58,9 @@ class AutofillTest {
|
||||
"www.coolblue.nl",
|
||||
)
|
||||
|
||||
assertEquals(1, matches.size)
|
||||
assertEquals(2, matches.size)
|
||||
assertEquals("Coolblue", matches[0].service.name)
|
||||
assertEquals("Coolblue App", matches[1].service.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -5,7 +5,7 @@ buildscript {
|
||||
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'
|
||||
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '30')
|
||||
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
|
||||
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
|
||||
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35')
|
||||
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
|
||||
detektVersion = '1.23.5'
|
||||
|
||||
|
||||
@@ -54,3 +54,7 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
||||
|
||||
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||
expo.useLegacyPackaging=false
|
||||
|
||||
# Workaround for Expo modules compatibility with Android Gradle Plugin 8.x
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
"expo": {
|
||||
"name": "AliasVault",
|
||||
"slug": "AliasVault",
|
||||
"version": "0.20.2",
|
||||
"version": "0.21.2",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "net.aliasvault.app",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"platforms": ["ios", "android", "web"],
|
||||
"platforms": [
|
||||
"ios",
|
||||
"android",
|
||||
"web"
|
||||
],
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "net.aliasvault.app",
|
||||
@@ -51,7 +55,8 @@
|
||||
"enforceNavigationBarContrast": false
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"expo-localization"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Tabs, router } from 'expo-router';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
@@ -20,6 +21,7 @@ export default function TabLayout() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Check if user is authenticated and database is available
|
||||
const isFullyInitialized = authContext.isInitialized && dbContext.dbInitialized;
|
||||
@@ -95,7 +97,7 @@ export default function TabLayout() : React.ReactNode {
|
||||
<Tabs.Screen
|
||||
name="credentials"
|
||||
options={{
|
||||
title: 'Credentials',
|
||||
title: t('navigation.credentials'),
|
||||
/**
|
||||
* Icon for the credentials tab.
|
||||
*/
|
||||
@@ -105,7 +107,7 @@ export default function TabLayout() : React.ReactNode {
|
||||
<Tabs.Screen
|
||||
name="emails"
|
||||
options={{
|
||||
title: 'Emails',
|
||||
title: t('navigation.emails'),
|
||||
/**
|
||||
* Icon for the emails tab.
|
||||
*/
|
||||
@@ -115,7 +117,7 @@ export default function TabLayout() : React.ReactNode {
|
||||
<Tabs.Screen
|
||||
name="settings"
|
||||
options={{
|
||||
title: 'Settings',
|
||||
title: t('navigation.settings'),
|
||||
/**
|
||||
* Icon for the settings tab.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ActivityIndicator, View, Text, StyleSheet, TouchableOpacity, Linking, Pressable } from 'react-native';
|
||||
import { ActivityIndicator, View, Text, StyleSheet, TouchableOpacity, Linking, Pressable, Platform } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
@@ -11,6 +11,7 @@ import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
import { CredentialIcon } from '@/components/credentials/CredentialIcon';
|
||||
import { AliasDetails } from '@/components/credentials/details/AliasDetails';
|
||||
import { AttachmentSection } from '@/components/credentials/details/AttachmentSection';
|
||||
import { EmailPreview } from '@/components/credentials/details/EmailPreview';
|
||||
import { LoginCredentials } from '@/components/credentials/details/LoginCredentials';
|
||||
import { NotesSection } from '@/components/credentials/details/NotesSection';
|
||||
@@ -48,19 +49,32 @@ export default function CredentialDetailsScreen() : React.ReactNode {
|
||||
*/
|
||||
headerRight: () => (
|
||||
<View style={styles.headerRightContainer}>
|
||||
<Pressable
|
||||
onPressIn={handleEdit}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="edit"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</Pressable>
|
||||
{Platform.OS === 'android' ? (
|
||||
<Pressable
|
||||
onPressIn={handleEdit}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="edit"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</Pressable>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={handleEdit}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="edit"
|
||||
size={22}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
),
|
||||
});
|
||||
@@ -135,6 +149,7 @@ export default function CredentialDetailsScreen() : React.ReactNode {
|
||||
<LoginCredentials credential={credential} />
|
||||
<AliasDetails credential={credential} />
|
||||
<NotesSection credential={credential} />
|
||||
<AttachmentSection credential={credential} />
|
||||
</ThemedScrollView>
|
||||
</ThemedContainer>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import { defaultHeaderOptions } from '@/components/themed/ThemedHeader';
|
||||
@@ -8,12 +9,14 @@ import { defaultHeaderOptions } from '@/components/themed/ThemedHeader';
|
||||
* @returns {React.ReactNode} The credentials layout component
|
||||
*/
|
||||
export default function CredentialsLayout(): React.ReactNode {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Credentials',
|
||||
title: t('credentials.title'),
|
||||
headerShown: Platform.OS === 'android',
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
@@ -21,7 +24,7 @@ export default function CredentialsLayout(): React.ReactNode {
|
||||
<Stack.Screen
|
||||
name="add-edit"
|
||||
options={{
|
||||
title: 'Add Credential',
|
||||
title: t('credentials.addCredential'),
|
||||
presentation: Platform.OS === 'ios' ? 'modal' : 'card',
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
@@ -29,14 +32,14 @@ export default function CredentialsLayout(): React.ReactNode {
|
||||
<Stack.Screen
|
||||
name="add-edit-page"
|
||||
options={{
|
||||
title: 'Add Credential',
|
||||
title: t('credentials.addCredential'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="autofill-credential-created"
|
||||
options={{
|
||||
title: 'Credential Created',
|
||||
title: t('credentials.credentialCreated'),
|
||||
presentation: Platform.OS === 'ios' ? 'modal' : 'card',
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
@@ -44,14 +47,14 @@ export default function CredentialsLayout(): React.ReactNode {
|
||||
<Stack.Screen
|
||||
name="[id]"
|
||||
options={{
|
||||
title: 'Credential Details',
|
||||
title: t('credentials.credentialDetails'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="email/[id]"
|
||||
options={{
|
||||
title: 'Email Preview',
|
||||
title: t('credentials.emailPreview'),
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -4,21 +4,24 @@ import * as Haptics from 'expo-haptics';
|
||||
import { Stack, useLocalSearchParams, useNavigation, useRouter } from 'expo-router';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, Alert, Keyboard, KeyboardAvoidingView, Platform, Pressable } from 'react-native';
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { CreateIdentityGenerator, IdentityHelperUtils, IdentityGenerator } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CreateIdentityGenerator, IdentityGenerator, IdentityHelperUtils } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Attachment, Credential, PasswordSettings } from '@/utils/dist/shared/models/vault';
|
||||
import type { FaviconExtractModel } from '@/utils/dist/shared/models/webapi';
|
||||
import { CreatePasswordGenerator, PasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import { extractServiceNameFromUrl } from '@/utils/UrlUtility';
|
||||
import { credentialSchema } from '@/utils/ValidationSchema';
|
||||
import { createCredentialSchema } from '@/utils/ValidationSchema';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useVaultMutate } from '@/hooks/useVaultMutate';
|
||||
|
||||
import { AttachmentUploader } from '@/components/credentials/details/AttachmentUploader';
|
||||
import { AdvancedPasswordField } from '@/components/form/AdvancedPasswordField';
|
||||
import { ValidatedFormField, ValidatedFormFieldRef } from '@/components/form/ValidatedFormField';
|
||||
import LoadingOverlay from '@/components/LoadingOverlay';
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
@@ -47,9 +50,13 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
const serviceNameRef = useRef<ValidatedFormFieldRef>(null);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
|
||||
const [passwordSettings, setPasswordSettings] = useState<PasswordSettings | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { control, handleSubmit, setValue, watch } = useForm<Credential>({
|
||||
resolver: yupResolver(credentialSchema) as Resolver<Credential>,
|
||||
resolver: yupResolver(createCredentialSchema(t)) as Resolver<Credential>,
|
||||
defaultValues: {
|
||||
Id: "",
|
||||
Username: "",
|
||||
@@ -87,55 +94,75 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
if (existingCredential.Alias?.FirstName || existingCredential.Alias?.LastName) {
|
||||
setMode('manual');
|
||||
}
|
||||
|
||||
// Load attachments for this credential
|
||||
const credentialAttachments = await dbContext.sqliteClient!.getAttachmentsForCredential(id);
|
||||
setAttachments(credentialAttachments);
|
||||
setOriginalAttachmentIds(credentialAttachments.map(a => a.Id));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading credential:', err);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Failed to load credential',
|
||||
text2: 'Please try again'
|
||||
text1: t('credentials.errors.loadFailed'),
|
||||
text2: t('auth.errors.enterPassword')
|
||||
});
|
||||
}
|
||||
}, [id, dbContext.sqliteClient, setValue]);
|
||||
}, [id, dbContext.sqliteClient, setValue, t]);
|
||||
|
||||
/**
|
||||
* On mount, load an existing credential if we're in edit mode, or extract the service name from the service URL
|
||||
* if we're in add mode and the service URL is provided (by native autofill component).
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (authContext.isOffline) {
|
||||
// Show toast and close the modal
|
||||
setTimeout(() => {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'You are offline and in read-only mode. Please connect to the internet to add or edit a credential.',
|
||||
position: 'bottom'
|
||||
});
|
||||
}, 100);
|
||||
router.dismiss();
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Initialize the component by loading settings and handling initial state.
|
||||
*/
|
||||
const initializeComponent = async (): Promise<void> => {
|
||||
if (authContext.isOffline) {
|
||||
// Show toast and close the modal
|
||||
setTimeout(() => {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: t('credentials.offlineMessage'),
|
||||
position: 'bottom'
|
||||
});
|
||||
}, 100);
|
||||
router.dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditMode) {
|
||||
loadExistingCredential();
|
||||
} else if (serviceUrl) {
|
||||
const decodedUrl = decodeURIComponent(serviceUrl);
|
||||
const serviceName = extractServiceNameFromUrl(decodedUrl);
|
||||
setValue('ServiceUrl', decodedUrl);
|
||||
setValue('ServiceName', serviceName);
|
||||
}
|
||||
// Load password settings
|
||||
try {
|
||||
const settings = await dbContext.sqliteClient!.getPasswordSettings();
|
||||
setPasswordSettings(settings);
|
||||
} catch (err) {
|
||||
console.error('Error loading password settings:', err);
|
||||
}
|
||||
|
||||
// On create mode, focus the service name field after a short delay to ensure the component is mounted
|
||||
if (!isEditMode) {
|
||||
setTimeout(() => {
|
||||
serviceNameRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
}, [id, isEditMode, serviceUrl, loadExistingCredential, setValue, authContext.isOffline, router]);
|
||||
if (isEditMode) {
|
||||
loadExistingCredential();
|
||||
} else if (serviceUrl) {
|
||||
const decodedUrl = decodeURIComponent(serviceUrl);
|
||||
const serviceName = extractServiceNameFromUrl(decodedUrl);
|
||||
setValue('ServiceUrl', decodedUrl);
|
||||
setValue('ServiceName', serviceName);
|
||||
}
|
||||
|
||||
// On create mode, focus the service name field after a short delay to ensure the component is mounted
|
||||
if (!isEditMode) {
|
||||
setTimeout(() => {
|
||||
serviceNameRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
initializeComponent();
|
||||
}, [id, isEditMode, serviceUrl, loadExistingCredential, setValue, authContext.isOffline, router, t, dbContext.sqliteClient]);
|
||||
|
||||
/**
|
||||
* Initialize the identity and password generators with settings from user's vault.
|
||||
* @returns {identityGenerator: IdentityGenerator, passwordGenerator: PasswordGenerator}
|
||||
* @returns {identityGenerator: IIdentityGenerator, passwordGenerator: PasswordGenerator}
|
||||
*/
|
||||
const initializeGenerators = useCallback(async () : Promise<{ identityGenerator: IdentityGenerator, passwordGenerator: PasswordGenerator }> => {
|
||||
// Get default identity language from database
|
||||
@@ -280,9 +307,9 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
|
||||
await executeVaultMutation(async () => {
|
||||
if (isEditMode) {
|
||||
await dbContext.sqliteClient!.updateCredentialById(credentialToSave);
|
||||
await dbContext.sqliteClient!.updateCredentialById(credentialToSave, originalAttachmentIds, attachments);
|
||||
} else {
|
||||
const credentialId = await dbContext.sqliteClient!.createCredential(credentialToSave);
|
||||
const credentialId = await dbContext.sqliteClient!.createCredential(credentialToSave, attachments);
|
||||
credentialToSave.Id = credentialId;
|
||||
}
|
||||
|
||||
@@ -306,14 +333,14 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
setTimeout(() => {
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
text1: isEditMode ? 'Credential updated successfully' : 'Credential created successfully',
|
||||
text1: isEditMode ? t('credentials.toasts.credentialUpdated') : t('credentials.toasts.credentialCreated'),
|
||||
position: 'bottom'
|
||||
});
|
||||
}, 200);
|
||||
|
||||
setIsSyncing(false);
|
||||
}
|
||||
}, [isEditMode, id, serviceUrl, router, executeVaultMutation, dbContext.sqliteClient, mode, generateRandomAlias, webApi, watch, setIsSaveDisabled, setIsSyncing, isSaveDisabled]);
|
||||
}, [isEditMode, id, serviceUrl, router, executeVaultMutation, dbContext.sqliteClient, mode, generateRandomAlias, webApi, watch, setIsSaveDisabled, setIsSyncing, isSaveDisabled, t, originalAttachmentIds, attachments]);
|
||||
|
||||
/**
|
||||
* Generate a random username.
|
||||
@@ -334,8 +361,8 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
console.error('Error generating random username:', error);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Failed to generate username',
|
||||
text2: 'Please try again'
|
||||
text1: t('credentials.errors.generateUsernameFailed'),
|
||||
text2: t('auth.errors.enterPassword')
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -353,8 +380,8 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
console.error('Error generating random password:', error);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Failed to generate password',
|
||||
text2: 'Please try again'
|
||||
text1: t('credentials.errors.generatePasswordFailed'),
|
||||
text2: t('auth.errors.enterPassword')
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -370,15 +397,15 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
Keyboard.dismiss();
|
||||
|
||||
Alert.alert(
|
||||
"Delete Credential",
|
||||
"Are you sure you want to delete this credential? This action cannot be undone.",
|
||||
t('credentials.deleteCredential'),
|
||||
t('credentials.deleteConfirm'),
|
||||
[
|
||||
{
|
||||
text: "Cancel",
|
||||
text: t('common.cancel'),
|
||||
style: "cancel"
|
||||
},
|
||||
{
|
||||
text: "Delete",
|
||||
text: t('common.delete'),
|
||||
style: "destructive",
|
||||
/**
|
||||
* Delete the credential.
|
||||
@@ -394,7 +421,7 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
setTimeout(() => {
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
text1: 'Credential deleted successfully',
|
||||
text1: t('credentials.toasts.credentialDeleted'),
|
||||
position: 'bottom'
|
||||
});
|
||||
}, 200);
|
||||
@@ -441,7 +468,6 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
borderRadius: 8,
|
||||
flexDirection: 'row',
|
||||
marginBottom: 8,
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
@@ -518,7 +544,7 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
onPress={() => router.back()}
|
||||
style={styles.headerLeftButton}
|
||||
>
|
||||
<ThemedText style={styles.headerLeftButtonText}>Cancel</ThemedText>
|
||||
<ThemedText style={styles.headerLeftButtonText}>{t('common.cancel')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
/**
|
||||
@@ -541,11 +567,11 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
*/
|
||||
headerRight: () => (
|
||||
<Pressable
|
||||
onPress={handleSubmit(onSubmit)}
|
||||
style={[styles.headerRightButton, isSaveDisabled && styles.headerRightButtonDisabled]}
|
||||
onPressIn={handleSubmit(onSubmit)}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
style={[styles.headerRightButton, isSaveDisabled && styles.headerRightButtonDisabled]}
|
||||
disabled={isSaveDisabled}
|
||||
>
|
||||
<MaterialIcons name="save" size={24} color={colors.primary} />
|
||||
@@ -553,11 +579,11 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [navigation, mode, handleSubmit, onSubmit, colors.primary, isEditMode, router, styles.headerLeftButton, styles.headerLeftButtonText, styles.headerRightButton, styles.headerRightButtonDisabled, isSaveDisabled]);
|
||||
}, [navigation, mode, handleSubmit, onSubmit, colors.primary, isEditMode, router, styles.headerLeftButton, styles.headerLeftButtonText, styles.headerRightButton, styles.headerRightButtonDisabled, isSaveDisabled, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: isEditMode ? 'Edit Credential' : 'Add Credential' }} />
|
||||
<Stack.Screen options={{ title: isEditMode ? t('credentials.editCredential') : t('credentials.addCredential') }} />
|
||||
{(isSyncing) && (
|
||||
<LoadingOverlay status={syncStatus} />
|
||||
)}
|
||||
@@ -584,7 +610,7 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
color={mode === 'random' ? colors.primarySurfaceText : colors.text}
|
||||
/>
|
||||
<ThemedText style={[styles.modeButtonText, mode === 'random' && styles.modeButtonTextActive]}>
|
||||
Random Alias
|
||||
{t('credentials.randomAlias')}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
@@ -597,36 +623,41 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
color={mode === 'manual' ? colors.primarySurfaceText : colors.text}
|
||||
/>
|
||||
<ThemedText style={[styles.modeButtonText, mode === 'manual' && styles.modeButtonTextActive]}>
|
||||
Manual
|
||||
{t('credentials.manual')}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>Service</ThemedText>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.service')}</ThemedText>
|
||||
<ValidatedFormField
|
||||
ref={serviceNameRef}
|
||||
control={control}
|
||||
name="ServiceName"
|
||||
label="Service Name"
|
||||
label={t('credentials.serviceName')}
|
||||
required
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="ServiceUrl"
|
||||
label="Service URL"
|
||||
label={t('credentials.serviceUrl')}
|
||||
/>
|
||||
</View>
|
||||
{(mode === 'manual' || isEditMode) && (
|
||||
<>
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>Login credentials</ThemedText>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.loginCredentials')}</ThemedText>
|
||||
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.Email"
|
||||
label={t('credentials.email')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Username"
|
||||
label="Username"
|
||||
label={t('credentials.username')}
|
||||
buttons={[
|
||||
{
|
||||
icon: "refresh",
|
||||
@@ -634,73 +665,80 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Password"
|
||||
label="Password"
|
||||
secureTextEntry={!isPasswordVisible}
|
||||
buttons={[
|
||||
{
|
||||
icon: isPasswordVisible ? "visibility-off" : "visibility",
|
||||
/**
|
||||
* Toggle the visibility of the password.
|
||||
*/
|
||||
onPress: () => setIsPasswordVisible(!isPasswordVisible)
|
||||
},
|
||||
{
|
||||
icon: "refresh",
|
||||
onPress: generateRandomPassword
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<TouchableOpacity style={styles.generateButton} onPress={handleGenerateRandomAlias}>
|
||||
<MaterialIcons name="auto-fix-high" size={20} color="#fff" />
|
||||
<ThemedText style={styles.generateButtonText}>Generate Random Alias</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.Email"
|
||||
label="Email"
|
||||
/>
|
||||
{passwordSettings ? (
|
||||
<AdvancedPasswordField
|
||||
control={control}
|
||||
name="Password"
|
||||
label={t('credentials.password')}
|
||||
initialSettings={passwordSettings}
|
||||
showPassword={isPasswordVisible}
|
||||
onShowPasswordChange={setIsPasswordVisible}
|
||||
isNewCredential={!isEditMode}
|
||||
/>
|
||||
) : (
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Password"
|
||||
label={t('credentials.password')}
|
||||
secureTextEntry={!isPasswordVisible}
|
||||
buttons={[
|
||||
{
|
||||
icon: isPasswordVisible ? "visibility-off" : "visibility",
|
||||
/**
|
||||
* Toggle the visibility of the password.
|
||||
*/
|
||||
onPress: () => setIsPasswordVisible(!isPasswordVisible)
|
||||
},
|
||||
{
|
||||
icon: "refresh",
|
||||
onPress: generateRandomPassword
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>Alias</ThemedText>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.alias')}</ThemedText>
|
||||
<TouchableOpacity style={styles.generateButton} onPress={handleGenerateRandomAlias}>
|
||||
<MaterialIcons name="auto-fix-high" size={20} color="#fff" />
|
||||
<ThemedText style={styles.generateButtonText}>{t('credentials.generateRandomAlias')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.FirstName"
|
||||
label="First Name"
|
||||
label={t('credentials.firstName')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.LastName"
|
||||
label="Last Name"
|
||||
label={t('credentials.lastName')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.NickName"
|
||||
label="Nick Name"
|
||||
label={t('credentials.nickName')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.Gender"
|
||||
label="Gender"
|
||||
label={t('credentials.gender')}
|
||||
/>
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Alias.BirthDate"
|
||||
label="Birth Date"
|
||||
placeholder="YYYY-MM-DD"
|
||||
label={t('credentials.birthDate')}
|
||||
placeholder={t('credentials.birthDatePlaceholder')}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>Metadata</ThemedText>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.metadata')}</ThemedText>
|
||||
|
||||
<ValidatedFormField
|
||||
control={control}
|
||||
name="Notes"
|
||||
label="Notes"
|
||||
label={t('credentials.notes')}
|
||||
multiline={true}
|
||||
numberOfLines={4}
|
||||
textAlignVertical="top"
|
||||
@@ -708,12 +746,21 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
{/* TODO: Add TOTP management */}
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>{t('credentials.attachments')}</ThemedText>
|
||||
|
||||
<AttachmentUploader
|
||||
attachments={attachments}
|
||||
onAttachmentsChange={setAttachments}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{isEditMode && (
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={handleDelete}
|
||||
>
|
||||
<ThemedText style={styles.deleteButtonText}>Delete Credential</ThemedText>
|
||||
<ThemedText style={styles.deleteButtonText}>{t('credentials.deleteCredential')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useNavigation, useRouter } from 'expo-router';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { StyleSheet, View, TouchableOpacity, AppState } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, AppState, Platform, Pressable } from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
@@ -16,6 +17,7 @@ export default function AutofillCredentialCreatedScreen() : React.ReactNode {
|
||||
const router = useRouter();
|
||||
const colors = useColors();
|
||||
const navigation = useNavigation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
* Handle the stay in app button press.
|
||||
@@ -78,16 +80,27 @@ export default function AutofillCredentialCreatedScreen() : React.ReactNode {
|
||||
/**
|
||||
* Header right button.
|
||||
*/
|
||||
headerRight: () => (
|
||||
<TouchableOpacity
|
||||
onPress={handleStayInApp}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<ThemedText style={{ color: colors.primary }}>Dismiss</ThemedText>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerRight: () =>
|
||||
Platform.OS === 'android' ? (
|
||||
<Pressable
|
||||
onPressIn={handleStayInApp}
|
||||
android_ripple={{ color: 'lightgray' }}
|
||||
pressRetentionOffset={100}
|
||||
hitSlop={100}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<ThemedText style={{ color: colors.primary }}>{t('common.cancel')}</ThemedText>
|
||||
</Pressable>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={handleStayInApp}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<ThemedText style={{ color: colors.primary }}>{t('common.cancel')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
});
|
||||
}, [navigation, colors.primary, styles.headerRightButton, handleStayInApp]);
|
||||
}, [navigation, colors.primary, styles.headerRightButton, handleStayInApp, t]);
|
||||
|
||||
return (
|
||||
<ThemedSafeAreaView style={styles.container}>
|
||||
@@ -100,13 +113,13 @@ export default function AutofillCredentialCreatedScreen() : React.ReactNode {
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ThemedText style={styles.title}>Credential Created!</ThemedText>
|
||||
<ThemedText style={styles.title}>{t('credentials.credentialCreated')}</ThemedText>
|
||||
|
||||
<ThemedText style={styles.message}>
|
||||
Your new credential has been added to your vault and is now available for password autofill.
|
||||
{t('credentials.credentialCreatedMessage')}
|
||||
</ThemedText>
|
||||
<ThemedText style={[styles.message, styles.boldMessage]}>
|
||||
Switch back to your browser to continue.
|
||||
{t('credentials.switchBackToBrowser')}
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
</ThemedSafeAreaView>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, Text, FlatList, TouchableOpacity, TextInput, RefreshControl, Platform, Animated, Alert } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import Toast from 'react-native-toast-message';
|
||||
@@ -37,6 +38,7 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
const { syncVault } = useVaultSync();
|
||||
const webApi = useWebApi();
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
const scrollY = useRef(new Animated.Value(0)).current;
|
||||
const navigation = useNavigation();
|
||||
@@ -69,12 +71,12 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
// Error loading credentials, show error toast
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Error loading credentials',
|
||||
text1: t('credentials.errorLoadingCredentials'),
|
||||
text2: err instanceof Error ? err.message : 'Unknown error',
|
||||
});
|
||||
setIsLoadingCredentials(false);
|
||||
}
|
||||
}, [dbContext.sqliteClient, setIsLoadingCredentials]);
|
||||
}, [dbContext.sqliteClient, setIsLoadingCredentials, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeFocus = navigation.addListener('focus', () => {
|
||||
@@ -140,7 +142,7 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
setTimeout(() => {
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
text1: hasNewVault ? 'Vault synced successfully' : 'Vault is up-to-date',
|
||||
text1: hasNewVault ? t('credentials.vaultSyncedSuccessfully') : t('credentials.vaultUpToDate'),
|
||||
position: 'top',
|
||||
visibilityTime: 1200,
|
||||
});
|
||||
@@ -156,7 +158,7 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
setTimeout(() => {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'You are offline. Please connect to the internet to sync your vault.',
|
||||
text1: t('credentials.offlineMessage'),
|
||||
position: 'bottom',
|
||||
});
|
||||
}, 200);
|
||||
@@ -188,11 +190,11 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
setIsLoadingCredentials(false);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Vault sync failed',
|
||||
text1: t('credentials.vaultSyncFailed'),
|
||||
text2: err instanceof Error ? err.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}, [syncVault, loadCredentials, setIsLoadingCredentials, setRefreshing, webApi, authContext, router]);
|
||||
}, [syncVault, loadCredentials, setIsLoadingCredentials, setRefreshing, webApi, authContext, router, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !isDatabaseAvailable) {
|
||||
@@ -206,12 +208,22 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
const filteredCredentials = credentialsList.filter(credential => {
|
||||
const searchLower = searchQuery.toLowerCase();
|
||||
|
||||
return (
|
||||
credential.ServiceName?.toLowerCase().includes(searchLower) ??
|
||||
credential.Username?.toLowerCase().includes(searchLower) ??
|
||||
credential.Alias?.Email?.toLowerCase().includes(searchLower) ??
|
||||
credential.ServiceUrl?.toLowerCase().includes(searchLower)
|
||||
);
|
||||
/**
|
||||
* We filter credentials by searching in the following fields:
|
||||
* - Service name
|
||||
* - Username
|
||||
* - Alias email
|
||||
* - Service URL
|
||||
* - Notes
|
||||
*/
|
||||
const searchableFields = [
|
||||
credential.ServiceName?.toLowerCase(),
|
||||
credential.Username?.toLowerCase(),
|
||||
credential.Alias?.Email?.toLowerCase(),
|
||||
credential.ServiceUrl?.toLowerCase(),
|
||||
credential.Notes?.toLowerCase(),
|
||||
];
|
||||
return searchableFields.some(field => field?.includes(searchLower));
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -297,9 +309,9 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
* Define custom header which is shown on Android. iOS displays the custom CollapsibleHeader component instead.
|
||||
* @returns
|
||||
*/
|
||||
headerTitle: (): React.ReactNode => Platform.OS === 'android' ? <AndroidHeader title="Credentials" /> : <Text>Credentials</Text>,
|
||||
headerTitle: (): React.ReactNode => Platform.OS === 'android' ? <AndroidHeader title={t('credentials.title')} /> : <Text>{t('credentials.title')}</Text>,
|
||||
});
|
||||
}, [navigation]);
|
||||
}, [navigation, t]);
|
||||
|
||||
/**
|
||||
* Delete a credential.
|
||||
@@ -332,7 +344,7 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
<LoadingOverlay status={syncStatus} />
|
||||
)}
|
||||
<CollapsibleHeader
|
||||
title="Credentials"
|
||||
title={t('credentials.title')}
|
||||
scrollY={scrollY}
|
||||
showNavigationHeader={true}
|
||||
alwaysVisible={true}
|
||||
@@ -366,7 +378,7 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
removeClippedSubviews={false}
|
||||
ListHeaderComponent={
|
||||
<ThemedView>
|
||||
<TitleContainer title="Credentials" />
|
||||
<TitleContainer title={t('credentials.title')} />
|
||||
{serviceUrl && (
|
||||
<ServiceUrlNotice
|
||||
serviceUrl={serviceUrl}
|
||||
@@ -382,7 +394,7 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
placeholder="Search credentials..."
|
||||
placeholder={t('credentials.searchPlaceholder')}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
value={searchQuery}
|
||||
autoCorrect={false}
|
||||
@@ -419,13 +431,13 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
ListEmptyComponent={
|
||||
!isLoadingCredentials ? (
|
||||
<Text style={styles.emptyText}>
|
||||
{searchQuery ? 'No matching credentials found' : 'No credentials found. Create one to get started. Tip: you can also login to the AliasVault web app to import credentials from other password managers.'}
|
||||
{searchQuery ? t('credentials.noMatchingCredentials') : t('credentials.noCredentialsFound')}
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</ThemedView>
|
||||
{isLoading && <LoadingOverlay status={syncStatus || 'Deleting credential...'} />}
|
||||
{isLoading && <LoadingOverlay status={syncStatus || t('credentials.deletingCredential')} />}
|
||||
</ThemedContainer>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import { useLocalSearchParams, useRouter, useNavigation, Stack } from 'expo-router';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { StyleSheet, View, TouchableOpacity, ActivityIndicator, Alert, Share, useColorScheme, TextInput, Linking } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, ActivityIndicator, Alert, Share, useColorScheme, Linking, Text, TextInput, Platform, Pressable } from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
@@ -29,6 +30,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [email, setEmail] = useState<Email | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -70,11 +72,11 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
setHtmlView(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
setError(err instanceof Error ? err.message : t('emails.errors.generic'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [dbContext.sqliteClient, id, webApi]);
|
||||
}, [dbContext.sqliteClient, id, webApi, t]);
|
||||
|
||||
useEffect(() => {
|
||||
loadEmail();
|
||||
@@ -85,15 +87,15 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
*/
|
||||
const handleDelete = useCallback(async () : Promise<void> => {
|
||||
Alert.alert(
|
||||
'Delete Email',
|
||||
'Are you sure you want to delete this email? This action is permanent and cannot be undone.',
|
||||
t('emails.deleteEmail'),
|
||||
t('emails.deleteEmailConfirm'),
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
text: t('common.cancel'),
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
text: t('common.delete'),
|
||||
style: 'destructive',
|
||||
/**
|
||||
* Handle the delete button press.
|
||||
@@ -109,13 +111,13 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
// Go back to the emails list screen.
|
||||
router.back();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete email');
|
||||
setError(err instanceof Error ? err.message : t('emails.errors.deleteFailed'));
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}, [id, router, webApi]);
|
||||
}, [id, router, webApi, t]);
|
||||
|
||||
/**
|
||||
* Handle the download attachment button press.
|
||||
@@ -127,7 +129,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
);
|
||||
|
||||
if (!dbContext?.sqliteClient || !email) {
|
||||
setError('Database context or email not available');
|
||||
setError(t('emails.errors.dbNotAvailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,7 +141,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
);
|
||||
|
||||
if (!decryptedBytes) {
|
||||
setError('Failed to decrypt attachment');
|
||||
setError(t('emails.errors.decryptFailed'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -158,7 +160,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
await FileSystem.deleteAsync(tempFile);
|
||||
} catch (err) {
|
||||
console.error('handleDownloadAttachment error', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to download attachment');
|
||||
setError(err instanceof Error ? err.message : t('emails.errors.downloadFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -232,6 +234,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
},
|
||||
metadataCredential: {
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
metadataCredentialIcon: {
|
||||
@@ -261,6 +264,10 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
justifyContent: 'flex-start',
|
||||
padding: 2,
|
||||
},
|
||||
metadataSubject: {
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
metadataText: {
|
||||
color: colors.text,
|
||||
fontSize: 13,
|
||||
@@ -317,22 +324,45 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
*/
|
||||
headerRight: () => (
|
||||
<View style={styles.headerRightContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => setHtmlView(!isHtmlView)}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons
|
||||
name={isHtmlView ? 'text-outline' : 'document-outline'}
|
||||
size={22}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={handleDelete}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={22} color="#FF0000" />
|
||||
</TouchableOpacity>
|
||||
{Platform.OS === 'android' ? (
|
||||
<>
|
||||
<Pressable
|
||||
onPressIn={() => setHtmlView(!isHtmlView)}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons
|
||||
name={isHtmlView ? 'text-outline' : 'document-outline'}
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPressIn={handleDelete}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={24} color="#FF0000" />
|
||||
</Pressable>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
onPress={() => setHtmlView(!isHtmlView)}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons
|
||||
name={isHtmlView ? 'text-outline' : 'document-outline'}
|
||||
size={22}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={handleDelete}
|
||||
style={styles.headerRightButton}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={22} color="#FF0000" />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
),
|
||||
});
|
||||
@@ -341,7 +371,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ThemedView style={styles.centerContainer}>
|
||||
<Stack.Screen options={{ title: 'Email Details' }} />
|
||||
<Stack.Screen options={{ title: t('emails.emailDetails') }} />
|
||||
<ActivityIndicator size="large" />
|
||||
</ThemedView>
|
||||
);
|
||||
@@ -350,7 +380,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
if (error) {
|
||||
return (
|
||||
<ThemedView style={styles.centerContainer}>
|
||||
<ThemedText style={styles.errorText}>Error: {error}</ThemedText>
|
||||
<ThemedText style={styles.errorText}>{t('common.error')}: {error}</ThemedText>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
@@ -358,7 +388,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
if (!email) {
|
||||
return (
|
||||
<ThemedView style={styles.centerContainer}>
|
||||
<ThemedText style={styles.emptyText}>Email not found</ThemedText>
|
||||
<ThemedText style={styles.emptyText}>{t('emails.emailNotFound')}</ThemedText>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
@@ -382,20 +412,20 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
<TouchableOpacity onPress={() => setMetadataMaximized(!isMetadataMaximized)}>
|
||||
<View style={styles.metadataContainer}>
|
||||
<View style={styles.metadataRow}>
|
||||
<View style={styles.metadataLabel}>
|
||||
<ThemedText style={styles.metadataHeading}>Subject:</ThemedText>
|
||||
</View>
|
||||
<View style={styles.metadataValue}>
|
||||
<ThemedText style={styles.metadataText}>{email.subject}</ThemedText>
|
||||
<ThemedText style={[styles.metadataText, styles.metadataSubject]}>{email.subject}</ThemedText>
|
||||
{associatedCredential && (
|
||||
<>
|
||||
<TouchableOpacity onPress={handleOpenCredential} style={styles.metadataCredential}>
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
onPress={handleOpenCredential}
|
||||
style={styles.metadataCredential}
|
||||
>
|
||||
<IconSymbol size={16} name={IconSymbolName.Key} color={colors.primary} style={styles.metadataCredentialIcon} />
|
||||
<ThemedText style={[styles.metadataText, { color: colors.primary }]}>
|
||||
{associatedCredential.ServiceName}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.metadataIcon}>
|
||||
@@ -406,7 +436,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
|
||||
<View style={styles.metadataRow}>
|
||||
<View style={styles.metadataLabel}>
|
||||
<ThemedText style={styles.metadataHeading}>Date:</ThemedText>
|
||||
<ThemedText style={styles.metadataHeading}>{t('emails.date')}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.metadataValue}>
|
||||
<ThemedText style={styles.metadataText}>
|
||||
@@ -418,7 +448,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
|
||||
<View style={styles.metadataRow}>
|
||||
<View style={styles.metadataLabel}>
|
||||
<ThemedText style={styles.metadataHeading}>From:</ThemedText>
|
||||
<ThemedText style={styles.metadataHeading}>{t('emails.from')}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.metadataValue}>
|
||||
<ThemedText style={styles.metadataText}>
|
||||
@@ -430,7 +460,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
|
||||
<View style={styles.metadataRow}>
|
||||
<View style={styles.metadataLabel}>
|
||||
<ThemedText style={styles.metadataHeading}>To:</ThemedText>
|
||||
<ThemedText style={styles.metadataHeading}>{t('emails.to')}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.metadataValue}>
|
||||
<ThemedText style={styles.metadataText}>
|
||||
@@ -460,24 +490,32 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
emailView = (
|
||||
emailView = Platform.OS === 'ios' ? (
|
||||
<TextInput
|
||||
style={[styles.plainText, isDarkMode ? styles.textDark : styles.textLight]}
|
||||
value={email.messagePlain || 'This email does not contain any plain-text.'}
|
||||
editable={false}
|
||||
multiline
|
||||
editable={false}
|
||||
selectTextOnFocus={true}
|
||||
style={[styles.plainText, isDarkMode ? styles.textDark : styles.textLight]}
|
||||
value={email.messagePlain || t('emails.noPlainText')}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
selectable
|
||||
style={[styles.plainText, isDarkMode ? styles.textDark : styles.textLight]}
|
||||
>
|
||||
{email.messagePlain || t('emails.noPlainText')}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<Stack.Screen options={{ title: 'Email Details' }} />
|
||||
<Stack.Screen options={{ title: t('emails.emailDetails') }} />
|
||||
{metadataView}
|
||||
{emailView}
|
||||
{email.attachments && email.attachments.length > 0 && (
|
||||
<View style={styles.attachments}>
|
||||
<ThemedText style={styles.attachmentsTitle}>Attachments</ThemedText>
|
||||
<ThemedText style={styles.attachmentsTitle}>{t('emails.attachments')}</ThemedText>
|
||||
{email.attachments.map((attachment) => (
|
||||
<TouchableOpacity
|
||||
key={attachment.id}
|
||||
@@ -486,7 +524,7 @@ export default function EmailDetailsScreen() : React.ReactNode {
|
||||
>
|
||||
<Ionicons name="attach" size={20} color="#666" />
|
||||
<ThemedText style={styles.attachmentName}>
|
||||
{attachment.filename} ({Math.ceil(attachment.filesize / 1024)} KB)
|
||||
{attachment.filename} ({Math.ceil(attachment.filesize / 1024)} {t('emails.sizeKB')})
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Platform, Text } from 'react-native';
|
||||
|
||||
import { defaultHeaderOptions } from '@/components/themed/ThemedHeader';
|
||||
@@ -8,12 +9,13 @@ import { AndroidHeader } from '@/components/ui/AndroidHeader';
|
||||
* Emails layout.
|
||||
*/
|
||||
export default function EmailsLayout(): React.ReactNode {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Emails',
|
||||
title: t('emails.title'),
|
||||
headerShown: Platform.OS === 'android',
|
||||
/**
|
||||
* On Android, we use a custom header component that includes the AliasVault logo.
|
||||
@@ -27,7 +29,7 @@ export default function EmailsLayout(): React.ReactNode {
|
||||
<Stack.Screen
|
||||
name="[id]"
|
||||
options={{
|
||||
title: 'Email',
|
||||
title: t('emails.title'),
|
||||
...defaultHeaderOptions,
|
||||
headerTransparent: false,
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useNavigation } from 'expo-router';
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, ScrollView, RefreshControl, Animated , Platform } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import Toast from 'react-native-toast-message';
|
||||
@@ -26,6 +27,7 @@ import { useWebApi } from '@/context/WebApiContext';
|
||||
* Emails screen.
|
||||
*/
|
||||
export default function EmailsScreen() : React.ReactNode {
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const authContext = useAuth();
|
||||
@@ -81,17 +83,17 @@ export default function EmailsScreen() : React.ReactNode {
|
||||
// Show toast and throw error
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Failed to load emails',
|
||||
text1: t('emails.errors.loadFailed'),
|
||||
position: 'bottom',
|
||||
});
|
||||
throw new Error('Failed to load emails');
|
||||
throw new Error(t('emails.errors.loadFailed'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
setError(err instanceof Error ? err.message : t('emails.errors.generic'));
|
||||
}
|
||||
}, [dbContext?.sqliteClient, webApi, setIsLoading, authContext.isOffline]);
|
||||
}, [dbContext?.sqliteClient, webApi, setIsLoading, authContext.isOffline, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeFocus = navigation.addListener('focus', () => {
|
||||
@@ -190,7 +192,7 @@ export default function EmailsScreen() : React.ReactNode {
|
||||
if (authContext.isOffline) {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<ThemedText style={styles.emptyText}>You are offline. Please connect to the internet to load your emails.</ThemedText>
|
||||
<ThemedText style={styles.emptyText}>{t('emails.offlineMessage')}</ThemedText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -198,7 +200,7 @@ export default function EmailsScreen() : React.ReactNode {
|
||||
if (error) {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<ThemedText style={styles.errorText}>Error: {error}</ThemedText>
|
||||
<ThemedText style={styles.errorText}>{t('common.error')}: {error}</ThemedText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -207,7 +209,7 @@ export default function EmailsScreen() : React.ReactNode {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<ThemedText style={styles.emptyText}>
|
||||
You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.
|
||||
{t('emails.emptyMessage')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
);
|
||||
@@ -221,7 +223,7 @@ export default function EmailsScreen() : React.ReactNode {
|
||||
return (
|
||||
<ThemedContainer>
|
||||
<CollapsibleHeader
|
||||
title="Emails"
|
||||
title={t('emails.title')}
|
||||
scrollY={scrollY}
|
||||
showNavigationHeader={true}
|
||||
/>
|
||||
@@ -243,7 +245,7 @@ export default function EmailsScreen() : React.ReactNode {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TitleContainer title="Emails" />
|
||||
<TitleContainer title={t('emails.title')} />
|
||||
{renderContent()}
|
||||
</Animated.ScrollView>
|
||||
</ThemedContainer>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Platform, Text } from 'react-native';
|
||||
|
||||
import { defaultHeaderOptions } from '@/components/themed/ThemedHeader';
|
||||
@@ -8,94 +9,96 @@ import { AndroidHeader } from '@/components/ui/AndroidHeader';
|
||||
* Settings layout.
|
||||
*/
|
||||
export default function SettingsLayout(): React.ReactNode {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Settings',
|
||||
title: t('settings.title'),
|
||||
headerShown: Platform.OS === 'android',
|
||||
/**
|
||||
* On Android, we use a custom header component that includes the AliasVault logo.
|
||||
* On iOS, we don't show the header as a custom collapsible header is used.
|
||||
* @returns {React.ReactNode} The header component
|
||||
*/
|
||||
headerTitle: (): React.ReactNode => Platform.OS === 'android' ? <AndroidHeader title="Settings" /> : <Text>Settings</Text>,
|
||||
headerTitle: (): React.ReactNode => Platform.OS === 'android' ? <AndroidHeader title={t('settings.title')} /> : <Text>{t('settings.title')}</Text>,
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ios-autofill"
|
||||
options={{
|
||||
title: 'iOS Autofill',
|
||||
headerBackTitle: 'Settings',
|
||||
title: t('settings.iosAutofill'),
|
||||
headerBackTitle: t('settings.title'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="android-autofill"
|
||||
options={{
|
||||
title: 'Android Autofill',
|
||||
headerBackTitle: 'Settings',
|
||||
title: t('settings.androidAutofill'),
|
||||
headerBackTitle: t('settings.title'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="vault-unlock"
|
||||
options={{
|
||||
title: 'Vault Unlock Method',
|
||||
headerBackTitle: 'Settings',
|
||||
title: t('settings.vaultUnlock'),
|
||||
headerBackTitle: t('settings.title'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="auto-lock"
|
||||
options={{
|
||||
title: 'Auto-lock Timeout',
|
||||
headerBackTitle: 'Settings',
|
||||
title: t('settings.autoLock'),
|
||||
headerBackTitle: t('settings.title'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="identity-generator"
|
||||
options={{
|
||||
title: 'Identity Generator',
|
||||
title: t('settings.identityGenerator'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/index"
|
||||
options={{
|
||||
title: 'Security Settings',
|
||||
headerBackTitle: 'Settings',
|
||||
title: t('settings.securitySettings.title'),
|
||||
headerBackTitle: t('settings.title'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/change-password"
|
||||
options={{
|
||||
title: 'Change Password',
|
||||
title: t('settings.securitySettings.changePassword.changePassword'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/active-sessions"
|
||||
options={{
|
||||
title: 'Active Sessions',
|
||||
title: t('settings.securitySettings.activeSessionsTitle'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/auth-logs"
|
||||
options={{
|
||||
title: 'Auth Logs',
|
||||
title: t('settings.securitySettings.recentAuthLogs'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/delete-account"
|
||||
options={{
|
||||
title: 'Delete Account',
|
||||
title: t('settings.securitySettings.deleteAccountTitle'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity } from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -14,6 +15,7 @@ import { useAuth } from '@/context/AuthContext';
|
||||
*/
|
||||
export default function AutoLockScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const { getAutoLockTimeout, setAutoLockTimeout } = useAuth();
|
||||
const [autoLockTimeout, setAutoLockTimeoutState] = useState<number>(0);
|
||||
|
||||
@@ -29,15 +31,15 @@ export default function AutoLockScreen() : React.ReactNode {
|
||||
}, [getAutoLockTimeout]);
|
||||
|
||||
const timeoutOptions = [
|
||||
{ label: 'Never', value: 0 },
|
||||
{ label: '5 seconds', value: 5 },
|
||||
{ label: '30 seconds', value: 30 },
|
||||
{ label: '1 minute', value: 60 },
|
||||
{ label: '15 minutes', value: 900 },
|
||||
{ label: '30 minutes', value: 1800 },
|
||||
{ label: '1 hour', value: 3600 },
|
||||
{ label: '4 hours', value: 14400 },
|
||||
{ label: '8 hours', value: 28800 },
|
||||
{ label: t('settings.autoLockOptions.never'), value: 0 },
|
||||
{ label: t('settings.autoLockOptions.5seconds'), value: 5 },
|
||||
{ label: t('settings.autoLockOptions.30seconds'), value: 30 },
|
||||
{ label: t('settings.autoLockOptions.1minute'), value: 60 },
|
||||
{ label: t('settings.autoLockOptions.15minutes'), value: 900 },
|
||||
{ label: t('settings.autoLockOptions.30minutes'), value: 1800 },
|
||||
{ label: t('settings.autoLockOptions.1hour'), value: 3600 },
|
||||
{ label: t('settings.autoLockOptions.4hours'), value: 14400 },
|
||||
{ label: t('settings.autoLockOptions.8hours'), value: 28800 },
|
||||
];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -76,7 +78,7 @@ export default function AutoLockScreen() : React.ReactNode {
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
<ThemedText style={styles.headerText}>
|
||||
Choose how long the app can stay in the background before requiring re-authentication. You'll need to use Face ID or enter your password to unlock the vault again.
|
||||
{t('settings.autoLockSettings.description')}
|
||||
</ThemedText>
|
||||
<View style={styles.optionContainer}>
|
||||
{timeoutOptions.map((option, index) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFocusEffect } from 'expo-router';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, Alert, TouchableOpacity } from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -11,28 +12,31 @@ import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
|
||||
const LANGUAGE_OPTIONS = [
|
||||
{ label: 'English', value: 'en' },
|
||||
{ label: 'Dutch', value: 'nl' }
|
||||
];
|
||||
|
||||
const GENDER_OPTIONS = [
|
||||
{ label: 'Random', value: 'random' },
|
||||
{ label: 'Male', value: 'male' },
|
||||
{ label: 'Female', value: 'female' }
|
||||
];
|
||||
// Language and gender options will be defined inside the component to use translations
|
||||
|
||||
/**
|
||||
* Identity Generator Settings screen.
|
||||
*/
|
||||
export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const { executeVaultMutation } = useVaultMutate();
|
||||
|
||||
const [language, setLanguage] = useState<string>('en');
|
||||
const [gender, setGender] = useState<string>('random');
|
||||
|
||||
const LANGUAGE_OPTIONS = [
|
||||
{ label: t('settings.identityGeneratorSettings.languageOptions.english'), value: 'en' },
|
||||
{ label: t('settings.identityGeneratorSettings.languageOptions.dutch'), value: 'nl' }
|
||||
];
|
||||
|
||||
const GENDER_OPTIONS = [
|
||||
{ label: t('settings.identityGeneratorSettings.genderOptions.random'), value: 'random' },
|
||||
{ label: t('settings.identityGeneratorSettings.genderOptions.male'), value: 'male' },
|
||||
{ label: t('settings.identityGeneratorSettings.genderOptions.female'), value: 'female' }
|
||||
];
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
/**
|
||||
@@ -49,12 +53,12 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
|
||||
setGender(currentGender);
|
||||
} catch (error) {
|
||||
console.error('Error loading identity generator settings:', error);
|
||||
Alert.alert('Error', 'Failed to load identity generator settings.');
|
||||
Alert.alert(t('common.error'), t('settings.identityGeneratorSettings.errors.loadFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, [dbContext.sqliteClient])
|
||||
}, [dbContext.sqliteClient, t])
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -69,9 +73,9 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
|
||||
setLanguage(newLanguage);
|
||||
} catch (error) {
|
||||
console.error('Error updating language setting:', error);
|
||||
Alert.alert('Error', 'Failed to update language setting.');
|
||||
Alert.alert(t('common.error'), t('settings.identityGeneratorSettings.errors.languageUpdateFailed'));
|
||||
}
|
||||
}, [executeVaultMutation, dbContext.sqliteClient]);
|
||||
}, [executeVaultMutation, dbContext.sqliteClient, t]);
|
||||
|
||||
/**
|
||||
* Handle gender change.
|
||||
@@ -84,9 +88,9 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
|
||||
setGender(newGender);
|
||||
} catch (error) {
|
||||
console.error('Error updating gender setting:', error);
|
||||
Alert.alert('Error', 'Failed to update gender setting.');
|
||||
Alert.alert(t('common.error'), t('settings.identityGeneratorSettings.errors.genderUpdateFailed'));
|
||||
}
|
||||
}, [executeVaultMutation, dbContext.sqliteClient]);
|
||||
}, [executeVaultMutation, dbContext.sqliteClient, t]);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
descriptionText: {
|
||||
@@ -137,12 +141,12 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
<ThemedText style={styles.headerText}>
|
||||
Configure the default language and gender preference for generating new identities.
|
||||
{t('settings.identityGeneratorSettings.description')}
|
||||
</ThemedText>
|
||||
|
||||
<ThemedText style={styles.sectionTitle}>Language</ThemedText>
|
||||
<ThemedText style={styles.sectionTitle}>{t('settings.identityGeneratorSettings.languageSection')}</ThemedText>
|
||||
<ThemedText style={styles.descriptionText}>
|
||||
Set the language that will be used when generating new identities.
|
||||
{t('settings.identityGeneratorSettings.languageDescription')}
|
||||
</ThemedText>
|
||||
<View style={styles.optionContainer}>
|
||||
{LANGUAGE_OPTIONS.map((option, index) => {
|
||||
@@ -162,9 +166,9 @@ export default function IdentityGeneratorSettingsScreen(): React.ReactNode {
|
||||
})}
|
||||
</View>
|
||||
|
||||
<ThemedText style={styles.sectionTitle}>Gender</ThemedText>
|
||||
<ThemedText style={styles.sectionTitle}>{t('settings.identityGeneratorSettings.genderSection')}</ThemedText>
|
||||
<ThemedText style={styles.descriptionText}>
|
||||
Set the gender preference for generating new identities.
|
||||
{t('settings.identityGeneratorSettings.genderDescription')}
|
||||
</ThemedText>
|
||||
<View style={styles.optionContainer}>
|
||||
{GENDER_OPTIONS.map((option, index) => {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router, useFocusEffect } from 'expo-router';
|
||||
import { useRef, useState, useCallback } from 'react';
|
||||
import { StyleSheet, View, ScrollView, TouchableOpacity, Animated, Platform, Alert } from 'react-native';
|
||||
import { StyleSheet, View, ScrollView, TouchableOpacity, Animated, Platform, Alert, Linking } from 'react-native';
|
||||
|
||||
import { useApiUrl } from '@/utils/ApiUrlUtility';
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
@@ -24,6 +25,7 @@ import { useWebApi } from '@/context/WebApiContext';
|
||||
export default function SettingsScreen() : React.ReactNode {
|
||||
const webApi = useWebApi();
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const { getAuthMethodDisplay, shouldShowAutofillReminder } = useAuth();
|
||||
const { getAutoLockTimeout } = useAuth();
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
@@ -40,24 +42,24 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
*/
|
||||
const loadAutoLockDisplay = async () : Promise<void> => {
|
||||
const autoLockTimeout = await getAutoLockTimeout();
|
||||
let display = 'Never';
|
||||
let display = t('common.never');
|
||||
|
||||
if (autoLockTimeout === 5) {
|
||||
display = '5 seconds';
|
||||
display = t('settings.autoLockOptions.5seconds');
|
||||
} else if (autoLockTimeout === 30) {
|
||||
display = '30 seconds';
|
||||
display = t('settings.autoLockOptions.30seconds');
|
||||
} else if (autoLockTimeout === 60) {
|
||||
display = '1 minute';
|
||||
display = t('settings.autoLockOptions.1minute');
|
||||
} else if (autoLockTimeout === 900) {
|
||||
display = '15 minutes';
|
||||
display = t('settings.autoLockOptions.15minutes');
|
||||
} else if (autoLockTimeout === 1800) {
|
||||
display = '30 minutes';
|
||||
display = t('settings.autoLockOptions.30minutes');
|
||||
} else if (autoLockTimeout === 3600) {
|
||||
display = '1 hour';
|
||||
display = t('settings.autoLockOptions.1hour');
|
||||
} else if (autoLockTimeout === 14400) {
|
||||
display = '4 hours';
|
||||
display = t('settings.autoLockOptions.4hours');
|
||||
} else if (autoLockTimeout === 28800) {
|
||||
display = '8 hours';
|
||||
display = t('settings.autoLockOptions.8hours');
|
||||
}
|
||||
|
||||
setAutoLockDisplay(display);
|
||||
@@ -80,7 +82,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [getAutoLockTimeout, getAuthMethodDisplay, setIsFirstLoad, loadApiUrl])
|
||||
}, [getAutoLockTimeout, getAuthMethodDisplay, setIsFirstLoad, loadApiUrl, t])
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -89,11 +91,11 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
// Show native confirmation dialog
|
||||
Alert.alert(
|
||||
'Logout',
|
||||
'Are you sure you want to logout? You need to login again with your master password to access your vault.',
|
||||
t('auth.logout'),
|
||||
t('auth.confirmLogout'),
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: 'Logout', style: 'destructive',
|
||||
{ text: t('common.cancel'), style: 'cancel' },
|
||||
{ text: t('auth.logout'), style: 'destructive',
|
||||
/**
|
||||
* Handle the logout.
|
||||
*/
|
||||
@@ -141,6 +143,56 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
router.push('/(tabs)/settings/identity-generator');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the language settings press.
|
||||
*/
|
||||
const handleLanguagePress = (): void => {
|
||||
const isIOS = Platform.OS === 'ios';
|
||||
|
||||
Alert.alert(
|
||||
t('settings.language'),
|
||||
t('settings.languageSystemMessage'),
|
||||
[
|
||||
{ text: t('common.cancel'), style: 'cancel' },
|
||||
{
|
||||
text: t('settings.openSettings'),
|
||||
style: 'default',
|
||||
/**
|
||||
* Open platform-specific settings
|
||||
*/
|
||||
onPress: async (): Promise<void> => {
|
||||
if (isIOS) {
|
||||
// Open iOS Settings app
|
||||
await Linking.openURL('app-settings:');
|
||||
} else {
|
||||
// Fallback to general locale settings
|
||||
try {
|
||||
await Linking.openSettings();
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn('Failed to open general locale settings:', error);
|
||||
}
|
||||
|
||||
// Fallback to general settings
|
||||
try {
|
||||
await Linking.openSettings();
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn('Failed to open general settings:', error);
|
||||
}
|
||||
|
||||
// Final fallback - show manual instructions
|
||||
Alert.alert(
|
||||
t('common.error') ?? 'Error',
|
||||
'Unable to open device settings. Please manually navigate to the app settings and change the language.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollContent: {
|
||||
paddingBottom: 40,
|
||||
@@ -224,7 +276,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
return (
|
||||
<ThemedContainer>
|
||||
<CollapsibleHeader
|
||||
title="Settings"
|
||||
title={t('settings.title')}
|
||||
scrollY={scrollY}
|
||||
showNavigationHeader={false}
|
||||
/>
|
||||
@@ -239,7 +291,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
scrollIndicatorInsets={{ bottom: 40 }}
|
||||
style={styles.scrollView}
|
||||
>
|
||||
<TitleContainer title="Settings" />
|
||||
<TitleContainer title={t('settings.title')} />
|
||||
<UsernameDisplay />
|
||||
<View style={styles.section}>
|
||||
{Platform.OS === 'ios' && (
|
||||
@@ -252,7 +304,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="key-outline" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>iOS Autofill</ThemedText>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.iosAutofill')}</ThemedText>
|
||||
{shouldShowAutofillReminder && (
|
||||
<View style={styles.settingItemBadge}>
|
||||
<ThemedText style={styles.settingItemBadgeText}>1</ThemedText>
|
||||
@@ -274,7 +326,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="key-outline" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Android Autofill</ThemedText>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.androidAutofill')}</ThemedText>
|
||||
{shouldShowAutofillReminder && (
|
||||
<View style={styles.settingItemBadge}>
|
||||
<ThemedText style={styles.settingItemBadgeText}>1</ThemedText>
|
||||
@@ -294,7 +346,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="lock-closed" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Vault Unlock Method</ThemedText>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.vaultUnlock')}</ThemedText>
|
||||
{isFirstLoad ? (
|
||||
<InlineSkeletonLoader width={100} style={styles.skeletonLoader} />
|
||||
) : (
|
||||
@@ -312,7 +364,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="timer-outline" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Auto-lock Timeout</ThemedText>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.autoLock')}</ThemedText>
|
||||
{isFirstLoad ? (
|
||||
<InlineSkeletonLoader width={80} style={styles.skeletonLoader} />
|
||||
) : (
|
||||
@@ -321,6 +373,19 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.separator} />
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={handleLanguagePress}
|
||||
>
|
||||
<View style={styles.settingItemIcon}>
|
||||
<Ionicons name="language" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.language')}</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
@@ -332,7 +397,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="person-outline" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Identity Generator</ThemedText>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.identityGenerator')}</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -345,7 +410,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="shield-checkmark" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Security</ThemedText>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.security')}</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -360,13 +425,13 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="log-out" size={20} color={colors.primary} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={[styles.settingItemText, { color: colors.primary }]}>Logout</ThemedText>
|
||||
<ThemedText style={[styles.settingItemText, { color: colors.primary }]}>{t('auth.logout')}</ThemedText>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.versionContainer}>
|
||||
<ThemedText style={styles.versionText}>App version {AppInfo.VERSION} ({getDisplayUrl()})</ThemedText>
|
||||
<ThemedText style={styles.versionText}>{t('settings.appVersion', { version: AppInfo.VERSION, url: getDisplayUrl() })}</ThemedText>
|
||||
</View>
|
||||
</Animated.ScrollView>
|
||||
</ThemedContainer>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { router } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity } from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -14,6 +15,7 @@ import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
*/
|
||||
export default function IosAutofillScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const { markAutofillConfigured, shouldShowAutofillReminder } = useAuth();
|
||||
|
||||
/**
|
||||
@@ -96,13 +98,13 @@ export default function IosAutofillScreen() : React.ReactNode {
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
<ThemedText style={styles.headerText}>
|
||||
You can configure AliasVault to provide native password autofill functionality in iOS. Follow the instructions below to enable it.
|
||||
{t('settings.iosAutofillSettings.headerText')}
|
||||
</ThemedText>
|
||||
|
||||
<View style={styles.instructionContainer}>
|
||||
<ThemedText style={styles.instructionTitle}>How to enable:</ThemedText>
|
||||
<ThemedText style={styles.instructionTitle}>{t('settings.iosAutofillSettings.howToEnable')}</ThemedText>
|
||||
<ThemedText style={styles.instructionStep}>
|
||||
1. Open iOS Settings via the button below
|
||||
{t('settings.iosAutofillSettings.step1')}
|
||||
</ThemedText>
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
@@ -110,24 +112,24 @@ export default function IosAutofillScreen() : React.ReactNode {
|
||||
onPress={handleConfigurePress}
|
||||
>
|
||||
<ThemedText style={styles.configureButtonText}>
|
||||
Open iOS Settings
|
||||
{t('settings.iosAutofillSettings.openIosSettings')}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ThemedText style={styles.instructionStep}>
|
||||
2. Go to "General"
|
||||
{t('settings.iosAutofillSettings.step2')}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.instructionStep}>
|
||||
3. Tap "AutoFill & Passwords"
|
||||
{t('settings.iosAutofillSettings.step3')}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.instructionStep}>
|
||||
4. Enable "AliasVault"
|
||||
{t('settings.iosAutofillSettings.step4')}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.instructionStep}>
|
||||
5. Disable other password providers (e.g. "iCloud Passwords") to avoid conflicts
|
||||
{t('settings.iosAutofillSettings.step5')}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.warningText}>
|
||||
Note: You'll need to authenticate with Face ID/Touch ID or your device passcode when using autofill.
|
||||
{t('settings.iosAutofillSettings.warningText')}
|
||||
</ThemedText>
|
||||
<View style={styles.buttonContainer}>
|
||||
{shouldShowAutofillReminder && (
|
||||
@@ -136,7 +138,7 @@ export default function IosAutofillScreen() : React.ReactNode {
|
||||
onPress={handleAlreadyConfigured}
|
||||
>
|
||||
<ThemedText style={styles.secondaryButtonText}>
|
||||
I already configured it
|
||||
{t('settings.iosAutofillSettings.alreadyConfigured')}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, Alert, RefreshControl, Platform } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
@@ -19,6 +20,7 @@ import { useWebApi } from '@/context/WebApiContext';
|
||||
export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const webApi = useWebApi();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [refreshTokens, setRefreshTokens] = useState<RefreshToken[]>([]);
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 200);
|
||||
@@ -88,23 +90,23 @@ export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
const response = await webApi.getActiveSessions();
|
||||
setRefreshTokens(response);
|
||||
} catch {
|
||||
Alert.alert('Error', 'Failed to load active sessions');
|
||||
Alert.alert(t('common.error'), t('settings.securitySettings.activeSessions.failedToLoad'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [webApi, setIsLoading, setRefreshTokens]);
|
||||
}, [webApi, setIsLoading, setRefreshTokens, t]);
|
||||
|
||||
/**
|
||||
* Handle the revoke session action.
|
||||
*/
|
||||
const handleRevokeSession = async (sessionId: string) : Promise<void> => {
|
||||
Alert.alert(
|
||||
'Revoke Session',
|
||||
'Are you sure you want to revoke this session? This will log you out of the chosen device.',
|
||||
t('settings.securitySettings.activeSessions.revokeSession'),
|
||||
t('settings.securitySettings.activeSessions.revokeConfirmation'),
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: t('common.cancel'), style: 'cancel' },
|
||||
{
|
||||
text: 'Revoke',
|
||||
text: t('settings.securitySettings.activeSessions.revoke'),
|
||||
style: 'destructive',
|
||||
/**
|
||||
* Revoke the session and refresh the sessions.
|
||||
@@ -116,14 +118,14 @@ export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
|
||||
// Show success toast
|
||||
Toast.show({
|
||||
text1: 'Session successfully revoked',
|
||||
text1: t('settings.securitySettings.activeSessions.sessionRevoked'),
|
||||
type: 'success',
|
||||
position: 'bottom',
|
||||
});
|
||||
} catch {
|
||||
// Show error toast
|
||||
Toast.show({
|
||||
text1: 'Failed to revoke session',
|
||||
text1: t('settings.securitySettings.activeSessions.failedToRevoke'),
|
||||
type: 'error',
|
||||
position: 'bottom',
|
||||
});
|
||||
@@ -175,14 +177,14 @@ export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
}
|
||||
>
|
||||
<ThemedText style={styles.headerText}>
|
||||
Below is a list of devices where your account is currently logged in or has an active session. You can log out from any of these sessions here.
|
||||
{t('settings.securitySettings.activeSessions.headerText')}
|
||||
</ThemedText>
|
||||
<View style={styles.section}>
|
||||
{isLoading ? (
|
||||
<SkeletonLoader count={1} height={100} parts={3} />
|
||||
) : refreshTokens.length === 0 ? (
|
||||
<View style={styles.emptyState}>
|
||||
<ThemedText style={styles.emptyStateText}>No active sessions</ThemedText>
|
||||
<ThemedText style={styles.emptyStateText}>{t('settings.securitySettings.activeSessions.noSessions')}</ThemedText>
|
||||
</View>
|
||||
) : (
|
||||
refreshTokens.map((item) => (
|
||||
@@ -190,12 +192,12 @@ export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
<View style={styles.sessionHeader}>
|
||||
<ThemedText style={styles.deviceName} numberOfLines={2}>{item.deviceIdentifier}</ThemedText>
|
||||
<TouchableOpacity onPress={() => handleRevokeSession(item.id)}>
|
||||
<ThemedText style={styles.revokeButton}>Revoke</ThemedText>
|
||||
<ThemedText style={styles.revokeButton}>{t('settings.securitySettings.activeSessions.revoke')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.sessionDetails}>
|
||||
<ThemedText style={styles.detailText}>Last active: {formatDate(item.createdAt)}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>Expires: {formatDate(item.expireDate)}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>{t('settings.securitySettings.activeSessions.lastActive')}: {formatDate(item.createdAt)}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>{t('settings.securitySettings.activeSessions.expires')}: {formatDate(item.expireDate)}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, RefreshControl, Platform } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
@@ -21,6 +22,7 @@ import { useWebApi } from '@/context/WebApiContext';
|
||||
export default function AuthLogsScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const webApi = useWebApi();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [logs, setLogs] = useState<AuthLogModel[]>([]);
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 200);
|
||||
@@ -94,13 +96,13 @@ export default function AuthLogsScreen() : React.ReactNode {
|
||||
} catch {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Failed to load auth logs',
|
||||
text1: t('settings.securitySettings.authLogs.failedToLoad'),
|
||||
position: 'bottom',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [webApi, setIsLoading, setLogs]);
|
||||
}, [webApi, setIsLoading, setLogs, t]);
|
||||
|
||||
/**
|
||||
* Refresh the logs on pull to refresh.
|
||||
@@ -145,7 +147,7 @@ export default function AuthLogsScreen() : React.ReactNode {
|
||||
if (logs.length === 0) {
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<ThemedText style={styles.emptyStateText}>No auth logs found</ThemedText>
|
||||
<ThemedText style={styles.emptyStateText}>{t('settings.securitySettings.authLogs.noLogs')}</ThemedText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -161,14 +163,14 @@ export default function AuthLogsScreen() : React.ReactNode {
|
||||
styles.status,
|
||||
item.isSuccess ? styles.statusSuccess : styles.statusFailure
|
||||
]}>
|
||||
{item.isSuccess ? 'Success' : 'Failed'}
|
||||
{item.isSuccess ? t('settings.securitySettings.authLogs.success') : t('settings.securitySettings.authLogs.failed')}
|
||||
</ThemedText>
|
||||
</View>
|
||||
<View>
|
||||
<ThemedText style={styles.detailText}>Time: {formatDate(item.timestamp)}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>Device: {item.userAgent}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>IP Address: {item.ipAddress}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>Client: {item.client}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>{t('settings.securitySettings.authLogs.time')}: {formatDate(item.timestamp)}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>{t('settings.securitySettings.authLogs.device')}: {item.userAgent}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>{t('settings.securitySettings.authLogs.ipAddress')}: {item.ipAddress}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>{t('settings.securitySettings.authLogs.client')}: {item.client}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -188,7 +190,7 @@ export default function AuthLogsScreen() : React.ReactNode {
|
||||
}
|
||||
>
|
||||
<ThemedText style={styles.headerText}>
|
||||
Below you can find an overview of recent login attempts to your account.
|
||||
{t('settings.securitySettings.authLogs.headerText')}
|
||||
</ThemedText>
|
||||
<View style={styles.section}>
|
||||
{renderContent()}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { router } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, Alert, KeyboardAvoidingView, Platform } from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -22,6 +23,7 @@ export default function ChangePasswordScreen(): React.ReactNode {
|
||||
const colors = useColors();
|
||||
const authContext = useAuth();
|
||||
const { executeVaultPasswordChange, syncStatus } = useVaultMutate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
@@ -63,35 +65,35 @@ export default function ChangePasswordScreen(): React.ReactNode {
|
||||
*/
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
if (!currentPassword || !newPassword || !confirmPassword) {
|
||||
Alert.alert('Error', 'Please fill in all fields');
|
||||
Alert.alert(t('common.error'), t('settings.securitySettings.changePassword.fillAllFields'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
Alert.alert('Error', 'New passwords do not match');
|
||||
Alert.alert(t('common.error'), t('settings.securitySettings.changePassword.passwordsDoNotMatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authContext.username) {
|
||||
Alert.alert('Error', 'User not authenticated');
|
||||
Alert.alert(t('common.error'), t('settings.securitySettings.changePassword.userNotAuthenticated'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setLoadingStatus('Initiating password change...');
|
||||
setLoadingStatus(t('settings.securitySettings.changePassword.initiatingChange'));
|
||||
|
||||
const currentPasswordHashBase64 = await authContext.verifyPassword(currentPassword);
|
||||
if (!currentPasswordHashBase64) {
|
||||
Alert.alert('Error', 'Current password is not correct');
|
||||
Alert.alert(t('common.error'), t('settings.securitySettings.changePassword.currentPasswordIncorrect'));
|
||||
return;
|
||||
}
|
||||
|
||||
await executeVaultPasswordChange(currentPasswordHashBase64, newPassword);
|
||||
|
||||
// Show confirm dialog and go back to the settings screen
|
||||
Alert.alert('Success', 'Password changed successfully', [
|
||||
{ text: 'OK',
|
||||
Alert.alert(t('common.success'), t('settings.securitySettings.changePassword.passwordChangedSuccessfully'), [
|
||||
{ text: t('common.ok'),
|
||||
/**
|
||||
* Reset the password change state and go back to the settings screen
|
||||
*/
|
||||
@@ -104,7 +106,7 @@ export default function ChangePasswordScreen(): React.ReactNode {
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Password change error:', error);
|
||||
Alert.alert('Error', 'Failed to change password. Please try again.');
|
||||
Alert.alert(t('common.error'), t('settings.securitySettings.changePassword.failedToChange'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setLoadingStatus(null);
|
||||
@@ -123,42 +125,42 @@ export default function ChangePasswordScreen(): React.ReactNode {
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
<ThemedText style={styles.headerText}>
|
||||
Changing your master password also changes the vault encryption keys. It is advised to periodically change your master password to keep your vaults secure.
|
||||
{t('settings.securitySettings.changePassword.headerText')}
|
||||
</ThemedText>
|
||||
<UsernameDisplay />
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.label}>Current Password</ThemedText>
|
||||
<ThemedText style={styles.label}>{t('settings.securitySettings.changePassword.currentPassword')}</ThemedText>
|
||||
<ThemedTextInput
|
||||
secureTextEntry
|
||||
value={currentPassword}
|
||||
onChangeText={setCurrentPassword}
|
||||
placeholder="Enter current password"
|
||||
placeholder={t('settings.securitySettings.changePassword.enterCurrentPassword')}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.label}>New Password</ThemedText>
|
||||
<ThemedText style={styles.label}>{t('settings.securitySettings.changePassword.newPassword')}</ThemedText>
|
||||
<ThemedTextInput
|
||||
secureTextEntry
|
||||
value={newPassword}
|
||||
onChangeText={setNewPassword}
|
||||
placeholder="Enter new password"
|
||||
placeholder={t('settings.securitySettings.changePassword.enterNewPassword')}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.label}>Confirm New Password</ThemedText>
|
||||
<ThemedText style={styles.label}>{t('settings.securitySettings.changePassword.confirmNewPassword')}</ThemedText>
|
||||
<ThemedTextInput
|
||||
secureTextEntry
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
placeholder="Confirm new password"
|
||||
placeholder={t('settings.securitySettings.changePassword.confirmNewPassword')}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ThemedButton
|
||||
title="Change Password"
|
||||
title={t('settings.securitySettings.changePassword.changePassword')}
|
||||
onPress={handleSubmit}
|
||||
loading={isLoading}
|
||||
style={styles.button}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { router } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, Alert, KeyboardAvoidingView, Platform } from 'react-native';
|
||||
import srp from 'secure-remote-password/client';
|
||||
|
||||
@@ -24,6 +25,7 @@ export default function DeleteAccountScreen(): React.ReactNode {
|
||||
const colors = useColors();
|
||||
const webApi = useWebApi();
|
||||
const { username, verifyPassword, logout } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [confirmUsername, setConfirmUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
@@ -95,7 +97,7 @@ export default function DeleteAccountScreen(): React.ReactNode {
|
||||
*/
|
||||
const handleUsernameSubmit = (): void => {
|
||||
if (confirmUsername !== username) {
|
||||
Alert.alert('Error', 'Username does not match');
|
||||
Alert.alert(t('common.error'), t('settings.securitySettings.deleteAccount.usernameDoesNotMatch'));
|
||||
return;
|
||||
}
|
||||
setStep('password');
|
||||
@@ -106,17 +108,17 @@ export default function DeleteAccountScreen(): React.ReactNode {
|
||||
*/
|
||||
const handleDeleteAccount = async (): Promise<void> => {
|
||||
if (!password) {
|
||||
Alert.alert('Error', 'Please enter your password');
|
||||
Alert.alert(t('common.error'), t('settings.securitySettings.deleteAccount.enterPassword'));
|
||||
return;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
'Delete Account',
|
||||
'Are you absolutely sure you want to delete your account? This action cannot be undone.',
|
||||
t('settings.securitySettings.deleteAccount.deleteAccount'),
|
||||
t('settings.securitySettings.deleteAccount.confirmationMessage'),
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: t('common.cancel'), style: 'cancel' },
|
||||
{
|
||||
text: 'Delete Account',
|
||||
text: t('settings.securitySettings.deleteAccount.deleteAccount'),
|
||||
style: 'destructive',
|
||||
/**
|
||||
* Handles the delete account press.
|
||||
@@ -133,19 +135,19 @@ export default function DeleteAccountScreen(): React.ReactNode {
|
||||
const handleDeleteAccountPress = async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setLoadingStatus('Verifying password...');
|
||||
setLoadingStatus(t('settings.securitySettings.deleteAccount.verifyingPassword'));
|
||||
const currentPasswordHashBase64 = await verifyPassword(password);
|
||||
if (!currentPasswordHashBase64) {
|
||||
Alert.alert('Error', 'Current password is not correct');
|
||||
Alert.alert(t('common.error'), t('settings.securitySettings.deleteAccount.currentPasswordIncorrect'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingStatus('Initiating account deletion');
|
||||
setLoadingStatus(t('settings.securitySettings.deleteAccount.initiatingDeletion'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
if (!username) {
|
||||
throw new Error('Username not found. Please login again.');
|
||||
throw new Error(t('settings.securitySettings.deleteAccount.usernameNotFound'));
|
||||
}
|
||||
|
||||
const deleteAccountInitiateRequest: DeleteAccountInitiateRequest = {
|
||||
@@ -157,7 +159,7 @@ export default function DeleteAccountScreen(): React.ReactNode {
|
||||
const currentSalt = data.salt;
|
||||
const currentServerEphemeral = data.serverEphemeral;
|
||||
|
||||
setLoadingStatus('Verifying with server');
|
||||
setLoadingStatus(t('settings.securitySettings.deleteAccount.verifyingWithServer'));
|
||||
// Convert base64 string to hex string
|
||||
const currentPasswordHashString = Buffer.from(currentPasswordHashBase64, 'base64').toString('hex').toUpperCase();
|
||||
|
||||
@@ -167,7 +169,7 @@ export default function DeleteAccountScreen(): React.ReactNode {
|
||||
// Get username from the auth context, always lowercase and trimmed which is required for the argon2id key derivation
|
||||
const sanitizedUsername = username?.toLowerCase().trim();
|
||||
if (!sanitizedUsername) {
|
||||
throw new Error('Username not found. Please login again.');
|
||||
throw new Error(t('settings.securitySettings.deleteAccount.usernameNotFound'));
|
||||
}
|
||||
|
||||
const privateKey = srp.derivePrivateKey(currentSalt, sanitizedUsername, currentPasswordHashString);
|
||||
@@ -185,18 +187,18 @@ export default function DeleteAccountScreen(): React.ReactNode {
|
||||
clientSessionProof: newClientSession.proof,
|
||||
};
|
||||
|
||||
setLoadingStatus('Deleting account');
|
||||
setLoadingStatus(t('settings.securitySettings.deleteAccount.deletingAccount'));
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
// Send final delete request with SRP proof.
|
||||
await webApi.post('Auth/delete-account/confirm', deleteAccountRequest);
|
||||
|
||||
// Logout silently and navigate to login screen.
|
||||
await logout('Account deleted successfully');
|
||||
await logout(t('settings.securitySettings.deleteAccount.accountDeleted'));
|
||||
router.replace('/login');
|
||||
} catch (error) {
|
||||
console.error('Error deleting account:', error);
|
||||
Alert.alert('Error', 'Failed to delete account. Please try again.');
|
||||
Alert.alert(t('common.error'), t('settings.securitySettings.deleteAccount.failedToDelete'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setLoadingStatus(null);
|
||||
@@ -229,29 +231,29 @@ export default function DeleteAccountScreen(): React.ReactNode {
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
<ThemedText style={styles.headerText}>
|
||||
Deleting your account will immediately and permanently delete all of your data.
|
||||
{t('settings.securitySettings.deleteAccount.headerText')}
|
||||
</ThemedText>
|
||||
<UsernameDisplay />
|
||||
<View style={styles.form}>
|
||||
{step === 'username' ? (
|
||||
<>
|
||||
<ThemedText style={styles.warningText}>
|
||||
Warning: This action cannot be undone. All your data will be permanently deleted.
|
||||
{t('settings.securitySettings.deleteAccount.warningText')}
|
||||
</ThemedText>
|
||||
<WarningItem text="All encrypted vaults which includes all of your credentials will be permanently deleted" />
|
||||
<WarningItem text="Your email aliases will be orphaned and cannot be claimed by other users" />
|
||||
<WarningItem text="Your account cannot be recovered after deletion" />
|
||||
<WarningItem text={t('settings.securitySettings.deleteAccount.warningVaults')} />
|
||||
<WarningItem text={t('settings.securitySettings.deleteAccount.warningAliases')} />
|
||||
<WarningItem text={t('settings.securitySettings.deleteAccount.warningRecovery')} />
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.label}>Enter your username to continue</ThemedText>
|
||||
<ThemedText style={styles.label}>{t('settings.securitySettings.deleteAccount.enterUsername')}</ThemedText>
|
||||
<ThemedTextInput
|
||||
value={confirmUsername}
|
||||
onChangeText={setConfirmUsername}
|
||||
placeholder="Enter username"
|
||||
placeholder={t('settings.securitySettings.deleteAccount.enterUsername')}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
<ThemedButton
|
||||
title="Continue"
|
||||
title={t('common.continue')}
|
||||
onPress={handleUsernameSubmit}
|
||||
style={styles.button}
|
||||
/>
|
||||
@@ -259,20 +261,20 @@ export default function DeleteAccountScreen(): React.ReactNode {
|
||||
) : (
|
||||
<>
|
||||
<ThemedText style={styles.warningText}>
|
||||
Final warning: Enter your password to permanently delete your account.
|
||||
{t('settings.securitySettings.deleteAccount.finalWarning')}
|
||||
</ThemedText>
|
||||
<WarningItem text="Account deletion is irreversible and cannot be undone. Pressing the button below will delete your account immmediately and permanently." />
|
||||
<WarningItem text={t('settings.securitySettings.deleteAccount.irreversibleWarning')} />
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.label}>Password</ThemedText>
|
||||
<ThemedText style={styles.label}>{t('settings.securitySettings.deleteAccount.password')}</ThemedText>
|
||||
<ThemedTextInput
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
placeholder="Enter password"
|
||||
placeholder={t('settings.securitySettings.deleteAccount.enterPassword')}
|
||||
/>
|
||||
</View>
|
||||
<ThemedButton
|
||||
title="Delete Account"
|
||||
title={t('settings.securitySettings.deleteAccount.deleteAccount')}
|
||||
onPress={handleDeleteAccount}
|
||||
loading={isLoading}
|
||||
style={styles.button}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity } from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -14,6 +15,7 @@ import { SettingsHeader } from '@/components/ui/SettingsHeader';
|
||||
*/
|
||||
export default function SecuritySettingsScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
section: {
|
||||
@@ -57,7 +59,7 @@ export default function SecuritySettingsScreen() : React.ReactNode {
|
||||
return (
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView>
|
||||
<SettingsHeader title="Security" description="Manage your account and vault security settings." icon="shield-checkmark" />
|
||||
<SettingsHeader title={t('settings.securitySettings.title')} description={t('settings.securitySettings.description')} icon="shield-checkmark" />
|
||||
<View style={styles.section}>
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
@@ -67,7 +69,7 @@ export default function SecuritySettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="key" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Change Master Password</ThemedText>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.securitySettings.changeMasterPassword')}</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -81,7 +83,7 @@ export default function SecuritySettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="desktop" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Active Sessions</ThemedText>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.securitySettings.activeSessionsTitle')}</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -95,7 +97,7 @@ export default function SecuritySettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="list" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Recent Auth Logs</ThemedText>
|
||||
<ThemedText style={styles.settingItemText}>{t('settings.securitySettings.recentAuthLogs')}</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -109,7 +111,7 @@ export default function SecuritySettingsScreen() : React.ReactNode {
|
||||
<Ionicons name="trash" size={20} color={colors.primary} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={[styles.settingItemText, { color: colors.primary }]}>Delete Account</ThemedText>
|
||||
<ThemedText style={[styles.settingItemText, { color: colors.primary }]}>{t('settings.securitySettings.deleteAccountTitle')}</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user