1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-11 05:43:30 +00:00

Compare commits

..

123 Commits

Author SHA1 Message Date
André Bispo
fdc05d3cdf Merge branch 'feature/pm-1208-f3-options' into auth/pm-2293-tde-auth-requests
# Conflicts:
#	src/App/Pages/Accounts/LoginPasswordlessRequestViewModel.cs
2023-07-28 00:04:38 +01:00
André Bispo
c25906206e [PM-2289] [PM-2293] TDE Login with device Admin Request (#2642) 2023-07-27 21:18:36 +01:00
André Bispo
148b2ad048 [PM-2293] PR fixes 2023-07-27 18:22:07 +01:00
André Bispo
6edac4f296 [PM-2408] Pending admin approval requests (#2643)
* [PM-2408] Save PendingAdminAuthRequest to state

* [PM-2408] Add logic to check pending auth requests before creating a new one.

* [PM-2408] Remove active user params

* [PM-2408] Fix bug where TDE MP unlock wasn't syncing and vault appeared empty

* [PM-2408] Add reconciledOptions

* [PM-2408] Set user key when login using master password

* [PM-2408] Generate Fingerprint phrase and PubKey locally to avoid MITM attacks

* [PM-2408] inheritdoc

* [PM-1280] TDE 2FA - approval options navigation (#2646)

* [PM-1208] Add navigation to device approval options after 2FA auth when TDE is enabled

* [PM-1208] Add navigations to iOS extensions

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2023-07-27 18:17:04 +01:00
André Bispo
5fda15bbc7 [PM-2293] PR fixes 2023-07-27 17:13:37 +01:00
André Bispo
d3023c101b Merge branch 'feature/pm-1208-f3-options' into auth/pm-2293-tde-auth-requests
# Conflicts:
#	src/Core/Services/AuthService.cs
2023-07-27 17:08:46 +01:00
André Bispo
dfc7c55b77 [PM-2297] Login with trusted device (Flow 2) (#2623)
* [PM-2297] Add DecryptUserKeyWithDeviceKey method

* [PM-2297] Add methods to DeviceTrustCryptoService update decryption options model

* [PM-2297] Update account decryption options model

* [PM-2297] Fix TrustedDeviceOption and DeviceResponse model. Change StateService device key get set to have default user id

* [PM-2297] Update navigation to decryption options

* [PM-2297] Add missing action navigations to iOS extensions

* [PM-2297] Fix trust device bug/typo

* [PM-2297] Fix model bug

* [PM-2297] Fix state var crash

* [PM-2297] Add trust device login logic to auth service

* [PM-2297] Refactor auth service key connector code

* [PM-2297] Remove reconciledOptions for deviceKey in state service

* [PM-2297] Remove unnecessary user id params
2023-07-27 16:55:06 +01:00
André Bispo
7901931899 Merge branch 'feature/pm-2297-f2-trusted' into auth/pm-2293-tde-auth-requests 2023-07-27 12:27:40 +01:00
André Bispo
8ff79d0e86 [PM-2297] PR fixes 2023-07-27 12:27:08 +01:00
André Bispo
a938a3f0fc [PM-2293] PR fixes 2023-07-27 12:18:01 +01:00
André Bispo
0f776074c8 [PM-2293] code format 2023-07-27 12:14:26 +01:00
André Bispo
2208ae67fb [PM-2293] Check for user key only 2023-07-27 10:14:09 +01:00
André Bispo
9b7dc7210f [PM-2297] remove uncessary user id params 2023-07-25 21:46:37 +01:00
André Bispo
28addfeb43 [PM-2293] Remove userid params 2023-07-25 17:39:57 +01:00
André Bispo
13a46fae5a Merge branch 'feature/pm-2297-f2-trusted' into auth/pm-2293-tde-auth-requests 2023-07-25 09:44:02 +01:00
André Bispo
52aabe8237 [PM-2297] Remove reconciledOptions for deviceKey in state service 2023-07-24 22:52:25 +01:00
André Bispo
eceee581c9 [PM-2297] Refactor auth service key connector code 2023-07-24 22:23:17 +01:00
André Bispo
15be0f8c2e [PM-2293] Fix admin approval auth call 2023-07-24 13:37:43 +01:00
André Bispo
01ed99e4b1 [PM-2293] Fix login with device normal flow and SSO 2023-07-24 13:07:06 +01:00
André Bispo
e2841e8d89 [PM-2297] remove unnecessary using 2023-07-24 10:47:37 +01:00
André Bispo
9b02879782 [PM-2293] Change endpoint based on request type 2023-07-24 10:46:59 +01:00
André Bispo
dd2cd0d9df [PM-2297] code format 2023-07-24 09:39:42 +01:00
André Bispo
661ff3da5c [PM-2297] Add trust device login logic to auth service 2023-07-24 09:29:44 +01:00
André Bispo
cdacbafae4 [PM-2297] Fix state var crash 2023-07-24 09:29:06 +01:00
André Bispo
acf7d9d9c4 [PM-2297] Fix model bug 2023-07-24 09:28:41 +01:00
André Bispo
f12cc69b60 [PM-2297] Fix trust device bug/typo 2023-07-24 09:28:10 +01:00
André Bispo
2465a28667 Merge branch 'feature/pm-1208-f3-options' into feature/pm-2297-f2-trusted 2023-07-20 15:40:05 +01:00
André Bispo
080aabfe82 [PM-1208] Fix merge 2023-07-20 15:39:03 +01:00
André Bispo
b7a8e0ab07 Merge branch 'feature/pm-1208-f3-options' into feature/pm-2297-f2-trusted 2023-07-20 15:37:24 +01:00
André Bispo
c0688c584e [PM-1208] Fix merge 2023-07-20 15:37:08 +01:00
André Bispo
c09672ff88 [PM-1208] Fix app resource file 2023-07-20 15:30:29 +01:00
André Bispo
80eec8d81e Merge branch 'feature/pm-1208-f3-options' into feature/pm-2297-f2-trusted 2023-07-20 14:56:56 +01:00
André Bispo
635b6bc184 Merge branch 'feature/pm-1029-tde-login' into feature/pm-1208-f3-options
# Conflicts:
#	src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs
#	src/App/Resources/AppResources.resx
#	src/Core/Abstractions/IStateService.cs
2023-07-20 14:56:40 +01:00
Jacob Fink
da7a1964ef Update crypto service api call to fix build 2023-07-20 09:28:56 -04:00
Jacob Fink
73b8d8e6b8 fix bad merge 2023-07-20 09:20:31 -04:00
Jacob Fink
c61f9f0357 Merge branch 'auth/pm-2713/drop-master-key-dependency' into feature/pm-1029-tde-login 2023-07-20 09:19:48 -04:00
Jacob Fink
a3183857b9 [PM-2713] set decrypt and set user key in login helper 2023-07-19 21:23:21 -04:00
Jacob Fink
bedbca841d [PM-2713] remove unused cached values in crypto service 2023-07-19 21:23:21 -04:00
Jacob Fink
0ff314f076 [PM-2713] use new crypto service api in auth service 2023-07-19 21:23:21 -04:00
Jacob Fink
c9a7c29190 [PM-2713] More conversions to crypto api 2023-07-19 21:23:20 -04:00
Jacob Fink
546bf8dcb1 [PM-2713] convert cipher service and others to crypto service api 2023-07-19 21:23:20 -04:00
Jacob Fink
7fdc5597fc [PM-2713] more conversions to new crypto service api 2023-07-19 21:23:20 -04:00
Jacob Fink
7c664f58b3 [PM-2713] add migration for pin on lock screens 2023-07-19 21:23:20 -04:00
Jacob Fink
bdfe806846 [PM-2713] converting calls to new crypto service api 2023-07-19 21:23:19 -04:00
Jacob Fink
5ed567ab90 [PM-2713] add toggle method to crypto service for keys 2023-07-19 21:23:19 -04:00
Jacob Fink
cd4f44e6f6 [PM-2713] use new MakeMasterKey method 2023-07-19 21:23:19 -04:00
Jacob Fink
d58f0b281b [PM-2713] refresh pin key when setting user key 2023-07-19 21:23:19 -04:00
Jacob Fink
5ba3fac0c0 [PM-2713] add make user key method to crypto service 2023-07-19 21:23:19 -04:00
Jacob Fink
1e30524985 [PM-2713] fix signature of GetUserKeyPin 2023-07-19 21:23:18 -04:00
Jacob Fink
515decb4c9 [PM-2713] add new pin methods to state service 2023-07-19 21:23:18 -04:00
Jacob Fink
bf28d373e9 [PM-2713] more updates to crypto service 2023-07-19 21:23:18 -04:00
Jacob Fink
69d38d4d75 [PM-2713] continue organizing crypto service 2023-07-19 21:23:18 -04:00
Jacob Fink
c1619536aa [PM-2713] rename key hash to password hash & begin add methods to crypto service 2023-07-19 21:23:17 -04:00
Jacob Fink
079e02e4e5 [PM-271] add UserKey and MasterKey support to crypto service 2023-07-19 21:23:17 -04:00
Jacob Fink
15d3da607b [PM-2713] add new state for new keys and obsolete old ones
- UserKey
- MasterKey
- UserKeyMasterKey (enc UserKey from User Table)
2023-07-19 21:23:17 -04:00
Jacob Fink
b5cf9fd79d [PM-2731] add user key and master key types 2023-07-19 21:23:17 -04:00
André Bispo
848e620647 [PM-2297] Add missing action navigations to iOS extensions 2023-07-19 11:28:47 +01:00
André Bispo
4e886de103 [PM-2297] update navigation to decryption options 2023-07-19 10:46:04 +01:00
André Bispo
0e450904c5 [PM-2297] fix TrustedDeviceOption and DeviceResponse model. Change StateService device key get set to have default user id 2023-07-18 18:01:42 +01:00
Federico Maccaroni
dd52ff0dcc [PM-2320] Improve Android block Auto-fill URIs (#2616)
* PM-2320 Added new view for block autofill URIs on Android

* PM-2320 Fix formatting

* PM-2320 Improved validations on block autofill uris

* PM-2320 Improved autofill block uris placeholder colors on different themes
2023-07-18 11:25:38 -03:00
github-actions[bot]
c678c17ebc Bumped version to 2023.7.1 (#2625)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-07-18 14:23:08 +00:00
André Bispo
5e1229f943 [PM-2297] Update account decryption options model 2023-07-18 11:34:39 +01:00
Federico Maccaroni
cd9e49b13b ac-1425 added main thread invocations when updating the vault properties to fix cases where the screen stays blank and doesn't update (#2604) 2023-07-17 16:53:30 -03:00
Federico Maccaroni
6d7970f767 [AC-762] Configure Crowdin to localize watch app (#2552)
* AC-762 Added localization files to watch project

* AC-762 Added crowdin config for watchOS localizable files
2023-07-17 13:35:54 -03:00
André Bispo
9b6b2c005e [PM-2297] Add methods to DeviceTrustCryptoService update decryption options model 2023-07-17 15:30:15 +01:00
mpbw2
9adc4d3080 Catch additional exception types when validating intents (#2618) 2023-07-17 08:40:35 -04:00
ifernandezdiaz
1f20f70d13 Fixing show value id button (#2620) 2023-07-16 20:13:55 -03:00
Vince Grassia
a25da68437 Fix syntax in Version Auto Bump workflow (#2615) 2023-07-13 11:55:16 -04:00
André Bispo
e9ac1e3b5b [PM-2297] Add DecryptUserKeyWithDeviceKey method 2023-07-13 15:41:08 +01:00
Vince Grassia
fdc0313d10 Fix Build Workflow (#2613) 2023-07-13 10:05:57 -04:00
André Bispo
b688b85d0f [PM-1201] Change timeout actions available based on hasMasterPassword (#2610)
* [PM-1201] Change timeout actions available based on hasMasterPassword
2023-07-12 20:42:21 +01:00
André Bispo
a5df6c0c65 [PM-2287][PM-2289][PM-2293] Approval Options (#2608)
* [PM-2293] Add AuthRequestType to PasswordlessLoginPage.

* [PM-2293] Add Actions to ApproveWithDevicePage

* [PM-2293] Change screen text based on AuthRequestType

* [PM-2293] Refactor AuthRequestType enum. Add label. Remove unnecessary actions.

* [PM-2293] Change boolean variable expression.

* [PM-2293] Trust device after admin request login.

* code format

* [PM-2287] Add trust device to master password unlock. Change trust device method. Remove email from SSO login page.

* [PM-2293] Fix state variable get set.

* [PM-2287][PM-2289][PM-2293] Rename method
2023-07-12 19:12:57 +01:00
github-actions[bot]
f31c87b52e Bumped version to 2023.7.0 (#2612)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-07-12 17:34:23 +00:00
André Bispo
c2d4fa4429 [PM-2583] Answer auth request with mp field as null if doesn't have it. (#2609) 2023-07-11 23:05:35 +01:00
Cesar Gonzalez
1e79e1182f [PM-1063] Re-prompt for Master Password Can be Bypassed When Using Gboard Inline Autofill (#2593) 2023-07-11 08:15:37 -05:00
André Bispo
548bd12a8e Merge branch 'feature/pm-1029-tde-login' into feature/pm-1208-f3-options 2023-07-10 12:37:13 +01:00
André Bispo
58542fd255 Merge branch 'master' into feature/pm-1029-tde-login 2023-07-10 12:36:59 +01:00
André Bispo
800b4c71de Merge branch 'feature/pm-1029-tde-login' into feature/pm-1208-f3-options
# Conflicts:
#	src/Core/Models/Response/DeviceResponse.cs
#	src/Core/Services/ApiService.cs
2023-07-10 12:32:27 +01:00
github-actions[bot]
11947ce99a Autosync the updated translations (#2603)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-07-07 05:13:18 +00:00
Jake Fink
3053eaa036 [PM-1379] add DeviceTrustCryptoService with establish trust logic (#2535)
* [PM-1379] add DeviceCryptoService with establish trust logic

* PM-1379 update api location and other minor refactors

* pm-1379 fix encoding

* update trusted device keys api call to Put

* [PM-1379] rename DeviceCryptoService to DeviceTrustCryptoService
- refactors to prevent side effects

* [PM-1379] rearrange methods in DeviceTrustCryptoService

* [PM-1379] rearrange methods in abstraction

* [PM-1379] deconstruct tuples

* [PM-1379] remove extra tasks
2023-07-05 16:13:20 -04:00
mpbw2
4abb472998 Revert "reset lock delay when returning from activity result (#2539)" (#2597)
This reverts commit 0288a6659c.
2023-07-03 09:56:10 -04:00
André Bispo
6268f0776b Merge branch 'feature/pm-1029-tde-login' into feature/pm-1208-f3-options 2023-07-03 10:34:53 +01:00
André Bispo
cbbc41be67 [PM-1208] Add continue button and not you option 2023-07-03 10:34:02 +01:00
github-actions[bot]
1d541e5b8e Autosync the updated translations (#2595)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-06-30 07:06:12 +00:00
ifernandezdiaz
175b9936b6 [PM-2798] Fixing Toolbar locators in FolderAddEditPage (#2592)
* Fixing Toolbar locators

* Adding Matt's suggestions
2023-06-29 15:50:01 -03:00
ifernandezdiaz
72e67bd6f2 [PM-2691] Adding AutomationIDs for Vault Page sections (#2580)
* Adding IDs for Vault Page sections

* Removing extra spaces

* Adding Matt's comments

* Fixing Filters Id bug

* Adding Fede's suggestions

* Fixing Settings Ids issues

* Fixing AutomationIds issues with RecyclerViews + implementing AutomationId helper class

* Adding Fede's suggestion

* Adding latest Fede's suggestions
2023-06-29 15:37:08 -03:00
André Bispo
e164fb9823 Merge branch 'feature/pm-1029-tde-login' into feature/pm-1208-f3-options
# Conflicts:
#	src/App/Resources/AppResources.resx
#	src/Core/Abstractions/IApiService.cs
#	src/Core/Services/StateService.cs
2023-06-29 14:36:46 +01:00
André Bispo
87866304a6 [PM-1208] Add device related api endpoint. Add AccoundDecryptOptions model and property to user Account. 2023-06-28 22:37:08 +01:00
ifernandezdiaz
216c6abcf6 [PM-2737] Adding AutomationIDs for Send page elements (#2583)
* Adding AutomationIDs for Send page elements

* Fixing some spaces

* Adding Matt's suggestion

* Adding Fede's suggestion

* Removing unnecesarry breaks
2023-06-28 14:07:03 -04:00
Federico Maccaroni
1014563c75 [PM-192] Refactor forwarded email providers (#2579)
* PM-192 Refactor Forwarded email providers to use better patterns and code reuse.

* PM-192 fix format
2023-06-27 18:49:38 -03:00
ifernandezdiaz
3506269811 [PM-2688] Adding IDs for Options and Folders pages (#2585)
* Adding IDs for Options and Folders pages

* Fixing extra spaces
2023-06-26 10:31:57 -03:00
ifernandezdiaz
31487a31bb [PM-2748] Refactoring locator strategy for Cipher Details page (#2586)
* Refactoring locator strategy for Cipher Details page

* Fixing extra spaces
2023-06-26 10:30:13 -03:00
ifernandezdiaz
1407aa5655 [PM-2678] Adding IDs for Settings Page elements (#2584)
* Adding IDS for Settings elements

* Adding IDS for Settings elements
2023-06-23 13:31:24 -03:00
github-actions[bot]
16f59e2698 Autosync the updated translations (#2582)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-06-23 05:40:37 +00:00
ifernandezdiaz
d876b54f45 Adding IDs for AttachmentPage elements (#2577) 2023-06-20 17:49:26 -03:00
ifernandezdiaz
6644e3b449 [PM-2545] Adding Automation IDs for CipherDetailsPage elements (#2576)
* Adding IDs to CipherDetailsPage

* Fixing extra spaces

* Fixing extra space
2023-06-20 16:26:43 -03:00
ifernandezdiaz
8d98d1d5bd [PM-2612] Adding AutomationIDs for LoginPasswordlessPage elements (#2574)
* Adding AutomationIDs for LoginPasswordlessPag elements

* Adding AutomationIDs for LoginPasswordlessRequest page elements

* Fixing missing space
2023-06-20 15:12:15 -03:00
ifernandezdiaz
3e9711f8f2 [PM-2611] Adding IDs for Cipher/Send search results (#2575)
* Adding IDs for Cipher/Send search results

* Adding missing spaces
2023-06-20 11:28:54 -03:00
github-actions[bot]
3af37f01d3 Autosync the updated translations (#2570)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-06-19 09:05:12 +00:00
ifernandezdiaz
43d2d386b1 [PM-2645] Adding IDs for Account Switching elements (#2572)
* Adding IDs for Account Switching elements

* Fixing Active/Inactive vault icon IDs
2023-06-16 16:20:09 -03:00
ifernandezdiaz
bc5c11b47f Adding AutomationIDs on Generator page elements (#2569)
* Adding AutomationIDs on Generator pages

* Adding missing spaces
2023-06-15 16:11:55 -03:00
ifernandezdiaz
52843b4181 [PM-2544] Adding AutomationIDs for CipherAddEditViewPage elements (#2564)
* Adding AutomationIDs for Add/Edit Items page

* Adding IDs to CustomFields

* Adding Matt's suggestions

* Adding newest suggestions
2023-06-14 09:34:38 -03:00
Federico Maccaroni
98705e443f PM-2575 Fixed extension freeze when using the return button on the keyboard when unlocking the extension. Also added way to prevent multiple executions of checking the password and logging exceptions. (#2568) 2023-06-13 22:38:08 +02:00
mpbw2
1332ef7b43 Enhancement to login field detection for Android autofill (#2561) 2023-06-13 13:54:28 -04:00
mpbw2
04e30c2146 Update F-Droid listing author name (#2501)
Update F-Droid listing author name to `Bitwarden Inc`
2023-06-13 18:46:41 +02:00
Opeyemi
f604da13a1 add more comment to missing actions (#2567) 2023-06-13 15:57:02 +01:00
github-actions[bot]
dcf9acb51c Autosync the updated translations (#2562)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-06-09 09:19:34 +02:00
Federico Maccaroni
3b087c50ae PM-1076 added warning on unlocking iOS extensions when the kdf type is argon2id and the memory is higher than 48MB, to let the user know that unlocking might crash the extension (#2560) 2023-06-07 16:21:51 +02:00
ifernandezdiaz
1c13ed9895 [PS-2558] Mobile Automation - Starting automationIDs additions to our codebase (#2558)
* Adding locators for Environment, Hope, Login and Register pages

* Adding Locators on LockPage

* Adding Álison's suggestions
2023-06-06 21:00:01 -03:00
Federico Maccaroni
eeb634e698 PM-1798 Added accessibility names on entries on cipher add (#2550) 2023-06-05 18:58:38 +02:00
github-actions[bot]
8bc2df6c8a Autosync the updated translations (#2555)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-06-04 16:23:34 +02:00
github-actions[bot]
7cd40d4d89 Bumped version to 2023.5.1 (#2554)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-06-01 12:18:12 -04:00
Federico Maccaroni
bebf23785d PM-2232 Fix api response not being read as string because the content was not being considered json when it was indeed. Now Netacea messages are shown on the UI. (#2541) 2023-06-01 10:35:35 +03:00
github-actions[bot]
e78833cbcb Bumped version to 2023.5.0 (#2553)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-05-31 09:33:47 -04:00
github-actions[bot]
b7ff636862 Autosync the updated translations (#2540)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-05-26 08:12:16 +02:00
mpbw2
0288a6659c reset lock delay when returning from activity result (#2539) 2023-05-25 11:43:45 -04:00
André Bispo
c7fd113f26 [PM-2347] Refresh feature flags when environment urls change (#2538) 2023-05-25 14:37:53 +01:00
Michał Chęciński
79241731e7 Add github actions to renovate (#2536)
* Add github actions to renovate

* Add gh actions manager

* Apply whole renovate config

* Add newline
2023-05-24 16:04:39 +02:00
github-actions[bot]
74e9914f5b Autosync the updated translations (#2531)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-05-22 11:35:15 +02:00
André Bispo
65307f6eab [PM-1351][PM-190] Add a mobile service to retrieve feature flags from API (#2431) 2023-05-19 12:42:41 +01:00
github-actions[bot]
e9f83aee90 Autosync the updated translations (#2524)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-05-19 08:28:02 +02:00
André Bispo
84a82f0876 [PM-1208] Add Device approval options screen. View model waiting for additional logic to be added. 2023-05-17 17:46:45 +01:00
Federico Maccaroni
fdaf743868 PM-2249 Fix vault timeout action policy check (#2521) 2023-05-15 15:28:18 +02:00
286 changed files with 8800 additions and 3171 deletions

39
.github/renovate.json vendored
View File

@@ -2,21 +2,36 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": [
"config:base", "config:base",
"schedule:monthly", ":combinePatchMinorReleases",
":maintainLockFilesMonthly", ":dependencyDashboard",
":preserveSemverRanges", ":maintainLockFilesWeekly",
":pinAllExceptPeerDependencies",
":prConcurrentLimit10",
":rebaseStalePrs", ":rebaseStalePrs",
":disableDependencyDashboard" "schedule:weekends",
], ":separateMajorReleases"
"enabledManagers": [
"nuget"
], ],
"enabledManagers": ["cargo", "github-actions", "npm", "nuget"],
"packageRules": [ "packageRules": [
{ {
"groupName": "cargo minor",
"matchManagers": ["cargo"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"groupName": "gh minor",
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"groupName": "npm minor",
"matchManagers": ["npm"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"groupName": "nuget minor",
"matchManagers": ["nuget"], "matchManagers": ["nuget"],
"groupName": "Nuget updates", "matchUpdateTypes": ["minor", "patch"]
"groupSlug": "nuget", },
"separateMajorMinor": false
}
] ]
} }

View File

@@ -71,6 +71,11 @@ jobs:
with: with:
nuget-version: 5.9.0 nuget-version: 5.9.0
- name: Set up .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '3.1.x'
- name: Set up MSBuild - name: Set up MSBuild
uses: microsoft/setup-msbuild@1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c # v1.3.1 uses: microsoft/setup-msbuild@1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c # v1.3.1
@@ -793,7 +798,7 @@ jobs:
done done
- name: Upload Sources - name: Upload Sources
uses: crowdin/github-action@ecd7eb0ef6f3cfa16293c79e9cbc4bc5b5fd9c49 # v1.4.9 uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -30,7 +30,7 @@ jobs:
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase" secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Download translations - name: Download translations
uses: crowdin/github-action@ecd7eb0ef6f3cfa16293c79e9cbc4bc5b5fd9c49 # v1.4.9 uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Enforce Label - name: Enforce Label
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # v2.2.2 uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
with: with:
BANNED_LABELS: "hold,needs-qa" BANNED_LABELS: "hold,needs-qa"
BANNED_LABELS_DESCRIPTION: "PRs with the hold or needs-qa labels cannot be merged" BANNED_LABELS_DESCRIPTION: "PRs with the hold or needs-qa labels cannot be merged"

View File

@@ -1,23 +0,0 @@
# Runs on creation of Pull Requests
# If the PR destination branch is master, add a needs-qa label
---
name: Label Issue Pull Request
on:
pull_request:
types:
- opened # Check when PR is opened
paths-ignore:
- .github/workflows/** # We don't need QA on workflow changes
branches:
- 'master' # We only want to check when PRs target master
jobs:
add-needs-qa-label:
runs-on: ubuntu-latest
steps:
- name: Add label to pull request
uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 # 1.0.4
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
add-labels: "needs-qa"

View File

@@ -68,7 +68,7 @@ jobs:
- name: Download all artifacts - name: Download all artifacts
if: ${{ github.event.inputs.release_type != 'Dry Run' }} if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10 uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
@@ -76,7 +76,7 @@ jobs:
- name: Dry Run - Download all artifacts - name: Dry Run - Download all artifacts
if: ${{ github.event.inputs.release_type == 'Dry Run' }} if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10 uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
@@ -130,7 +130,7 @@ jobs:
- name: Download F-Droid .apk artifact - name: Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type != 'Dry Run' }} if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10 uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
@@ -139,7 +139,7 @@ jobs:
- name: Dry Run - Download F-Droid .apk artifact - name: Dry Run - Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type == 'Dry Run' }} if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10 uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success

View File

@@ -32,12 +32,8 @@ jobs:
echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT
trigger_version_bump: trigger_version_bump:
name: "Version bump" name: Bump version to ${{ needs.setup.outputs.version_number }}
runs-on: ubuntu-22.04 needs: setup
needs:
- setup
steps:
- name: Bump version to ${{ needs.setup.outputs.version_number }}
uses: ./.github/workflows/version-bump.yml uses: ./.github/workflows/version-bump.yml
secrets: secrets:
AZURE_PROD_KV_CREDENTIALS: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} AZURE_PROD_KV_CREDENTIALS: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

View File

@@ -38,3 +38,15 @@ files:
pt-PT: pt-PT pt-PT: pt-PT
en-GB: en-GB en-GB: en-GB
en-IN: en-IN en-IN: en-IN
- source: "/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/Localizable.strings"
dest: "/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/%original_file_name%"
translation: "/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization//%two_letters_code%.lproj/%original_file_name%"
update_option: update_as_unapproved
languages_mapping:
two_letters_code:
zh-CN: zh-Hans
zh-TW: zh-Hant
pt-BR: pt-BR
pt-PT: pt-PT
en-GB: en-GB
en-IN: en-IN

View File

@@ -159,6 +159,7 @@
<Compile Include="Constants.cs" /> <Compile Include="Constants.cs" />
<Compile Include="Effects\RemoveFontPaddingEffect.cs" /> <Compile Include="Effects\RemoveFontPaddingEffect.cs" />
<Compile Include="Services\WatchDeviceService.cs" /> <Compile Include="Services\WatchDeviceService.cs" />
<Compile Include="Renderers\CustomLabelRenderer.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidAsset Include="Assets\bwi-font.ttf" /> <AndroidAsset Include="Assets\bwi-font.ttf" />
@@ -232,6 +233,18 @@
<SubType></SubType> <SubType></SubType>
<Generator></Generator> <Generator></Generator>
</AndroidResource> </AndroidResource>
<AndroidResource Include="Resources\layout\validatable_input_dialog_layout.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\empty_uris_placeholder.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\empty_uris_placeholder_dark.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidResource Include="Resources\drawable\splash_screen.xml" /> <AndroidResource Include="Resources\drawable\splash_screen.xml" />

View File

@@ -12,7 +12,7 @@ namespace Bit.Droid.Autofill
private List<Field> _passwordFields = null; private List<Field> _passwordFields = null;
private List<Field> _usernameFields = null; private List<Field> _usernameFields = null;
private HashSet<string> _ignoreSearchTerms = new HashSet<string> { "search", "find", "recipient", "edit" }; private HashSet<string> _ignoreSearchTerms = new HashSet<string> { "search", "find", "recipient", "edit" };
private HashSet<string> _usernameTerms = new HashSet<string> { "email", "phone", "username"}; private HashSet<string> _usernameTerms = new HashSet<string> { "email", "phone", "username" };
private HashSet<string> _passwordTerms = new HashSet<string> { "password", "pswd" }; private HashSet<string> _passwordTerms = new HashSet<string> { "password", "pswd" };
public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>(); public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>();
@@ -54,16 +54,15 @@ namespace Bit.Droid.Autofill
if (HintToFieldsMap.ContainsKey(View.AutofillHintPassword)) if (HintToFieldsMap.ContainsKey(View.AutofillHintPassword))
{ {
_passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]); _passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]);
return _passwordFields;
} }
} }
else
{
_passwordFields = Fields.Where(f => FieldIsPassword(f)).ToList(); _passwordFields = Fields.Where(f => FieldIsPassword(f)).ToList();
if (!_passwordFields.Any()) if (!_passwordFields.Any())
{ {
_passwordFields = Fields.Where(f => FieldHasPasswordTerms(f)).ToList(); _passwordFields = Fields.Where(f => FieldHasPasswordTerms(f)).ToList();
} }
}
return _passwordFields; return _passwordFields;
} }
} }
@@ -87,9 +86,12 @@ namespace Bit.Droid.Autofill
{ {
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]); _usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]);
} }
} if (_usernameFields.Any())
else
{ {
return _usernameFields;
}
}
foreach (var passwordField in PasswordFields) foreach (var passwordField in PasswordFields)
{ {
var usernameField = Fields.TakeWhile(f => f.AutofillId != passwordField.AutofillId) var usernameField = Fields.TakeWhile(f => f.AutofillId != passwordField.AutofillId)
@@ -104,7 +106,6 @@ namespace Bit.Droid.Autofill
{ {
_usernameFields = Fields.Where(f => FieldIsUsername(f)).ToList(); _usernameFields = Fields.Where(f => FieldIsUsername(f)).ToList();
} }
}
return _usernameFields; return _usernameFields;
} }
} }

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.4.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.7.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />

View File

@@ -0,0 +1,43 @@
using System.ComponentModel;
using Android.Content;
using Android.OS;
using Bit.App.Controls;
using Bit.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(CustomLabel), typeof(CustomLabelRenderer))]
namespace Bit.Droid.Renderers
{
public class CustomLabelRenderer : LabelRenderer
{
public CustomLabelRenderer(Context context)
: base(context)
{ }
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (Control != null && e.NewElement is CustomLabel label)
{
if (label.FontWeight.HasValue && Build.VERSION.SdkInt >= BuildVersionCodes.P)
{
Control.Typeface = Android.Graphics.Typeface.Create(null, label.FontWeight.Value, false);
}
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var label = sender as CustomLabel;
switch (e.PropertyName)
{
case nameof(CustomLabel.AutomationId):
Control.ContentDescription = label.AutomationId;
break;
}
base.OnElementPropertyChanged(sender, e);
}
}
}

View File

@@ -0,0 +1,35 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="129"
android:viewportHeight="124"
android:width="129dp"
android:height="124dp">
<path
android:pathData="M126.8227 61.9441A59.6843 59.6843 0 0 1 7.4541 61.9441A59.6843 59.6843 0 0 1 126.8227 61.9441Z"
android:fillColor="#F0F0F0"
android:strokeColor="#89929F"
android:strokeWidth="3" />
<path
android:pathData="M21.6167 100.851C52.597 103.31 79.6937 80.3264 82.1391 49.5156C83.6205 30.8497 76.0789 14.8844 62.7275 3.63385"
android:strokeColor="#89929F"
android:strokeWidth="1.5"
android:strokeLineCap="round" />
<path
android:pathData="M14.5633 34.2845C12.2035 66.7711 38.5225 96.3429 72.6666 98.8232C74.2596 98.9389 78.629 98.9975 80.1951 99C84.6245 98.8232 97.8063 96.593 106.813 91.8485C113.439 88.3581 119.745 84.6984 124.644 79.1121"
android:strokeColor="#89929F"
android:strokeWidth="1.5"
android:strokeLineCap="round" />
<path
android:pathData="M124.502 48.5051C106.554 24.3817 68.8237 21.6709 41.4178 42.0617C24.8146 54.4149 14.7327 72.4183 13.9255 90.1427"
android:strokeColor="#89929F"
android:strokeWidth="1.5"
android:strokeLineCap="round" />
<path
android:pathData="M83.4034 28.3934A5 5 0 0 1 73.4034 28.3934A5 5 0 0 1 83.4034 28.3934Z"
android:fillColor="#89929F" />
<path
android:pathData="M24.7698 66.5518A5 5 0 0 1 14.7698 66.5518A5 5 0 0 1 24.7698 66.5518Z"
android:fillColor="#89929F" />
<path
android:pathData="M57.344 94.4726A5 5 0 0 1 47.344 94.4726A5 5 0 0 1 57.344 94.4726Z"
android:fillColor="#89929F" />
</vector>

View File

@@ -0,0 +1,35 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="129"
android:viewportHeight="124"
android:width="129dp"
android:height="124dp">
<path
android:pathData="M126.8227 61.9441A59.6843 59.6843 0 0 1 7.4541 61.9441A59.6843 59.6843 0 0 1 126.8227 61.9441Z"
android:fillColor="@android:color/transparent"
android:strokeColor="#A3A3A3"
android:strokeWidth="3" />
<path
android:pathData="M21.6167 100.851C52.597 103.31 79.6937 80.3264 82.1391 49.5156C83.6205 30.8497 76.0789 14.8844 62.7275 3.63385"
android:strokeColor="#A3A3A3"
android:strokeWidth="1.5"
android:strokeLineCap="round" />
<path
android:pathData="M14.5633 34.2845C12.2035 66.7711 38.5225 96.3429 72.6666 98.8232C74.2596 98.9389 78.629 98.9975 80.1951 99C84.6245 98.8232 97.8063 96.593 106.813 91.8485C113.439 88.3581 119.745 84.6984 124.644 79.1121"
android:strokeColor="#A3A3A3"
android:strokeWidth="1.5"
android:strokeLineCap="round" />
<path
android:pathData="M124.502 48.5051C106.554 24.3817 68.8237 21.6709 41.4178 42.0617C24.8146 54.4149 14.7327 72.4183 13.9255 90.1427"
android:strokeColor="#A3A3A3"
android:strokeWidth="1.5"
android:strokeLineCap="round" />
<path
android:pathData="M83.4034 28.3934A5 5 0 0 1 73.4034 28.3934A5 5 0 0 1 83.4034 28.3934Z"
android:fillColor="#A3A3A3" />
<path
android:pathData="M24.7698 66.5518A5 5 0 0 1 14.7698 66.5518A5 5 0 0 1 24.7698 66.5518Z"
android:fillColor="#A3A3A3" />
<path
android:pathData="M57.344 94.4726A5 5 0 0 1 47.344 94.4726A5 5 0 0 1 57.344 94.4726Z"
android:fillColor="#A3A3A3" />
</vector>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="30dp"
android:paddingRight="30dp">
<TextView
android:id="@+id/lblHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/dialog_header_text_size"
android:layout_marginTop="5dp"
android:layout_marginBottom="-3dp"
android:labelFor="@+id/txtValue"/>
<EditText
android:id="@id/txtValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/dialog_input_text_size"/>
<TextView
android:id="@+id/lblValueSubinfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/dialog_sub_value_info_text_size"/>
</LinearLayout>

View File

@@ -2,4 +2,7 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<dimen name="design_bottom_navigation_text_size" tools:override="true">15sp</dimen> <dimen name="design_bottom_navigation_text_size" tools:override="true">15sp</dimen>
<dimen name="design_bottom_navigation_active_text_size" tools:override="true">15sp</dimen> <dimen name="design_bottom_navigation_active_text_size" tools:override="true">15sp</dimen>
<dimen name="dialog_input_text_size">16sp</dimen>
<dimen name="dialog_header_text_size">12sp</dimen>
<dimen name="dialog_sub_value_info_text_size">12sp</dimen>
</resources> </resources>

View File

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.Media;
using Android.Nfc; using Android.Nfc;
using Android.OS; using Android.OS;
using Android.Provider; using Android.Provider;
@@ -13,11 +14,17 @@ using Android.Views.InputMethods;
using Android.Widget; using Android.Widget;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.App.Utilities.Prompts;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Droid.Utilities; using Bit.Droid.Utilities;
using Plugin.CurrentActivity; using Plugin.CurrentActivity;
using Xamarin.Forms.Platform.Android;
using static Android.Icu.Text.CaseMap;
using static Android.Renderscripts.ScriptGroup;
using static Android.Util.EventLogTags;
using static Bit.App.Pages.SettingsPageViewModel; using static Bit.App.Pages.SettingsPageViewModel;
namespace Bit.Droid.Services namespace Bit.Droid.Services
@@ -209,10 +216,7 @@ namespace Bit.Droid.Services
} }
if (numericKeyboard) if (numericKeyboard)
{ {
input.InputType = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned; SetNumericKeyboardTo(input);
#pragma warning disable CS0618 // Type or member is obsolete
input.KeyListener = DigitsKeyListener.GetInstance(false, false);
#pragma warning restore CS0618 // Type or member is obsolete
} }
if (password) if (password)
{ {
@@ -248,6 +252,82 @@ namespace Bit.Droid.Services
return result.Task; return result.Task;
} }
public Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config)
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
if (activity == null)
{
return Task.FromResult<ValidatablePromptResponse?>(null);
}
var alertBuilder = new AlertDialog.Builder(activity);
alertBuilder.SetTitle(config.Title);
var view = activity.LayoutInflater.Inflate(Resource.Layout.validatable_input_dialog_layout, null);
alertBuilder.SetView(view);
var result = new TaskCompletionSource<ValidatablePromptResponse?>();
alertBuilder.SetPositiveButton(config.OkButtonText ?? AppResources.Ok, listener: null);
alertBuilder.SetNegativeButton(config.CancelButtonText ?? AppResources.Cancel, (sender, args) => result.TrySetResult(null));
if (!string.IsNullOrEmpty(config.ThirdButtonText))
{
alertBuilder.SetNeutralButton(config.ThirdButtonText, (sender, args) => result.TrySetResult(new ValidatablePromptResponse(null, true)));
}
var alert = alertBuilder.Create();
var input = view.FindViewById<EditText>(Resource.Id.txtValue);
var lblHeader = view.FindViewById<TextView>(Resource.Id.lblHeader);
var lblValueSubinfo = view.FindViewById<TextView>(Resource.Id.lblValueSubinfo);
lblHeader.Text = config.Subtitle;
lblValueSubinfo.Text = config.ValueSubInfo;
var defaultSubInfoColor = lblValueSubinfo.TextColors;
input.InputType = InputTypes.ClassText;
if (config.NumericKeyboard)
{
SetNumericKeyboardTo(input);
}
input.ImeOptions = input.ImeOptions | (ImeAction)ImeFlags.NoPersonalizedLearning | (ImeAction)ImeFlags.NoExtractUi;
input.Text = config.Text ?? string.Empty;
input.SetSelection(config.Text?.Length ?? 0);
input.AfterTextChanged += (sender, args) =>
{
if (lblValueSubinfo.Text != config.ValueSubInfo)
{
lblValueSubinfo.Text = config.ValueSubInfo;
lblHeader.SetTextColor(defaultSubInfoColor);
lblValueSubinfo.SetTextColor(defaultSubInfoColor);
}
};
alert.Window.SetSoftInputMode(SoftInput.StateVisible);
alert.Show();
var positiveButton = alert.GetButton((int)DialogButtonType.Positive);
positiveButton.Click += (sender, args) =>
{
var error = config.ValidateText(input.Text);
if (error != null)
{
lblHeader.SetTextColor(ThemeManager.GetResourceColor("DangerColor").ToAndroid());
lblValueSubinfo.SetTextColor(ThemeManager.GetResourceColor("DangerColor").ToAndroid());
lblValueSubinfo.Text = error;
lblValueSubinfo.SendAccessibilityEvent(Android.Views.Accessibility.EventTypes.ViewFocused);
return;
}
result.TrySetResult(new ValidatablePromptResponse(input.Text, false));
alert.Dismiss();
};
return result.Task;
}
public void RateApp() public void RateApp()
{ {
var activity = (MainActivity)CrossCurrentActivity.Current.Activity; var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
@@ -525,5 +605,13 @@ namespace Bit.Droid.Services
// only used by iOS // only used by iOS
throw new NotImplementedException(); throw new NotImplementedException();
} }
private void SetNumericKeyboardTo(EditText editText)
{
editText.InputType = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned;
#pragma warning disable CS0618 // Type or member is obsolete
editText.KeyListener = DigitsKeyListener.GetInstance(false, false);
#pragma warning restore CS0618 // Type or member is obsolete
}
} }
} }

View File

@@ -1,5 +1,6 @@
using Android.Content; using Android.Content;
using Android.OS; using Android.OS;
using Java.Lang;
namespace Bit.Droid.Utilities namespace Bit.Droid.Utilities
{ {
@@ -13,7 +14,12 @@ namespace Bit.Droid.Utilities
// Note: getting the bundle like this will cause to call unparcel() internally // Note: getting the bundle like this will cause to call unparcel() internally
var b = intent?.Extras?.GetBundle("trashstringwhichhasnousebuttocheckunparcel"); var b = intent?.Extras?.GetBundle("trashstringwhichhasnousebuttocheckunparcel");
} }
catch (BadParcelableException) catch (Exception ex) when
(
ex is BadParcelableException ||
ex is ClassNotFoundException ||
ex is RuntimeException
)
{ {
intent.ReplaceExtras((Bundle)null); intent.ReplaceExtras((Bundle)null);
} }

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Utilities.Prompts;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models; using Bit.Core.Models;
@@ -18,6 +19,7 @@ namespace Bit.App.Abstractions
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null, Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false, string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
bool autofocus = true, bool password = false); bool autofocus = true, bool password = false);
Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config);
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons); Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons); Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);

View File

@@ -145,6 +145,8 @@
<Folder Include="Controls\DateTime\" /> <Folder Include="Controls\DateTime\" />
<Folder Include="Controls\IconLabelButton\" /> <Folder Include="Controls\IconLabelButton\" />
<Folder Include="Controls\PasswordStrengthProgressBar\" /> <Folder Include="Controls\PasswordStrengthProgressBar\" />
<Folder Include="Utilities\Automation\" />
<Folder Include="Utilities\Prompts\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -440,5 +442,7 @@
<None Remove="MessagePack" /> <None Remove="MessagePack" />
<None Remove="MessagePack.MSBuild.Tasks" /> <None Remove="MessagePack.MSBuild.Tasks" />
<None Remove="Controls\PasswordStrengthProgressBar\" /> <None Remove="Controls\PasswordStrengthProgressBar\" />
<None Remove="Utilities\Automation\" />
<None Remove="Utilities\Prompts\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -38,6 +38,7 @@ namespace Bit.App
private readonly IFileService _fileService; private readonly IFileService _fileService;
private readonly IAccountsManager _accountsManager; private readonly IAccountsManager _accountsManager;
private readonly IPushNotificationService _pushNotificationService; private readonly IPushNotificationService _pushNotificationService;
private readonly IConfigService _configService;
private static bool _isResumed; private static bool _isResumed;
// these variables are static because the app is launching new activities on notification click, creating new instances of App. // these variables are static because the app is launching new activities on notification click, creating new instances of App.
private static bool _pendingCheckPasswordlessLoginRequests; private static bool _pendingCheckPasswordlessLoginRequests;
@@ -61,6 +62,7 @@ namespace Bit.App
_fileService = ServiceContainer.Resolve<IFileService>(); _fileService = ServiceContainer.Resolve<IFileService>();
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager"); _accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>(); _pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_configService = ServiceContainer.Resolve<IConfigService>();
_accountsManager.Init(() => Options, this); _accountsManager.Init(() => Options, this);
@@ -169,6 +171,10 @@ namespace Bit.App
new NavigationPage(new UpdateTempPasswordPage())); new NavigationPage(new UpdateTempPasswordPage()));
}); });
} }
else if (message.Command == "syncCompleted")
{
await _configService.GetAsync(true);
}
else if (message.Command == Constants.PasswordlessLoginRequestKey else if (message.Command == Constants.PasswordlessLoginRequestKey
|| message.Command == "unlocked" || message.Command == "unlocked"
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED) || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
@@ -293,6 +299,8 @@ namespace Bit.App
// Reset delay on every start // Reset delay on every start
_vaultTimeoutService.DelayLockAndLogoutMs = null; _vaultTimeoutService.DelayLockAndLogoutMs = null;
} }
await _configService.GetAsync();
_messagingService.Send("startEventTimer"); _messagingService.Send("startEventTimer");
} }

View File

@@ -30,13 +30,15 @@
BackgroundColor="{DynamicResource BackgroundColor}" BackgroundColor="{DynamicResource BackgroundColor}"
VerticalOptions="Start" VerticalOptions="Start"
RowHeight="{Binding AccountListRowHeight, Source={x:Reference _mainOverlay}}" RowHeight="{Binding AccountListRowHeight, Source={x:Reference _mainOverlay}}"
effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never"> effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never"
AutomationId="AccountListView">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate x:DataType="view:AccountView"> <DataTemplate x:DataType="view:AccountView">
<controls:AccountViewCell <controls:AccountViewCell
Account="{Binding .}" Account="{Binding .}"
SelectAccountCommand="{Binding SelectAccountCommand, Source={x:Reference _mainOverlay}}" SelectAccountCommand="{Binding SelectAccountCommand, Source={x:Reference _mainOverlay}}"
LongPressAccountCommand="{Binding LongPressAccountCommand, Source={x:Reference _mainOverlay}}" LongPressAccountCommand="{Binding LongPressAccountCommand, Source={x:Reference _mainOverlay}}"
AutomationId="AccountViewCell"
/> />
</DataTemplate> </DataTemplate>
</ListView.ItemTemplate> </ListView.ItemTemplate>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms" <ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit" xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
@@ -60,20 +60,23 @@
Text="{Binding AccountView.Email}" Text="{Binding AccountView.Email}"
IsVisible="{Binding IsActive}" IsVisible="{Binding IsActive}"
StyleClass="accountlist-title, accountlist-title-platform" StyleClass="accountlist-title, accountlist-title-platform"
LineBreakMode="TailTruncation" /> LineBreakMode="TailTruncation"
AutomationId="AccountEmailLabel" />
<Label <Label
Grid.Row="0" Grid.Row="0"
Text="{Binding AccountView.Email}" Text="{Binding AccountView.Email}"
IsVisible="{Binding IsActive, Converter={StaticResource inverseBool}}" IsVisible="{Binding IsActive, Converter={StaticResource inverseBool}}"
StyleClass="accountlist-title, accountlist-title-platform" StyleClass="accountlist-title, accountlist-title-platform"
TextColor="{DynamicResource MutedColor}" TextColor="{DynamicResource MutedColor}"
LineBreakMode="TailTruncation" /> LineBreakMode="TailTruncation"
AutomationId="AccountEmailLabel" />
<Label <Label
Grid.Row="1" Grid.Row="1"
IsVisible="{Binding ShowHostname}" IsVisible="{Binding ShowHostname}"
Text="{Binding AccountView.Hostname}" Text="{Binding AccountView.Hostname}"
StyleClass="accountlist-sub, accountlist-sub-platform" StyleClass="accountlist-sub, accountlist-sub-platform"
LineBreakMode="TailTruncation" /> LineBreakMode="TailTruncation"
AutomationId="AccountHostUrlLabel" />
<Label <Label
Grid.Row="2" Grid.Row="2"
Text="{u:I18n AccountUnlocked}" Text="{u:I18n AccountUnlocked}"
@@ -81,7 +84,8 @@
StyleClass="accountlist-sub, accountlist-sub-platform" StyleClass="accountlist-sub, accountlist-sub-platform"
FontAttributes="Italic" FontAttributes="Italic"
TextTransform="Lowercase" TextTransform="Lowercase"
LineBreakMode="TailTruncation" /> LineBreakMode="TailTruncation"
AutomationId="AccountStatusLabel" />
<Label <Label
Grid.Row="2" Grid.Row="2"
Text="{u:I18n AccountLocked}" Text="{u:I18n AccountLocked}"
@@ -89,7 +93,8 @@
StyleClass="accountlist-sub, accountlist-sub-platform" StyleClass="accountlist-sub, accountlist-sub-platform"
FontAttributes="Italic" FontAttributes="Italic"
TextTransform="Lowercase" TextTransform="Lowercase"
LineBreakMode="TailTruncation" /> LineBreakMode="TailTruncation"
AutomationId="AccountStatusLabel" />
<Label <Label
Grid.Row="2" Grid.Row="2"
Text="{u:I18n AccountLoggedOut}" Text="{u:I18n AccountLoggedOut}"
@@ -97,7 +102,8 @@
StyleClass="accountlist-sub, accountlist-sub-platform" StyleClass="accountlist-sub, accountlist-sub-platform"
FontAttributes="Italic" FontAttributes="Italic"
TextTransform="Lowercase" TextTransform="Lowercase"
LineBreakMode="TailTruncation" /> LineBreakMode="TailTruncation"
AutomationId="AccountStatusLabel" />
</Grid> </Grid>
<controls:IconLabel <controls:IconLabel
@@ -107,7 +113,8 @@
Margin="12,0" Margin="12,0"
HorizontalOptions="Center" HorizontalOptions="Center"
VerticalOptions="Center" VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform" /> StyleClass="list-icon, list-icon-platform"
AutomationId="InactiveVaultIcon" />
<controls:IconLabel <controls:IconLabel
Grid.Column="2" Grid.Column="2"
Text="{Binding AuthStatusIconActive}" Text="{Binding AuthStatusIconActive}"
@@ -116,7 +123,8 @@
HorizontalOptions="Center" HorizontalOptions="Center"
VerticalOptions="Center" VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform" StyleClass="list-icon, list-icon-platform"
TextColor="{DynamicResource TextColor}"/> TextColor="{DynamicResource TextColor}"
AutomationId="ActiveVaultIcon" />
</Grid> </Grid>
<Grid <Grid
@@ -147,7 +155,8 @@
StyleClass="accountlist-title, accountlist-title-platform" StyleClass="accountlist-title, accountlist-title-platform"
LineBreakMode="TailTruncation" LineBreakMode="TailTruncation"
VerticalOptions="Center" VerticalOptions="Center"
Grid.Column="1" /> Grid.Column="1"
AutomationId="AddAccountButton" />
</Grid> </Grid>
</Grid> </Grid>
</ViewCell> </ViewCell>

View File

@@ -9,7 +9,8 @@
StyleClass="list-row, list-row-platform" StyleClass="list-row, list-row-platform"
RowSpacing="0" RowSpacing="0"
ColumnSpacing="0" ColumnSpacing="0"
x:DataType="controls:CipherViewCellViewModel"> x:DataType="controls:CipherViewCellViewModel"
AutomationId="CipherCell">
<Grid.Resources> <Grid.Resources>
<u:IconGlyphConverter x:Key="iconGlyphConverter"/> <u:IconGlyphConverter x:Key="iconGlyphConverter"/>
@@ -36,7 +37,8 @@
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}" IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}" Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
ShouldUpdateFontSizeDynamicallyForAccesibility="True" ShouldUpdateFontSizeDynamicallyForAccesibility="True"
AutomationProperties.IsInAccessibleTree="False" /> AutomationProperties.IsInAccessibleTree="False"
AutomationId="CipherTypeIcon" />
<ff:CachedImage <ff:CachedImage
x:Name="_iconImage" x:Name="_iconImage"
@@ -52,7 +54,8 @@
Aspect="AspectFit" Aspect="AspectFit"
IsVisible="{Binding ShowIconImage}" IsVisible="{Binding ShowIconImage}"
Source="{Binding IconImageSource, Mode=OneTime}" Source="{Binding IconImageSource, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="False" /> AutomationProperties.IsInAccessibleTree="False"
AutomationId="CipherWebsiteIcon" />
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center" Padding="0, 7"> <Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center" Padding="0, 7">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -71,7 +74,8 @@
Grid.Column="0" Grid.Column="0"
Grid.Row="0" Grid.Row="0"
StyleClass="list-title, list-title-platform" StyleClass="list-title, list-title-platform"
Text="{Binding Cipher.Name}" /> Text="{Binding Cipher.Name}"
AutomationId="CipherNameLabel" />
<Label <Label
LineBreakMode="TailTruncation" LineBreakMode="TailTruncation"
Grid.Column="0" Grid.Column="0"
@@ -80,7 +84,8 @@
StyleClass="list-subtitle, list-subtitle-platform" StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Cipher.SubTitle}" Text="{Binding Cipher.SubTitle}"
IsVisible="{Binding Source={RelativeSource Self}, Path=Text, IsVisible="{Binding Source={RelativeSource Self}, Path=Text,
Converter={StaticResource stringHasValueConverter}}"/> Converter={StaticResource stringHasValueConverter}}"
AutomationId="CipherSubTitleLabel" />
<controls:IconLabel <controls:IconLabel
Grid.Column="1" Grid.Column="1"
Grid.Row="0" Grid.Row="0"
@@ -91,7 +96,8 @@
Text="{Binding Source={x:Static core:BitwardenIcons.Collection}}" Text="{Binding Source={x:Static core:BitwardenIcons.Collection}}"
IsVisible="{Binding Cipher.Shared, Mode=OneTime}" IsVisible="{Binding Cipher.Shared, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Shared}" /> AutomationProperties.Name="{u:I18n Shared}"
AutomationId="CipherInCollectionIcon" />
<controls:IconLabel <controls:IconLabel
Grid.Column="2" Grid.Column="2"
Grid.Row="0" Grid.Row="0"
@@ -102,7 +108,8 @@
Text="{Binding Source={x:Static core:BitwardenIcons.Paperclip}}" Text="{Binding Source={x:Static core:BitwardenIcons.Paperclip}}"
IsVisible="{Binding Cipher.HasAttachments, Mode=OneTime}" IsVisible="{Binding Cipher.HasAttachments, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Attachments}" /> AutomationProperties.Name="{u:I18n Attachments}"
AutomationId="CipherWithAttachmentsIcon" />
</Grid> </Grid>
<controls:MiButton <controls:MiButton
@@ -114,6 +121,7 @@
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
HorizontalOptions="EndAndExpand" HorizontalOptions="EndAndExpand"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" /> AutomationProperties.Name="{u:I18n Options}"
AutomationId="CipherOptionsButton" />
</controls:ExtendedGrid> </controls:ExtendedGrid>

View File

@@ -0,0 +1,13 @@
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class CustomLabel : Label
{
public CustomLabel()
{
}
public int? FontWeight { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms" <controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.SendViewCell" x:Class="Bit.App.Controls.SendViewCell"
@@ -54,14 +54,16 @@
Grid.Column="0" Grid.Column="0"
Grid.Row="0" Grid.Row="0"
StyleClass="list-title, list-title-platform" StyleClass="list-title, list-title-platform"
Text="{Binding Send.Name}" /> Text="{Binding Send.Name}"
AutomationId="SendNameLabel" />
<Label <Label
LineBreakMode="TailTruncation" LineBreakMode="TailTruncation"
Grid.Column="0" Grid.Column="0"
Grid.Row="1" Grid.Row="1"
Grid.ColumnSpan="6" Grid.ColumnSpan="6"
StyleClass="list-subtitle, list-subtitle-platform" StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Send.DisplayDate}" /> Text="{Binding Send.DisplayDate}"
AutomationId="SendDateLabel" />
<controls:IconLabel <controls:IconLabel
Grid.Column="1" Grid.Column="1"
Grid.Row="0" Grid.Row="0"
@@ -72,7 +74,8 @@
Text="{Binding Source={x:Static core:BitwardenIcons.ExclamationTriangle}}" Text="{Binding Source={x:Static core:BitwardenIcons.ExclamationTriangle}}"
IsVisible="{Binding Send.Disabled, Mode=OneTime}" IsVisible="{Binding Send.Disabled, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Disabled}" /> AutomationProperties.Name="{u:I18n Disabled}"
AutomationId="DisabledSendLabel" />
<controls:IconLabel <controls:IconLabel
Grid.Column="2" Grid.Column="2"
Grid.Row="0" Grid.Row="0"
@@ -83,7 +86,8 @@
Text="{Binding Source={x:Static core:BitwardenIcons.Key}}" Text="{Binding Source={x:Static core:BitwardenIcons.Key}}"
IsVisible="{Binding Send.HasPassword, Mode=OneTime}" IsVisible="{Binding Send.HasPassword, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Password}" /> AutomationProperties.Name="{u:I18n Password}"
AutomationId="PasswordProtectedSendLabel" />
<controls:IconLabel <controls:IconLabel
Grid.Column="3" Grid.Column="3"
Grid.Row="0" Grid.Row="0"
@@ -94,7 +98,8 @@
Text="{Binding Source={x:Static core:BitwardenIcons.Ban}}" Text="{Binding Source={x:Static core:BitwardenIcons.Ban}}"
IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneTime}" IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n MaxAccessCountReached}" /> AutomationProperties.Name="{u:I18n MaxAccessCountReached}"
AutomationId="SendMaxAccessCountReachedLabel" />
<controls:IconLabel <controls:IconLabel
Grid.Column="4" Grid.Column="4"
Grid.Row="0" Grid.Row="0"
@@ -105,7 +110,8 @@
Text="{Binding Source={x:Static core:BitwardenIcons.Clock}}" Text="{Binding Source={x:Static core:BitwardenIcons.Clock}}"
IsVisible="{Binding Send.Expired, Mode=OneTime}" IsVisible="{Binding Send.Expired, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Expired}" /> AutomationProperties.Name="{u:I18n Expired}"
AutomationId="ExpiredSendLabel" />
<controls:IconLabel <controls:IconLabel
Grid.Column="5" Grid.Column="5"
Grid.Row="0" Grid.Row="0"
@@ -116,7 +122,8 @@
Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}" Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
IsVisible="{Binding Send.PendingDelete, Mode=OneTime}" IsVisible="{Binding Send.PendingDelete, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n PendingDelete}" /> AutomationProperties.Name="{u:I18n PendingDelete}"
AutomationId="SendWithPendingDeletionLabel" />
</Grid> </Grid>
<controls:MiButton <controls:MiButton
@@ -129,6 +136,7 @@
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
HorizontalOptions="EndAndExpand" HorizontalOptions="EndAndExpand"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" /> AutomationProperties.Name="{u:I18n Options}"
AutomationId="SendOptionsButton" />
</controls:ExtendedGrid> </controls:ExtendedGrid>

View File

@@ -33,7 +33,8 @@
StyleClass="box-label" StyleClass="box-label"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" /> IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
AutomationId="BooleanCustomFieldNameLabel" />
<Label <Label
Text="{Binding Field.Name, Mode=OneWay}" Text="{Binding Field.Name, Mode=OneWay}"
IsVisible="{Binding IsEditing}" IsVisible="{Binding IsEditing}"
@@ -49,13 +50,15 @@
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
Margin="0, 5, 0, 0" Margin="0, 5, 0, 0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" /> IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
AutomationId="BooleanCustomFieldValueLabel" />
<Switch <Switch
IsToggled="{Binding BooleanValue}" IsToggled="{Binding BooleanValue}"
IsVisible="{Binding IsEditing}" IsVisible="{Binding IsEditing}"
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" /> Grid.RowSpan="2"
AutomationId="BooleanCustomFieldValueToggle" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}" Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"

View File

@@ -31,7 +31,8 @@
Text="{Binding Field.Name, Mode=OneWay}" Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label" StyleClass="box-label"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" /> Grid.Column="0"
AutomationId="HiddenCustomFieldNameLabel" />
<StackLayout <StackLayout
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
@@ -39,7 +40,8 @@
<controls:MonoLabel <controls:MonoLabel
Text="{Binding ValueText, Mode=OneWay}" Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue}" /> IsVisible="{Binding ShowHiddenValue}"
AutomationId="HiddenCustomFieldValueLabel" />
<controls:MonoLabel <controls:MonoLabel
Text="{Binding Field.MaskedValue, Mode=OneWay}" Text="{Binding Field.MaskedValue, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
@@ -54,7 +56,10 @@
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding ShowViewHidden}" IsEnabled="{Binding ShowViewHidden}"
IsSpellCheckEnabled="False" IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"> IsTextPredictionEnabled="False"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding Field.Name}"
AutomationId="HiddenCustomFieldValueEntry">
<Entry.Keyboard> <Entry.Keyboard>
<Keyboard x:FactoryMethod="Create"> <Keyboard x:FactoryMethod="Create">
<x:Arguments> <x:Arguments>
@@ -72,7 +77,8 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" /> AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationId="HiddenCustomFieldShowValueButton" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}" Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"

View File

@@ -29,13 +29,15 @@
Text="{Binding Field.Name, Mode=OneWay}" Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label" StyleClass="box-label"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" /> Grid.Column="0"
AutomationId="LinkedCustomFieldNameLabel" />
<controls:IconLabel <controls:IconLabel
Text="{Binding ValueText, Mode=OneWay}" Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" /> IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
AutomationId="LinkedCustomFieldValueLabel" />
<StackLayout <StackLayout
StyleClass="box-row, box-row-input" StyleClass="box-row, box-row-input"
IsVisible="{Binding IsEditing}"> IsVisible="{Binding IsEditing}">
@@ -44,7 +46,8 @@
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}" ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}" SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
ItemDisplayBinding="{Binding Key}" ItemDisplayBinding="{Binding Key}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="LinkedCustomFieldValuePicker" />
</StackLayout> </StackLayout>
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
@@ -55,7 +58,8 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" /> AutomationProperties.Name="{u:I18n Options}"
AutomationId="LinkedCustomFieldOptionsButton" />
</Grid> </Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" /> <BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
</StackLayout> </StackLayout>

View File

@@ -29,19 +29,24 @@
Text="{Binding Field.Name, Mode=OneWay}" Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label" StyleClass="box-label"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" /> Grid.Column="0"
AutomationId="TextCustomFieldNameLabel" />
<Label <Label
Text="{Binding ValueText, Mode=OneWay}" Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" /> IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
AutomationId="TextCustomFieldValueLabel" />
<Entry <Entry
Text="{Binding Field.Value}" Text="{Binding Field.Value}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
IsVisible="{Binding IsEditing}" /> IsVisible="{Binding IsEditing}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding Field.Name}"
AutomationId="TextCustomFieldValueEntry" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}" Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -51,7 +56,8 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Copy}" /> AutomationProperties.Name="{u:I18n Copy}"
AutomationId="TextCustomFieldCopyValue" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}" Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
@@ -61,7 +67,8 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" /> AutomationProperties.Name="{u:I18n Options}"
AutomationId="TextCustomFieldOptionsButton" />
</Grid> </Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" /> <BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
</StackLayout> </StackLayout>

View File

@@ -34,7 +34,8 @@
Placeholder="ex. https://bitwarden.company.com" Placeholder="ex. https://bitwarden.company.com"
StyleClass="box-value" StyleClass="box-value"
ReturnType="Go" ReturnType="Go"
ReturnCommand="{Binding SubmitCommand}" /> ReturnCommand="{Binding SubmitCommand}"
AutomationId="ServerUrlEntry"/>
</StackLayout> </StackLayout>
<Label <Label
Text="{u:I18n SelfHostedEnvironmentFooter}" Text="{u:I18n SelfHostedEnvironmentFooter}"
@@ -53,7 +54,8 @@
x:Name="_webVaultEntry" x:Name="_webVaultEntry"
Text="{Binding WebVaultUrl}" Text="{Binding WebVaultUrl}"
Keyboard="Url" Keyboard="Url"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="WebVaultUrlEntry"/>
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row"> <StackLayout StyleClass="box-row">
<Label <Label
@@ -63,7 +65,8 @@
x:Name="_apiEntry" x:Name="_apiEntry"
Text="{Binding ApiUrl}" Text="{Binding ApiUrl}"
Keyboard="Url" Keyboard="Url"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ApiUrlEntry"/>
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row"> <StackLayout StyleClass="box-row">
<Label <Label
@@ -73,7 +76,8 @@
x:Name="_identityEntry" x:Name="_identityEntry"
Text="{Binding IdentityUrl}" Text="{Binding IdentityUrl}"
Keyboard="Url" Keyboard="Url"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="IdentityUrlEntry"/>
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row"> <StackLayout StyleClass="box-row">
<Label <Label
@@ -85,7 +89,8 @@
Keyboard="Url" Keyboard="Url"
StyleClass="box-value" StyleClass="box-value"
ReturnType="Go" ReturnType="Go"
ReturnCommand="{Binding SubmitCommand}" /> ReturnCommand="{Binding SubmitCommand}"
AutomationId="IconsUrlEntry"/>
</StackLayout> </StackLayout>
<Label <Label
Text="{u:I18n CustomEnvironmentFooter}" Text="{u:I18n CustomEnvironmentFooter}"

View File

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Models.Data;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.ObjectModel;
@@ -18,7 +19,8 @@ namespace Bit.App.Pages
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService"); _environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
PageTitle = AppResources.Settings; PageTitle = AppResources.Settings;
BaseUrl = _environmentService.BaseUrl; BaseUrl = _environmentService.BaseUrl == EnvironmentUrlData.DefaultEU.Base || EnvironmentUrlData.DefaultUS.Base == _environmentService.BaseUrl ?
string.Empty : _environmentService.BaseUrl;
WebVaultUrl = _environmentService.WebVaultUrl; WebVaultUrl = _environmentService.WebVaultUrl;
ApiUrl = _environmentService.ApiUrl; ApiUrl = _environmentService.ApiUrl;
IdentityUrl = _environmentService.IdentityUrl; IdentityUrl = _environmentService.IdentityUrl;

View File

@@ -23,12 +23,9 @@
Priority="-1" Priority="-1"
UseOriginalImage="True" UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" /> AutomationProperties.Name="{u:I18n Account}"
AutomationId="AccountIconButton" />
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/> <ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
<ToolbarItem
Icon="cog_environment.png" Clicked="Environment_Clicked" Order="Primary"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>
<ContentPage.Resources> <ContentPage.Resources>
@@ -53,7 +50,9 @@
Keyboard="Email" Keyboard="Email"
StyleClass="box-value" StyleClass="box-value"
ReturnType="Go" ReturnType="Go"
ReturnCommand="{Binding ContinueCommand}"> ReturnCommand="{Binding ContinueCommand}"
AutomationId="EmailAddressEntry"
>
<VisualStateManager.VisualStateGroups> <VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates"> <VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Disabled"> <VisualState x:Name="Disabled">
@@ -66,7 +65,28 @@
</Entry> </Entry>
<StackLayout <StackLayout
Orientation="Horizontal" Orientation="Horizontal"
Margin="0, 16, 0 ,0"> Margin="0, 6, 0 ,0">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding ShowEnvironmentPickerCommand}" />
</StackLayout.GestureRecognizers>
<Label
Text="{Binding RegionText}"
FontSize="13"
TextColor="{DynamicResource MutedColor}"
VerticalOptions="Center"
VerticalTextAlignment="Center"/>
<controls:IconLabel
Text="{Binding SelectedEnvironmentName}"
FontSize="13"
TextColor="{DynamicResource PrimaryColor}"
VerticalOptions="Center"
VerticalTextAlignment="Center"
AutomationId="RegionSelectorDropdown"/>
</StackLayout>
<StackLayout
Orientation="Horizontal"
Margin="0, 20, 0 ,0">
<StackLayout.GestureRecognizers> <StackLayout.GestureRecognizers>
<TapGestureRecognizer <TapGestureRecognizer
Command="{Binding RememberEmailCommand}" /> Command="{Binding RememberEmailCommand}" />
@@ -76,21 +96,27 @@
StyleClass="text-sm" StyleClass="text-sm"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
VerticalOptions="Center" VerticalOptions="Center"
VerticalTextAlignment="Center"/> VerticalTextAlignment="Center"
/>
<Switch <Switch
Scale="0.8" Scale="0.8"
IsToggled="{Binding RememberEmail}" IsToggled="{Binding RememberEmail}"
VerticalOptions="Center"/> VerticalOptions="Center"
AutomationId="RememberMeSwitch"
/>
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
<Button Text="{u:I18n Continue}" <Button Text="{u:I18n Continue}"
StyleClass="btn-primary" StyleClass="btn-primary"
IsEnabled="{Binding CanContinue}" IsEnabled="{Binding CanContinue}"
Command="{Binding ContinueCommand}" /> Command="{Binding ContinueCommand}"
AutomationId="ContinueButton"
/>
<Label FormattedText="{Binding CreateAccountText}" <Label FormattedText="{Binding CreateAccountText}"
Margin="0, 10" Margin="0, 10"
StyleClass="box-footer-label"> StyleClass="box-footer-label"
AutomationId="CreateAccountLabel">
<Label.GestureRecognizers> <Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CreateAccountCommand}" /> <TapGestureRecognizer Command="{Binding CreateAccountCommand}" />
</Label.GestureRecognizers> </Label.GestureRecognizers>
@@ -116,5 +142,4 @@
MainPage="{Binding Source={x:Reference _page}}" MainPage="{Binding Source={x:Reference _page}}"
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/> BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
</AbsoluteLayout> </AbsoluteLayout>
</pages:BaseContentPage> </pages:BaseContentPage>

View File

@@ -15,6 +15,8 @@ namespace Bit.App.Pages
private readonly AppOptions _appOptions; private readonly AppOptions _appOptions;
private IBroadcasterService _broadcasterService; private IBroadcasterService _broadcasterService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
public HomePage(AppOptions appOptions = null) public HomePage(AppOptions appOptions = null)
{ {
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService"); _broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
@@ -70,6 +72,14 @@ namespace Bit.App.Pages
}); });
} }
}); });
try
{
await _vm.UpdateEnvironment();
}
catch (Exception ex)
{
_logger.Value?.Exception(ex);
}
} }
protected override bool OnBackButtonPressed() protected override bool OnBackButtonPressed()
@@ -128,14 +138,6 @@ namespace Bit.App.Pages
await Navigation.PushModalAsync(new NavigationPage(page)); await Navigation.PushModalAsync(new NavigationPage(page));
} }
private void Environment_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.StartEnvironmentAction();
}
}
private async Task StartEnvironmentAsync() private async Task StartEnvironmentAsync()
{ {
await _accountListOverlay.HideAsync(); await _accountListOverlay.HideAsync();

View File

@@ -4,7 +4,10 @@ using Bit.App.Abstractions;
using Bit.App.Controls; using Bit.App.Controls;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Response;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.ObjectModel;
@@ -17,16 +20,19 @@ namespace Bit.App.Pages
{ {
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly IMessagingService _messagingService; private readonly IMessagingService _messagingService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly ILogger _logger;
private readonly IEnvironmentService _environmentService;
private readonly IAccountsManager _accountManager;
private readonly IConfigService _configService;
private bool _showCancelButton; private bool _showCancelButton;
private bool _rememberEmail; private bool _rememberEmail;
private string _email; private string _email;
private string _selectedEnvironmentName;
private bool _isEmailEnabled; private bool _isEmailEnabled;
private bool _canLogin; private bool _canLogin;
private IPlatformUtilsService _platformUtilsService; private bool _displayEuEnvironment;
private ILogger _logger;
private IEnvironmentService _environmentService;
private IAccountsManager _accountManager;
public HomeViewModel() public HomeViewModel()
{ {
@@ -36,6 +42,7 @@ namespace Bit.App.Pages
_logger = ServiceContainer.Resolve<ILogger>(); _logger = ServiceContainer.Resolve<ILogger>();
_environmentService = ServiceContainer.Resolve<IEnvironmentService>(); _environmentService = ServiceContainer.Resolve<IEnvironmentService>();
_accountManager = ServiceContainer.Resolve<IAccountsManager>(); _accountManager = ServiceContainer.Resolve<IAccountsManager>();
_configService = ServiceContainer.Resolve<IConfigService>();
PageTitle = AppResources.Bitwarden; PageTitle = AppResources.Bitwarden;
@@ -49,6 +56,8 @@ namespace Bit.App.Pages
onException: _logger.Exception, allowsMultipleExecutions: false); onException: _logger.Exception, allowsMultipleExecutions: false);
CloseCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(CloseAction), CloseCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(CloseAction),
onException: _logger.Exception, allowsMultipleExecutions: false); onException: _logger.Exception, allowsMultipleExecutions: false);
ShowEnvironmentPickerCommand = new AsyncCommand(ShowEnvironmentPickerAsync,
onException: _logger.Exception, allowsMultipleExecutions: false);
InitAsync().FireAndForget(); InitAsync().FireAndForget();
} }
@@ -71,6 +80,13 @@ namespace Bit.App.Pages
additionalPropertyNames: new[] { nameof(CanContinue) }); additionalPropertyNames: new[] { nameof(CanContinue) });
} }
public string SelectedEnvironmentName
{
get => $"{_selectedEnvironmentName} {BitwardenIcons.AngleDown}";
set => SetProperty(ref _selectedEnvironmentName, value);
}
public string RegionText => $"{AppResources.Region}:";
public bool CanContinue => !string.IsNullOrEmpty(Email); public bool CanContinue => !string.IsNullOrEmpty(Email);
public FormattedString CreateAccountText public FormattedString CreateAccountText
@@ -101,11 +117,13 @@ namespace Bit.App.Pages
public AsyncCommand ContinueCommand { get; } public AsyncCommand ContinueCommand { get; }
public AsyncCommand CloseCommand { get; } public AsyncCommand CloseCommand { get; }
public AsyncCommand CreateAccountCommand { get; } public AsyncCommand CreateAccountCommand { get; }
public AsyncCommand ShowEnvironmentPickerCommand { get; }
public async Task InitAsync() public async Task InitAsync()
{ {
Email = await _stateService.GetRememberedEmailAsync(); Email = await _stateService.GetRememberedEmailAsync();
RememberEmail = !string.IsNullOrEmpty(Email); RememberEmail = !string.IsNullOrEmpty(Email);
_displayEuEnvironment = await _configService.GetFeatureFlagBoolAsync(Constants.DisplayEuEnvironmentFlag, forceRefresh: true);
} }
public async Task ContinueToLoginStepAsync() public async Task ContinueToLoginStepAsync()
@@ -144,5 +162,59 @@ namespace Bit.App.Pages
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred, AppResources.Ok); await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred, AppResources.Ok);
} }
} }
public async Task ShowEnvironmentPickerAsync()
{
_displayEuEnvironment = await _configService.GetFeatureFlagBoolAsync(Constants.DisplayEuEnvironmentFlag);
var options = _displayEuEnvironment
? new string[] { AppResources.US, AppResources.EU, AppResources.SelfHosted }
: new string[] { AppResources.US, AppResources.SelfHosted };
await Device.InvokeOnMainThreadAsync(async () =>
{
var result = await Page.DisplayActionSheet(AppResources.DataRegion, AppResources.Cancel, null, options);
if (result is null || result == AppResources.Cancel)
{
return;
}
if (result == AppResources.SelfHosted)
{
StartEnvironmentAction?.Invoke();
return;
}
await _environmentService.SetUrlsAsync(result == AppResources.EU ? EnvironmentUrlData.DefaultEU : EnvironmentUrlData.DefaultUS);
await _configService.GetAsync(true);
SelectedEnvironmentName = result;
});
}
public async Task UpdateEnvironment()
{
var environmentsSaved = await _stateService.GetPreAuthEnvironmentUrlsAsync();
if (environmentsSaved == null || environmentsSaved.IsEmpty)
{
await _environmentService.SetUrlsAsync(EnvironmentUrlData.DefaultUS);
environmentsSaved = EnvironmentUrlData.DefaultUS;
SelectedEnvironmentName = AppResources.US;
return;
}
if (environmentsSaved.Base == EnvironmentUrlData.DefaultUS.Base)
{
SelectedEnvironmentName = AppResources.US;
}
else if (environmentsSaved.Base == EnvironmentUrlData.DefaultEU.Base)
{
SelectedEnvironmentName = AppResources.EU;
}
else
{
await _configService.GetAsync(true);
SelectedEnvironmentName = AppResources.SelfHosted;
}
}
} }
} }

View File

@@ -24,7 +24,8 @@
Priority="-1" Priority="-1"
UseOriginalImage="True" UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" /> AutomationProperties.Name="{u:I18n Account}"
AutomationId="AccountIconButton" />
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>
<ContentPage.Resources> <ContentPage.Resources>
@@ -45,7 +46,7 @@
<StackLayout StyleClass="box"> <StackLayout StyleClass="box">
<Grid <Grid
StyleClass="box-row" StyleClass="box-row"
IsVisible="{Binding PinLock}" IsVisible="{Binding PinEnabled}"
Padding="0, 10, 0, 0"> Padding="0, 10, 0, 0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -71,7 +72,8 @@
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
ReturnType="Go" ReturnType="Go"
ReturnCommand="{Binding SubmitCommand}" /> ReturnCommand="{Binding SubmitCommand}"
AutomationId="PinEntry" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}" Text="{Binding ShowPasswordIcon}"
@@ -81,12 +83,13 @@
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/> AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
AutomationId="PinVisibilityToggle" />
</Grid> </Grid>
<Grid <Grid
x:Name="_passwordGrid" x:Name="_passwordGrid"
StyleClass="box-row" StyleClass="box-row"
IsVisible="{Binding PinLock, Converter={StaticResource inverseBool}}" IsVisible="{Binding PinEnabled, Converter={StaticResource inverseBool}}"
Padding="0, 10, 0, 0"> Padding="0, 10, 0, 0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -111,7 +114,8 @@
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
ReturnType="Go" ReturnType="Go"
ReturnCommand="{Binding SubmitCommand}" /> ReturnCommand="{Binding SubmitCommand}"
AutomationId="MasterPasswordEntry" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}" Text="{Binding ShowPasswordIcon}"
@@ -121,7 +125,9 @@
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" /> AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
AutomationId="PasswordVisibilityToggle"
/>
</Grid> </Grid>
<StackLayout <StackLayout
StyleClass="box-row" StyleClass="box-row"
@@ -147,7 +153,8 @@
x:Name="_unlockButton" x:Name="_unlockButton"
Text="{u:I18n Unlock}" Text="{u:I18n Unlock}"
StyleClass="btn-primary" StyleClass="btn-primary"
Clicked="Unlock_Clicked" /> Clicked="Unlock_Clicked"
AutomationId="UnlockVaultButton" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
</ScrollView> </ScrollView>

View File

@@ -44,7 +44,7 @@ namespace Bit.App.Pages
{ {
get get
{ {
if (_vm?.PinLock ?? false) if (_vm?.PinEnabled ?? false)
{ {
return _pin; return _pin;
} }
@@ -54,7 +54,7 @@ namespace Bit.App.Pages
public async Task PromptBiometricAfterResumeAsync() public async Task PromptBiometricAfterResumeAsync()
{ {
if (_vm.BiometricLock) if (_vm.BiometricEnabled)
{ {
await Task.Delay(500); await Task.Delay(500);
if (!_promptedAfterResume) if (!_promptedAfterResume)
@@ -91,13 +91,13 @@ namespace Bit.App.Pages
_vm.FocusSecretEntry += PerformFocusSecretEntry; _vm.FocusSecretEntry += PerformFocusSecretEntry;
if (!_vm.BiometricLock) if (!_vm.BiometricEnabled)
{ {
RequestFocus(SecretEntry); RequestFocus(SecretEntry);
} }
else else
{ {
if (_vm.UsingKeyConnector && !_vm.PinLock) if (_vm.UsingKeyConnector && !_vm.PinEnabled)
{ {
_passwordGrid.IsVisible = false; _passwordGrid.IsVisible = false;
_unlockButton.IsVisible = false; _unlockButton.IsVisible = false;

View File

@@ -33,21 +33,21 @@ namespace Bit.App.Pages
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>(); private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IPasswordGenerationService _passwordGenerationService; private readonly IPasswordGenerationService _passwordGenerationService;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
private readonly ISyncService _syncService;
private string _email; private string _email;
private string _masterPassword; private string _masterPassword;
private string _pin; private string _pin;
private bool _showPassword; private bool _showPassword;
private bool _pinLock; private PinLockEnum _pinStatus;
private bool _biometricLock; private bool _pinEnabled;
private bool _biometricEnabled;
private bool _biometricIntegrityValid = true; private bool _biometricIntegrityValid = true;
private bool _biometricButtonVisible; private bool _biometricButtonVisible;
private bool _usingKeyConnector; private bool _usingKeyConnector;
private string _biometricButtonText; private string _biometricButtonText;
private string _loggedInAsText; private string _loggedInAsText;
private string _lockedVerifyText; private string _lockedVerifyText;
private bool _isPinProtected;
private bool _isPinProtectedWithKey;
public LockPageViewModel() public LockPageViewModel()
{ {
@@ -65,6 +65,8 @@ namespace Bit.App.Pages
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>(); _watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
_policyService = ServiceContainer.Resolve<IPolicyService>(); _policyService = ServiceContainer.Resolve<IPolicyService>();
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(); _passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
_syncService = ServiceContainer.Resolve<ISyncService>();
PageTitle = AppResources.VerifyMasterPassword; PageTitle = AppResources.VerifyMasterPassword;
TogglePasswordCommand = new Command(TogglePassword); TogglePasswordCommand = new Command(TogglePassword);
@@ -100,10 +102,10 @@ namespace Bit.App.Pages
}); });
} }
public bool PinLock public bool PinEnabled
{ {
get => _pinLock; get => _pinEnabled;
set => SetProperty(ref _pinLock, value); set => SetProperty(ref _pinEnabled, value);
} }
public bool UsingKeyConnector public bool UsingKeyConnector
@@ -111,10 +113,10 @@ namespace Bit.App.Pages
get => _usingKeyConnector; get => _usingKeyConnector;
} }
public bool BiometricLock public bool BiometricEnabled
{ {
get => _biometricLock; get => _biometricEnabled;
set => SetProperty(ref _biometricLock, value); set => SetProperty(ref _biometricEnabled, value);
} }
public bool BiometricIntegrityValid public bool BiometricIntegrityValid
@@ -162,14 +164,18 @@ namespace Bit.App.Pages
public async Task InitAsync() public async Task InitAsync()
{ {
(_isPinProtected, _isPinProtectedWithKey) = await _vaultTimeoutService.IsPinLockSetAsync(); _pinStatus = await _vaultTimeoutService.IsPinLockSetAsync();
PinLock = (_isPinProtected && await _stateService.GetPinProtectedKeyAsync() != null) ||
_isPinProtectedWithKey; var ephemeralPinSet = await _stateService.GetUserKeyPinEphemeralAsync()
BiometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _cryptoService.HasKeyAsync(); ?? await _stateService.GetPinProtectedKeyAsync();
PinEnabled = (_pinStatus == PinLockEnum.Transient && ephemeralPinSet != null) ||
_pinStatus == PinLockEnum.Persistent;
BiometricEnabled = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _cryptoService.HasEncryptedUserKeyAsync();
// Users with key connector and without biometric or pin has no MP to unlock with // Users with key connector and without biometric or pin has no MP to unlock with
_usingKeyConnector = await _keyConnectorService.GetUsesKeyConnector(); _usingKeyConnector = await _keyConnectorService.GetUsesKeyConnector();
if (_usingKeyConnector && !(BiometricLock || PinLock)) if (_usingKeyConnector && !(BiometricEnabled || PinEnabled))
{ {
await _vaultTimeoutService.LogOutAsync(); await _vaultTimeoutService.LogOutAsync();
return; return;
@@ -188,7 +194,7 @@ namespace Bit.App.Pages
} }
var webVaultHostname = CoreHelpers.GetHostname(webVault); var webVaultHostname = CoreHelpers.GetHostname(webVault);
LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, webVaultHostname); LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, webVaultHostname);
if (PinLock) if (PinEnabled)
{ {
PageTitle = AppResources.VerifyPIN; PageTitle = AppResources.VerifyPIN;
LockedVerifyText = AppResources.VaultLockedPIN; LockedVerifyText = AppResources.VaultLockedPIN;
@@ -207,7 +213,7 @@ namespace Bit.App.Pages
} }
} }
if (BiometricLock) if (BiometricEnabled)
{ {
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync(); BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
if (!_biometricIntegrityValid) if (!_biometricIntegrityValid)
@@ -229,14 +235,14 @@ namespace Bit.App.Pages
public async Task SubmitAsync() public async Task SubmitAsync()
{ {
if (PinLock && string.IsNullOrWhiteSpace(Pin)) if (PinEnabled && string.IsNullOrWhiteSpace(Pin))
{ {
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
string.Format(AppResources.ValidationFieldRequired, AppResources.PIN), string.Format(AppResources.ValidationFieldRequired, AppResources.PIN),
AppResources.Ok); AppResources.Ok);
return; return;
} }
if (!PinLock && string.IsNullOrWhiteSpace(MasterPassword)) if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword))
{ {
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
@@ -247,34 +253,54 @@ namespace Bit.App.Pages
ShowPassword = false; ShowPassword = false;
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile)); var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
if (PinLock) if (PinEnabled)
{ {
var failed = true; var failed = true;
try try
{ {
if (_isPinProtected) EncString userKeyPin = null;
EncString oldPinProtected = null;
if (_pinStatus == PinLockEnum.Persistent)
{ {
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email, userKeyPin = await _stateService.GetUserKeyPinAsync();
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
}
else if (_pinStatus == PinLockEnum.Transient)
{
userKeyPin = await _stateService.GetUserKeyPinEphemeralAsync();
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
}
UserKey userKey;
if (oldPinProtected != null)
{
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
_pinStatus == PinLockEnum.Transient,
Pin,
_email,
kdfConfig, kdfConfig,
await _stateService.GetPinProtectedKeyAsync()); oldPinProtected
var encKey = await _cryptoService.GetEncKeyAsync(key); );
}
else
{
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
Pin,
_email,
kdfConfig,
userKeyPin
);
}
var protectedPin = await _stateService.GetProtectedPinAsync(); var protectedPin = await _stateService.GetProtectedPinAsync();
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey); var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
failed = decPin != Pin; failed = decryptedPin != Pin;
if (!failed) if (!failed)
{ {
Pin = string.Empty; Pin = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await SetKeyAndContinueAsync(key); await SetKeyAndContinueAsync(userKey);
}
}
else
{
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email, kdfConfig);
failed = false;
Pin = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await SetKeyAndContinueAsync(key);
} }
} }
catch catch
@@ -295,19 +321,21 @@ namespace Bit.App.Pages
} }
else else
{ {
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdfConfig); var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig);
var storedKeyHash = await _cryptoService.GetKeyHashAsync(); var storedKeyHash = await _cryptoService.GetPasswordHashAsync();
var passwordValid = false; var passwordValid = false;
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null; MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
if (storedKeyHash != null) if (storedKeyHash != null)
{ {
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, key); // Offline unlock possible
passwordValid = await _cryptoService.CompareAndUpdatePasswordHashAsync(MasterPassword, masterKey);
} }
else else
{ {
// Online unlock required
await _deviceActionService.ShowLoadingAsync(AppResources.Loading); await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization); var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, masterKey, HashPurpose.ServerAuthorization);
var request = new PasswordVerificationRequest(); var request = new PasswordVerificationRequest();
request.MasterPasswordHash = keyHash; request.MasterPasswordHash = keyHash;
@@ -316,8 +344,8 @@ namespace Bit.App.Pages
var response = await _apiService.PostAccountVerifyPasswordAsync(request); var response = await _apiService.PostAccountVerifyPasswordAsync(request);
enforcedMasterPasswordOptions = response.MasterPasswordPolicy; enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
passwordValid = true; passwordValid = true;
var localKeyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization); var localKeyHash = await _cryptoService.HashPasswordAsync(MasterPassword, masterKey, HashPurpose.LocalAuthorization);
await _cryptoService.SetKeyHashAsync(localKeyHash); await _cryptoService.SetPasswordHashAsync(localKeyHash);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -327,15 +355,6 @@ namespace Bit.App.Pages
} }
if (passwordValid) if (passwordValid)
{ {
if (_isPinProtected)
{
var protectedPin = await _stateService.GetProtectedPinAsync();
var encKey = await _cryptoService.GetEncKeyAsync(key);
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, _email, kdfConfig);
await _stateService.SetPinProtectedKeyAsync(await _cryptoService.EncryptAsync(key.Key, pinKey));
}
if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions)) if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions))
{ {
// Save the ForcePasswordResetReason to force a password reset after unlock // Save the ForcePasswordResetReason to force a password reset after unlock
@@ -345,10 +364,13 @@ namespace Bit.App.Pages
MasterPassword = string.Empty; MasterPassword = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await SetKeyAndContinueAsync(key);
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
await _cryptoService.SetMasterKeyAsync(masterKey);
await SetKeyAndContinueAsync(userKey);
// Re-enable biometrics // Re-enable biometrics
if (BiometricLock & !BiometricIntegrityValid) if (BiometricEnabled & !BiometricIntegrityValid)
{ {
await _biometricService.SetupBiometricAsync(); await _biometricService.SetupBiometricAsync();
} }
@@ -425,7 +447,7 @@ namespace Bit.App.Pages
public void TogglePassword() public void TogglePassword()
{ {
ShowPassword = !ShowPassword; ShowPassword = !ShowPassword;
var secret = PinLock ? Pin : MasterPassword; var secret = PinEnabled ? Pin : MasterPassword;
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry)); _secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
} }
@@ -433,12 +455,12 @@ namespace Bit.App.Pages
{ {
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync(); BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
BiometricButtonVisible = BiometricIntegrityValid; BiometricButtonVisible = BiometricIntegrityValid;
if (!BiometricLock || !BiometricIntegrityValid) if (!BiometricEnabled || !BiometricIntegrityValid)
{ {
return; return;
} }
var success = await _platformUtilsService.AuthenticateBiometricAsync(null, var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
PinLock ? AppResources.PIN : AppResources.MasterPassword, PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry))); () => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
await _stateService.SetBiometricLockedAsync(!success); await _stateService.SetBiometricLockedAsync(!success);
if (success) if (success)
@@ -447,18 +469,20 @@ namespace Bit.App.Pages
} }
} }
private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key) private async Task SetKeyAndContinueAsync(UserKey key)
{ {
var hasKey = await _cryptoService.HasKeyAsync(); var hasKey = await _cryptoService.HasUserKeyAsync();
if (!hasKey) if (!hasKey)
{ {
await _cryptoService.SetKeyAsync(key); await _cryptoService.SetUserKeyAsync(key);
} }
await _deviceTrustCryptoService.TrustDeviceIfNeededAsync();
await DoContinueAsync(); await DoContinueAsync();
} }
private async Task DoContinueAsync() private async Task DoContinueAsync()
{ {
_syncService.FullSyncAsync(false).FireAndForget();
await _stateService.SetBiometricLockedAsync(false); await _stateService.SetBiometricLockedAsync(false);
_watchDeviceService.SyncDataToWatchAsync().FireAndForget(); _watchDeviceService.SyncDataToWatchAsync().FireAndForget();
_messagingService.Send("unlocked"); _messagingService.Send("unlocked");

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.LoginApproveDevicePage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:LoginApproveDeviceViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:LoginApproveDeviceViewModel />
</ContentPage.BindingContext>
<StackLayout Padding="10, 10">
<StackLayout Padding="5, 10" Orientation="Horizontal">
<StackLayout HorizontalOptions="FillAndExpand">
<Label
StyleClass="text-md"
Text="{u:I18n RememberThisDevice}"/>
<Label
StyleClass="box-sub-label"
Text="{u:I18n TurnOffUsingPublicDevice}"/>
</StackLayout>
<Switch
Scale="0.8"
IsToggled="{Binding RememberThisDevice}"
VerticalOptions="Center"/>
</StackLayout>
<StackLayout Margin="0, 20, 0, 0">
<Button
x:Name="_continue"
Text="{u:I18n Continue}"
StyleClass="btn-primary"
Command="{Binding ContinueCommand}"
IsVisible="{Binding ContinueEnabled}"/>
<Button
x:Name="_approveWithMyOtherDevice"
Text="{u:I18n ApproveWithMyOtherDevice}"
StyleClass="btn-primary"
Command="{Binding ApproveWithMyOtherDeviceCommand}"
IsVisible="{Binding ApproveWithMyOtherDeviceEnabled}"/>
<Button
x:Name="_requestAdminApproval"
Text="{u:I18n RequestAdminApproval}"
StyleClass="box-button-row"
Command="{Binding RequestAdminApprovalCommand}"
IsVisible="{Binding RequestAdminApprovalEnabled}"/>
<Button
x:Name="_approveWithMasterPassword"
Text="{u:I18n ApproveWithMasterPassword}"
StyleClass="box-button-row"
Command="{Binding ApproveWithMasterPasswordCommand}"
IsVisible="{Binding ApproveWithMasterPasswordEnabled}"/>
<Label
Text="{Binding LoggingInAsText}"
StyleClass="text-sm"
Margin="0,40,0,0"
AutomationId="LoggingInAsLabel"
/>
<Label
Text="{u:I18n NotYou}"
StyleClass="text-md"
HorizontalOptions="Start"
TextColor="{DynamicResource HyperlinkColor}"
AutomationId="NotYouLabel">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="Cancel_Clicked" />
</Label.GestureRecognizers>
</Label>
</StackLayout>
</StackLayout>
</pages:BaseContentPage>

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class LoginApproveDevicePage : BaseContentPage
{
private readonly LoginApproveDeviceViewModel _vm;
private readonly AppOptions _appOptions;
public LoginApproveDevicePage(AppOptions appOptions = null)
{
InitializeComponent();
_vm = BindingContext as LoginApproveDeviceViewModel;
_vm.LogInWithMasterPasswordAction = () => StartLogInWithMasterPassword().FireAndForget();
_vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget();
_vm.RequestAdminApprovalAction = () => RequestAdminApprovalAsync().FireAndForget();
_vm.CloseAction = () => { Navigation.PopModalAsync(); };
_vm.Page = this;
_appOptions = appOptions;
}
protected override void OnAppearing()
{
_vm.InitAsync();
}
private void Cancel_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.CloseAction();
}
}
private async Task StartLogInWithMasterPassword()
{
var page = new LockPage(_appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task StartLoginWithDeviceAsync()
{
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task RequestAdminApprovalAsync()
{
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AdminApproval, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
}

View File

@@ -0,0 +1,132 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities.AccountManagement;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Request;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class LoginApproveDeviceViewModel : BaseViewModel
{
private bool _rememberThisDevice;
private bool _approveWithMyOtherDeviceEnabled;
private bool _requestAdminApprovalEnabled;
private bool _approveWithMasterPasswordEnabled;
private bool _continueEnabled;
private string _email;
private readonly IStateService _stateService;
private readonly IApiService _apiService;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
public ICommand ApproveWithMyOtherDeviceCommand { get; }
public ICommand RequestAdminApprovalCommand { get; }
public ICommand ApproveWithMasterPasswordCommand { get; }
public ICommand ContinueCommand { get; }
public Action LogInWithMasterPasswordAction { get; set; }
public Action LogInWithDeviceAction { get; set; }
public Action RequestAdminApprovalAction { get; set; }
public Action CloseAction { get; set; }
public LoginApproveDeviceViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>();
_apiService = ServiceContainer.Resolve<IApiService>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
PageTitle = AppResources.LoggedIn;
ApproveWithMyOtherDeviceCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithDeviceAction),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
RequestAdminApprovalCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(RequestAdminApprovalAction),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
ApproveWithMasterPasswordCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithMasterPasswordAction),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
ContinueCommand = new AsyncCommand(InitAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
}
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
public bool RememberThisDevice
{
get => _rememberThisDevice;
set => SetProperty(ref _rememberThisDevice, value);
}
public bool ApproveWithMyOtherDeviceEnabled
{
get => _approveWithMyOtherDeviceEnabled;
set => SetProperty(ref _approveWithMyOtherDeviceEnabled, value);
}
public bool RequestAdminApprovalEnabled
{
get => _requestAdminApprovalEnabled;
set => SetProperty(ref _requestAdminApprovalEnabled, value);
}
public bool ApproveWithMasterPasswordEnabled
{
get => _approveWithMasterPasswordEnabled;
set => SetProperty(ref _approveWithMasterPasswordEnabled, value);
}
public bool ContinueEnabled
{
get => _continueEnabled;
set => SetProperty(ref _continueEnabled, value);
}
public string Email
{
get => _email;
set => SetProperty(ref _email, value, additionalPropertyNames:
new string[] {
nameof(LoggingInAsText)
});
}
public async Task InitAsync()
{
try
{
Email = await _stateService.GetRememberedEmailAsync();
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
RequestAdminApprovalEnabled = decryptOptions?.TrustedDeviceOption?.HasAdminApproval ?? false;
ApproveWithMasterPasswordEnabled = decryptOptions?.HasMasterPassword ?? false;
ApproveWithMyOtherDeviceEnabled = decryptOptions?.TrustedDeviceOption?.HasLoginApprovingDevice ?? false;
}
catch (Exception ex)
{
HandleException(ex);
}
// TODO: Change this expression to, Appear if the browser is trusted and shared the key with the app
ContinueEnabled = !RequestAdminApprovalEnabled && !ApproveWithMasterPasswordEnabled && !ApproveWithMyOtherDeviceEnabled;
}
private async Task SetDeviceTrustAndInvokeAsync(Action action)
{
await _deviceTrustCryptoService.SetShouldTrustDeviceAsync(RememberThisDevice);
await Device.InvokeOnMainThreadAsync(action);
}
}
}

View File

@@ -9,23 +9,23 @@
xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:LoginPageViewModel" x:DataType="pages:LoginPageViewModel"
x:Name="_page" x:Name="_page"
Title="{Binding PageTitle}"> Title="{Binding PageTitle}"
AutomationId="PageTitleLabel">
<ContentPage.BindingContext> <ContentPage.BindingContext>
<pages:LoginPageViewModel /> <pages:LoginPageViewModel />
</ContentPage.BindingContext> </ContentPage.BindingContext>
<ContentPage.ToolbarItems> <ContentPage.ToolbarItems>
<controls:ExtendedToolbarItem <controls:ExtendedToolbarItem
x:Name="_accountAvatar" x:Name="_accountAvatar"
x:Key="accountAvatar"
IconImageSource="{Binding AvatarImageSource}" IconImageSource="{Binding AvatarImageSource}"
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}" Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
Order="Primary" Order="Primary"
Priority="-1" Priority="-1"
UseOriginalImage="True" UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" /> AutomationProperties.Name="{u:I18n Account}"
AutomationId="AccountIconButton" />
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>
<ContentPage.Resources> <ContentPage.Resources>
@@ -34,7 +34,8 @@
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary" <ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
x:Name="_moreItem" x:Key="moreItem" x:Name="_moreItem" x:Key="moreItem"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" /> AutomationProperties.Name="{u:I18n Options}"
AutomationId="OptionsButton" />
<ToolbarItem Text="{u:I18n GetPasswordHint}" <ToolbarItem Text="{u:I18n GetPasswordHint}"
x:Key="getPasswordHint" x:Key="getPasswordHint"
x:Name="_getPasswordHint" x:Name="_getPasswordHint"
@@ -75,7 +76,9 @@
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
ReturnType="Go" ReturnType="Go"
ReturnCommand="{Binding LogInCommand}" /> ReturnCommand="{Binding LogInCommand}"
AutomationId="MasterPasswordEntry"
/>
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}" Text="{Binding ShowPasswordIcon}"
@@ -84,6 +87,7 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="1" Grid.RowSpan="1"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationId="PasswordVisibilityToggle"
AutomationProperties.Name="{u:I18n ToggleVisibility}" AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/> AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
<Label <Label
@@ -93,7 +97,9 @@
Padding="0,5,0,0" Padding="0,5,0,0"
Grid.Row="2" Grid.Row="2"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="2"> Grid.ColumnSpan="2"
AutomationId="GetMasterPasswordHintLabel"
>
<Label.GestureRecognizers> <Label.GestureRecognizers>
<TapGestureRecognizer Tapped="Hint_Clicked" /> <TapGestureRecognizer Tapped="Hint_Clicked" />
</Label.GestureRecognizers> </Label.GestureRecognizers>
@@ -104,19 +110,24 @@
<Button x:Name="_loginWithMasterPassword" <Button x:Name="_loginWithMasterPassword"
Text="{u:I18n LogInWithMasterPassword}" Text="{u:I18n LogInWithMasterPassword}"
StyleClass="btn-primary" StyleClass="btn-primary"
Clicked="LogIn_Clicked" /> Clicked="LogIn_Clicked"
AutomationId="LogInWithMasterPasswordButton"
/>
<controls:IconLabelButton <controls:IconLabelButton
HorizontalOptions="Fill" HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}" Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
Label="{u:I18n LogInWithAnotherDevice}" Label="{u:I18n LogInWithAnotherDevice}"
ButtonCommand="{Binding LogInWithDeviceCommand}" ButtonCommand="{Binding LogInWithDeviceCommand}"
IsVisible="{Binding IsKnownDevice}"/> IsVisible="{Binding IsKnownDevice}"
AutomationId="LogInWithAnotherDeviceButton"
/>
<controls:IconLabelButton <controls:IconLabelButton
HorizontalOptions="Fill" HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
Icon="{Binding Source={x:Static core:BitwardenIcons.Suitcase}}" Icon="{Binding Source={x:Static core:BitwardenIcons.Suitcase}}"
Label="{u:I18n LogInSso}"> Label="{u:I18n LogInSso}"
AutomationId="LogInWithSsoButton">
<controls:IconLabelButton.GestureRecognizers> <controls:IconLabelButton.GestureRecognizers>
<TapGestureRecognizer Tapped="LogInSSO_Clicked" /> <TapGestureRecognizer Tapped="LogInSSO_Clicked" />
</controls:IconLabelButton.GestureRecognizers> </controls:IconLabelButton.GestureRecognizers>
@@ -124,12 +135,15 @@
<Label <Label
Text="{Binding LoggingInAsText}" Text="{Binding LoggingInAsText}"
StyleClass="text-sm" StyleClass="text-sm"
Margin="0,40,0,0"/> Margin="0,40,0,0"
AutomationId="LoggingInAsLabel"
/>
<Label <Label
Text="{u:I18n NotYou}" Text="{u:I18n NotYou}"
StyleClass="text-md" StyleClass="text-md"
HorizontalOptions="Start" HorizontalOptions="Start"
TextColor="{DynamicResource HyperlinkColor}"> TextColor="{DynamicResource HyperlinkColor}"
AutomationId="NotYouLabel">
<Label.GestureRecognizers> <Label.GestureRecognizers>
<TapGestureRecognizer Tapped="Cancel_Clicked" /> <TapGestureRecognizer Tapped="Cancel_Clicked" />
</Label.GestureRecognizers> </Label.GestureRecognizers>

View File

@@ -4,6 +4,7 @@ using Bit.App.Models;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.Core; using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.ObjectModel;
@@ -135,7 +136,7 @@ namespace Bit.App.Pages
private async Task StartLoginWithDeviceAsync() private async Task StartLoginWithDeviceAsync()
{ {
var page = new LoginPasswordlessRequestPage(_vm.Email, _appOptions); var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page)); await Navigation.PushModalAsync(new NavigationPage(page));
} }

View File

@@ -41,6 +41,7 @@ namespace Bit.App.Pages
private bool _isEmailEnabled; private bool _isEmailEnabled;
private bool _isKnownDevice; private bool _isKnownDevice;
private bool _isExecutingLogin; private bool _isExecutingLogin;
private string _environmentHostName;
public LoginPageViewModel() public LoginPageViewModel()
{ {
@@ -115,6 +116,16 @@ namespace Bit.App.Pages
set => SetProperty(ref _isKnownDevice, value); set => SetProperty(ref _isKnownDevice, value);
} }
public string EnvironmentDomainName
{
get => _environmentHostName;
set => SetProperty(ref _environmentHostName, value,
additionalPropertyNames: new string[]
{
nameof(LoggingInAsText)
});
}
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; } public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public Command LogInCommand { get; } public Command LogInCommand { get; }
public Command TogglePasswordCommand { get; } public Command TogglePasswordCommand { get; }
@@ -122,7 +133,7 @@ namespace Bit.App.Pages
public ICommand LogInWithDeviceCommand { get; } public ICommand LogInWithDeviceCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email); public string LoggingInAsText => string.Format(AppResources.LoggingInAsXOnY, Email, EnvironmentDomainName);
public bool IsIosExtension { get; set; } public bool IsIosExtension { get; set; }
public bool CanRemoveAccount { get; set; } public bool CanRemoveAccount { get; set; }
public Action StartTwoFactorAction { get; set; } public Action StartTwoFactorAction { get; set; }
@@ -151,6 +162,7 @@ namespace Bit.App.Pages
Email = await _stateService.GetRememberedEmailAsync(); Email = await _stateService.GetRememberedEmailAsync();
} }
CanRemoveAccount = await _stateService.GetActiveUserEmailAsync() != Email; CanRemoveAccount = await _stateService.GetActiveUserEmailAsync() != Email;
EnvironmentDomainName = CoreHelpers.GetDomain((await _stateService.GetPreAuthEnvironmentUrlsAsync())?.Base);
IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, await _appIdService.GetAppIdAsync()); IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, await _appIdService.GetAppIdAsync());
} }
catch (ApiException apiEx) when (apiEx.Error.StatusCode == System.Net.HttpStatusCode.Unauthorized) catch (ApiException apiEx) when (apiEx.Error.StatusCode == System.Net.HttpStatusCode.Unauthorized)

View File

@@ -32,7 +32,8 @@
<Label <Label
Text="{Binding LogInAttemptByLabel}" Text="{Binding LogInAttemptByLabel}"
FontSize="Small" FontSize="Small"
Margin="0,0,0,24"/> Margin="0,0,0,24"
AutomationId="LogInAttemptByLabel" />
<Label <Label
Text="{u:I18n FingerprintPhrase}" Text="{u:I18n FingerprintPhrase}"
FontSize="Small" FontSize="Small"
@@ -41,7 +42,8 @@
FormattedText="{Binding LoginRequest.FingerprintPhrase}" FormattedText="{Binding LoginRequest.FingerprintPhrase}"
FontSize="Medium" FontSize="Medium"
TextColor="{DynamicResource FingerprintPhrase}" TextColor="{DynamicResource FingerprintPhrase}"
Margin="0,0,0,27"/> Margin="0,0,0,27"
AutomationId="FingerprintValueLabel" />
<Label <Label
Text="{u:I18n DeviceType}" Text="{u:I18n DeviceType}"
FontSize="Small" FontSize="Small"
@@ -49,7 +51,8 @@
<Label <Label
Text="{Binding LoginRequest.DeviceType}" Text="{Binding LoginRequest.DeviceType}"
FontSize="Small" FontSize="Small"
Margin="0,0,0,21"/> Margin="0,0,0,21"
AutomationId="DeviceTypeValueLabel" />
<Label <Label
Text="{u:I18n IpAddress}" Text="{u:I18n IpAddress}"
IsVisible="{Binding ShowIpAddress}" IsVisible="{Binding ShowIpAddress}"
@@ -59,7 +62,8 @@
Text="{Binding LoginRequest.IpAddress}" Text="{Binding LoginRequest.IpAddress}"
IsVisible="{Binding ShowIpAddress}" IsVisible="{Binding ShowIpAddress}"
FontSize="Small" FontSize="Small"
Margin="0,0,0,21"/> Margin="0,0,0,21"
AutomationId="IpAddressValueLabel" />
<Label <Label
Text="{u:I18n Time}" Text="{u:I18n Time}"
FontSize="Small" FontSize="Small"
@@ -67,7 +71,8 @@
<Label <Label
Text="{Binding TimeOfRequestText}" Text="{Binding TimeOfRequestText}"
FontSize="Small" FontSize="Small"
Margin="0,0,0,57"/> Margin="0,0,0,57"
AutomationId="TimeOfRequestValueLabel" />
</StackLayout> </StackLayout>
</ScrollView> </ScrollView>
@@ -75,11 +80,13 @@
Text="{u:I18n ConfirmLogIn}" Text="{u:I18n ConfirmLogIn}"
Command="{Binding AcceptRequestCommand}" Command="{Binding AcceptRequestCommand}"
Margin="0,0,0,17" Margin="0,0,0,17"
StyleClass="btn-primary"/> StyleClass="btn-primary"
AutomationId="ConfirmLoginButton" />
<Button <Button
Text="{u:I18n DenyLogIn}" Text="{u:I18n DenyLogIn}"
Command="{Binding RejectRequestCommand}" Command="{Binding RejectRequestCommand}"
StyleClass="btn-secundary"/> StyleClass="btn-secundary"
AutomationId="DenyLoginButton" />
</StackLayout> </StackLayout>
</pages:BaseContentPage> </pages:BaseContentPage>

View File

@@ -21,16 +21,17 @@
<StackLayout <StackLayout
Padding="7, 0, 7, 20"> Padding="7, 0, 7, 20">
<Label <Label
Text="{u:I18n LogInInitiated}" Text="{Binding Tittle}"
FontSize="Title" FontSize="Title"
FontAttributes="Bold" FontAttributes="Bold"
Margin="0,14,0,21"/> Margin="0,14,0,21"
AutomationId="LogInInitiatedLabel" />
<Label <Label
Text="{u:I18n ANotificationHasBeenSentToYourDevice}" Text="{Binding SubTittle}"
FontSize="Small" FontSize="Small"
Margin="0,0,0,10"/> Margin="0,0,0,10"/>
<Label <Label
Text="{u:I18n PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice}" Text="{Binding Description}"
FontSize="Small" FontSize="Small"
Margin="0,0,0,24"/> Margin="0,0,0,24"/>
<Label <Label
@@ -40,13 +41,16 @@
<controls:MonoLabel <controls:MonoLabel
FormattedText="{Binding FingerprintPhrase}" FormattedText="{Binding FingerprintPhrase}"
FontSize="Medium" FontSize="Medium"
TextColor="{DynamicResource FingerprintPhrase}"/> TextColor="{DynamicResource FingerprintPhrase}"
AutomationId="FingerprintPhraseValue" />
<Label <Label
Text="{u:I18n ResendNotification}" Text="{u:I18n ResendNotification}"
IsVisible="{Binding ResendNotificationVisible}"
StyleClass="text-md" StyleClass="text-md"
HorizontalOptions="Start" HorizontalOptions="Start"
Margin="0,40,0,0" Margin="0,40,0,0"
TextColor="{DynamicResource HyperlinkColor}"> TextColor="{DynamicResource HyperlinkColor}"
AutomationId="ResendNotificationButton">
<Label.GestureRecognizers> <Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CreatePasswordlessLoginCommand}" /> <TapGestureRecognizer Command="{Binding CreatePasswordlessLoginCommand}" />
</Label.GestureRecognizers> </Label.GestureRecognizers>
@@ -55,7 +59,7 @@
Orientation="Horizontal" Orientation="Horizontal"
Margin="0,30,0,0"> Margin="0,30,0,0">
<Label <Label
Text="{u:I18n NeedAnotherOption}" Text="{Binding OtherOptions}"
FontSize="Small" FontSize="Small"
VerticalTextAlignment="End"/> VerticalTextAlignment="End"/>
<Label <Label
@@ -64,7 +68,8 @@
VerticalTextAlignment="End" VerticalTextAlignment="End"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
Margin="5, 0" Margin="5, 0"
TextColor="{DynamicResource HyperlinkColor}"> TextColor="{DynamicResource HyperlinkColor}"
AutomationId="ViewAllLoginOptionsButton">
<Label.GestureRecognizers> <Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CloseCommand}" /> <TapGestureRecognizer Command="{Binding CloseCommand}" />
</Label.GestureRecognizers> </Label.GestureRecognizers>

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Models; using Bit.App.Models;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.Core.Enums;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
@@ -12,13 +13,14 @@ namespace Bit.App.Pages
private LoginPasswordlessRequestViewModel _vm; private LoginPasswordlessRequestViewModel _vm;
private readonly AppOptions _appOptions; private readonly AppOptions _appOptions;
public LoginPasswordlessRequestPage(string email, AppOptions appOptions = null) public LoginPasswordlessRequestPage(string email, AuthRequestType authRequestType, AppOptions appOptions = null)
{ {
InitializeComponent(); InitializeComponent();
_appOptions = appOptions; _appOptions = appOptions;
_vm = BindingContext as LoginPasswordlessRequestViewModel; _vm = BindingContext as LoginPasswordlessRequestViewModel;
_vm.Page = this; _vm.Page = this;
_vm.Email = email; _vm.Email = email;
_vm.AuthRequestType = authRequestType;
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync()); _vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync()); _vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); _vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -12,6 +13,7 @@ using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Models.Response;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.ObjectModel;
@@ -32,6 +34,9 @@ namespace Bit.App.Pages
private IPlatformUtilsService _platformUtilsService; private IPlatformUtilsService _platformUtilsService;
private IEnvironmentService _environmentService; private IEnvironmentService _environmentService;
private ILogger _logger; private ILogger _logger;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly ICryptoService _cryptoService;
protected override II18nService i18nService => _i18nService; protected override II18nService i18nService => _i18nService;
protected override IEnvironmentService environmentService => _environmentService; protected override IEnvironmentService environmentService => _environmentService;
@@ -44,6 +49,7 @@ namespace Bit.App.Pages
private string _email; private string _email;
private string _requestId; private string _requestId;
private string _requestAccessCode; private string _requestAccessCode;
private AuthRequestType _authRequestType;
// Item1 publicKey, Item2 privateKey // Item1 publicKey, Item2 privateKey
private Tuple<byte[], byte[]> _requestKeyPair; private Tuple<byte[], byte[]> _requestKeyPair;
@@ -57,6 +63,9 @@ namespace Bit.App.Pages
_i18nService = ServiceContainer.Resolve<II18nService>(); _i18nService = ServiceContainer.Resolve<II18nService>();
_stateService = ServiceContainer.Resolve<IStateService>(); _stateService = ServiceContainer.Resolve<IStateService>();
_logger = ServiceContainer.Resolve<ILogger>(); _logger = ServiceContainer.Resolve<ILogger>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>();
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
PageTitle = AppResources.LogInWithAnotherDevice; PageTitle = AppResources.LogInWithAnotherDevice;
@@ -77,6 +86,70 @@ namespace Bit.App.Pages
public ICommand CreatePasswordlessLoginCommand { get; } public ICommand CreatePasswordlessLoginCommand { get; }
public ICommand CloseCommand { get; } public ICommand CloseCommand { get; }
public string Tittle
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.LogInInitiated;
case AuthRequestType.AdminApproval:
return AppResources.AdminApprovalRequested;
default:
return string.Empty;
};
}
}
public string SubTittle
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.ANotificationHasBeenSentToYourDevice;
case AuthRequestType.AdminApproval:
return AppResources.YourRequestHasBeenSentToYourAdmin;
default:
return string.Empty;
};
}
}
public string Description
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice;
case AuthRequestType.AdminApproval:
return AppResources.YouWillBeNotifiedOnceApproved;
default:
return string.Empty;
};
}
}
public string OtherOptions
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.NeedAnotherOption;
case AuthRequestType.AdminApproval:
return AppResources.TroubleLoggingIn;
default:
return string.Empty;
};
}
}
public string FingerprintPhrase public string FingerprintPhrase
{ {
get => _fingerprintPhrase; get => _fingerprintPhrase;
@@ -89,6 +162,21 @@ namespace Bit.App.Pages
set => SetProperty(ref _email, value); set => SetProperty(ref _email, value);
} }
public AuthRequestType AuthRequestType
{
get => _authRequestType;
set => SetProperty(ref _authRequestType, value, additionalPropertyNames: new string[]
{
nameof(Tittle),
nameof(SubTittle),
nameof(Description),
nameof(OtherOptions),
nameof(ResendNotificationVisible)
});
}
public bool ResendNotificationVisible => AuthRequestType == AuthRequestType.AuthenticateAndUnlock;
public void StartCheckLoginRequestStatus() public void StartCheckLoginRequestStatus()
{ {
try try
@@ -119,14 +207,22 @@ namespace Bit.App.Pages
private async Task CheckLoginRequestStatus() private async Task CheckLoginRequestStatus()
{ {
if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_requestAccessCode)) if (string.IsNullOrEmpty(_requestId))
{ {
return; return;
} }
try try
{ {
var response = await _authService.GetPasswordlessLoginResponseAsync(_requestId, _requestAccessCode); PasswordlessLoginResponse response = null;
if (await _stateService.IsAuthenticatedAsync())
{
response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId);
}
else
{
response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode);
}
if (response.RequestApproved == null || !response.RequestApproved.Value) if (response.RequestApproved == null || !response.RequestApproved.Value)
{ {
@@ -138,6 +234,12 @@ namespace Bit.App.Pages
var authResult = await _authService.LogInPasswordlessAsync(Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash); var authResult = await _authService.LogInPasswordlessAsync(Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await AppHelpers.ResetInvalidUnlockAttemptsAsync();
if (authResult == null && await _stateService.IsAuthenticatedAsync())
{
await HandleLoginCompleteAsync();
return;
}
if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus)) if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus))
{ {
return; return;
@@ -153,8 +255,7 @@ namespace Bit.App.Pages
} }
else else
{ {
_syncService.FullSyncAsync(true).FireAndForget(); await HandleLoginCompleteAsync();
LogInSuccessAction?.Invoke();
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -164,22 +265,66 @@ namespace Bit.App.Pages
} }
} }
private async Task HandleLoginCompleteAsync()
{
await _stateService.SetPendingAdminAuthRequestAsync(null);
_syncService.FullSyncAsync(true).FireAndForget();
LogInSuccessAction?.Invoke();
}
private async Task CreatePasswordlessLoginAsync() private async Task CreatePasswordlessLoginAsync()
{ {
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading)); await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
var response = await _authService.PasswordlessCreateLoginRequestAsync(_email); PasswordlessLoginResponse response = null;
if (response != null) var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
if (pendingRequest != null && _authRequestType == AuthRequestType.AdminApproval)
{ {
response = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
if (response == null || (response.IsAnswered && !response.RequestApproved.Value))
{
// handle pending auth request not valid remove it from state
await _stateService.SetPendingAdminAuthRequestAsync(null);
pendingRequest = null;
}
else
{
// Derive pubKey from privKey in state to avoid MITM attacks
// Also generate FingerprintPhrase locally for the same reason
var derivedPublicKey = await _cryptoFunctionService.RsaExtractPublicKeyAsync(pendingRequest.PrivateKey);
response.FingerprintPhrase = string.Join("-", await _cryptoService.GetFingerprintAsync(Email, derivedPublicKey));
response.RequestKeyPair = new Tuple<byte[], byte[]>(derivedPublicKey, pendingRequest.PrivateKey);
}
}
if (response == null)
{
response = await _authService.PasswordlessCreateLoginRequestAsync(_email, AuthRequestType);
}
await HandlePasswordlessLoginAsync(response, pendingRequest == null && _authRequestType == AuthRequestType.AdminApproval);
await _deviceActionService.HideLoadingAsync();
}
private async Task HandlePasswordlessLoginAsync(PasswordlessLoginResponse response, bool createPendingAdminRequest)
{
if (response == null)
{
throw new ArgumentNullException(nameof(response));
}
if (createPendingAdminRequest)
{
var pendingAuthRequest = new PendingAdminAuthRequest { Id = response.Id, PrivateKey = response.RequestKeyPair.Item2 };
await _stateService.SetPendingAdminAuthRequestAsync(pendingAuthRequest);
}
FingerprintPhrase = response.FingerprintPhrase; FingerprintPhrase = response.FingerprintPhrase;
_requestId = response.Id; _requestId = response.Id;
_requestAccessCode = response.RequestAccessCode; _requestAccessCode = response.RequestAccessCode;
_requestKeyPair = response.RequestKeyPair; _requestKeyPair = response.RequestKeyPair;
} }
await _deviceActionService.HideLoadingAsync();
}
private void HandleException(Exception ex) private void HandleException(Exception ex)
{ {
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () => Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>

View File

@@ -29,6 +29,8 @@ namespace Bit.App.Pages
_vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync()); _vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync());
_vm.UpdateTempPasswordAction = _vm.UpdateTempPasswordAction =
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
_vm.StartDeviceApprovalOptionsAction =
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
_vm.CloseAction = async () => _vm.CloseAction = async () =>
{ {
await Navigation.PopModalAsync(); await Navigation.PopModalAsync();
@@ -106,10 +108,17 @@ namespace Bit.App.Pages
await Navigation.PushModalAsync(new NavigationPage(page)); await Navigation.PushModalAsync(new NavigationPage(page));
} }
private async Task StartDeviceApprovalOptionsAsync()
{
var page = new LoginApproveDevicePage();
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task SsoAuthSuccessAsync() private async Task SsoAuthSuccessAsync()
{ {
RestoreAppOptionsFromCopy(); RestoreAppOptionsFromCopy();
await AppHelpers.ClearPreviousPage(); await AppHelpers.ClearPreviousPage();
if (await _vaultTimeoutService.IsLockedAsync()) if (await _vaultTimeoutService.IsLockedAsync())
{ {
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions)); Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));

View File

@@ -9,6 +9,7 @@ using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials; using Xamarin.Essentials;
@@ -29,6 +30,8 @@ namespace Bit.App.Pages
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
private readonly ICryptoService _cryptoService;
private string _orgIdentifier; private string _orgIdentifier;
@@ -45,7 +48,8 @@ namespace Bit.App.Pages
_stateService = ServiceContainer.Resolve<IStateService>("stateService"); _stateService = ServiceContainer.Resolve<IStateService>("stateService");
_logger = ServiceContainer.Resolve<ILogger>("logger"); _logger = ServiceContainer.Resolve<ILogger>("logger");
_organizationService = ServiceContainer.Resolve<IOrganizationService>(); _organizationService = ServiceContainer.Resolve<IOrganizationService>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
PageTitle = AppResources.Bitwarden; PageTitle = AppResources.Bitwarden;
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false); LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
@@ -61,6 +65,7 @@ namespace Bit.App.Pages
public Action StartTwoFactorAction { get; set; } public Action StartTwoFactorAction { get; set; }
public Action StartSetPasswordAction { get; set; } public Action StartSetPasswordAction { get; set; }
public Action SsoAuthSuccessAction { get; set; } public Action SsoAuthSuccessAction { get; set; }
public Action StartDeviceApprovalOptionsAction { get; set; }
public Action CloseAction { get; set; } public Action CloseAction { get; set; }
public Action UpdateTempPasswordAction { get; set; } public Action UpdateTempPasswordAction { get; set; }
@@ -197,6 +202,7 @@ namespace Bit.App.Pages
try try
{ {
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId); var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier); await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
await _deviceActionService.HideLoadingAsync(); await _deviceActionService.HideLoadingAsync();
@@ -212,9 +218,31 @@ namespace Bit.App.Pages
{ {
UpdateTempPasswordAction?.Invoke(); UpdateTempPasswordAction?.Invoke();
} }
else if (decryptOptions?.TrustedDeviceOption != null)
{
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (!decryptOptions.HasMasterPassword &&
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
{
StartSetPasswordAction?.Invoke();
}
else if (response.ForcePasswordReset)
{
UpdateTempPasswordAction?.Invoke();
}
else if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
{
_syncService.FullSyncAsync(true).FireAndForget();
SsoAuthSuccessAction?.Invoke();
}
else else
{ {
var task = Task.Run(async () => await _syncService.FullSyncAsync(true)); StartDeviceApprovalOptionsAction?.Invoke();
}
}
else
{
_syncService.FullSyncAsync(true).FireAndForget();
SsoAuthSuccessAction?.Invoke(); SsoAuthSuccessAction?.Invoke();
} }
} }

View File

@@ -35,7 +35,8 @@
x:Name="_email" x:Name="_email"
Text="{Binding Email}" Text="{Binding Email}"
Keyboard="Email" Keyboard="Email"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="EmailAddressEntry"/>
</StackLayout> </StackLayout>
<Grid StyleClass="box-row"> <Grid StyleClass="box-row">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -59,7 +60,8 @@
IsTextPredictionEnabled="False" IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}" IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" /> Grid.Column="0"
AutomationId="MasterPasswordEntry"/>
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}" Text="{Binding ShowPasswordIcon}"
@@ -69,7 +71,8 @@
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/> AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
AutomationId="PasswordVisibilityToggle"/>
</Grid> </Grid>
<Label <Label
StyleClass="box-sub-label" StyleClass="box-sub-label"
@@ -109,7 +112,8 @@
IsTextPredictionEnabled="False" IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}" IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" /> Grid.Column="0"
AutomationId="ConfirmMasterPasswordEntry"/>
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}" Text="{Binding ShowPasswordIcon}"
@@ -118,6 +122,7 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationId="ConfirmPasswordVisibilityToggle"
AutomationProperties.Name="{u:I18n ToggleVisibility}" AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" /> AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid> </Grid>
@@ -130,7 +135,8 @@
Text="{Binding Hint}" Text="{Binding Hint}"
StyleClass="box-value" StyleClass="box-value"
ReturnType="Go" ReturnType="Go"
ReturnCommand="{Binding SubmitCommand}" /> ReturnCommand="{Binding SubmitCommand}"
AutomationId="MasterPasswordHintLabel" />
</StackLayout> </StackLayout>
<Label <Label
Text="{u:I18n MasterPasswordHintDescription}" Text="{u:I18n MasterPasswordHintDescription}"
@@ -142,7 +148,8 @@
IsToggled="{Binding CheckExposedMasterPassword}" IsToggled="{Binding CheckExposedMasterPassword}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="Start" HorizontalOptions="Start"
Margin="0, 0, 10, 0"/> Margin="0, 0, 10, 0"
AutomationId="CheckExposedMasterPasswordToggle"/>
<Label <Label
Text="{u:I18n CheckKnownDataBreachesForThisPassword}" Text="{u:I18n CheckKnownDataBreachesForThisPassword}"
StyleClass="box-footer-label" StyleClass="box-footer-label"
@@ -154,7 +161,8 @@
IsToggled="{Binding AcceptPolicies}" IsToggled="{Binding AcceptPolicies}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="Start" HorizontalOptions="Start"
Margin="0, 0, 10, 0"/> Margin="0, 0, 10, 0"
AutomationId="AcceptPoliciesToggle"/>
<Label StyleClass="box-footer-label" <Label StyleClass="box-footer-label"
HorizontalOptions="Fill"> HorizontalOptions="Fill">
<Label.FormattedText> <Label.FormattedText>

View File

@@ -177,25 +177,28 @@ namespace Bit.App.Pages
Name = string.IsNullOrWhiteSpace(Name) ? null : Name; Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
Email = Email.Trim().ToLower(); Email = Email.Trim().ToLower();
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null); var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
var key = await _cryptoService.MakeKeyAsync(MasterPassword, Email, kdfConfig); var newMasterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, Email, kdfConfig);
var encKey = await _cryptoService.MakeEncKeyAsync(key); var (newUserKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(
var hashedPassword = await _cryptoService.HashPasswordAsync(MasterPassword, key); newMasterKey,
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1); await _cryptoService.MakeUserKeyAsync()
);
var hashedPassword = await _cryptoService.HashPasswordAsync(MasterPassword, newMasterKey);
var (newPublicKey, newProtectedPrivateKey) = await _cryptoService.MakeKeyPairAsync(newUserKey);
var request = new RegisterRequest var request = new RegisterRequest
{ {
Email = Email, Email = Email,
Name = Name, Name = Name,
MasterPasswordHash = hashedPassword, MasterPasswordHash = hashedPassword,
MasterPasswordHint = Hint, MasterPasswordHint = Hint,
Key = encKey.Item2.EncryptedString, Key = newProtectedUserKey.EncryptedString,
Kdf = kdfConfig.Type, Kdf = kdfConfig.Type,
KdfIterations = kdfConfig.Iterations, KdfIterations = kdfConfig.Iterations,
KdfMemory = kdfConfig.Memory, KdfMemory = kdfConfig.Memory,
KdfParallelism = kdfConfig.Parallelism, KdfParallelism = kdfConfig.Parallelism,
Keys = new KeysRequest Keys = new KeysRequest
{ {
PublicKey = keys.Item1, PublicKey = newPublicKey,
EncryptedPrivateKey = keys.Item2.EncryptedString EncryptedPrivateKey = newProtectedPrivateKey.EncryptedString
}, },
CaptchaResponse = _captchaToken, CaptchaResponse = _captchaToken,
}; };

View File

@@ -165,26 +165,18 @@ namespace Bit.App.Pages
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null); var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
var email = await _stateService.GetEmailAsync(); var email = await _stateService.GetEmailAsync();
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig); var newMasterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, email, kdfConfig);
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization); var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, newMasterKey, HashPurpose.ServerAuthorization);
var localMasterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization); var localMasterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, newMasterKey, HashPurpose.LocalAuthorization);
Tuple<SymmetricCryptoKey, EncString> encKey; var (newUserKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(newMasterKey,
var existingEncKey = await _cryptoService.GetEncKeyAsync(); await _cryptoService.GetUserKeyAsync() ?? await _cryptoService.MakeUserKeyAsync());
if (existingEncKey == null)
{
encKey = await _cryptoService.MakeEncKeyAsync(key);
}
else
{
encKey = await _cryptoService.RemakeEncKeyAsync(key);
}
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1); var keys = await _cryptoService.MakeKeyPairAsync(newUserKey);
var request = new SetPasswordRequest var request = new SetPasswordRequest
{ {
MasterPasswordHash = masterPasswordHash, MasterPasswordHash = masterPasswordHash,
Key = encKey.Item2.EncryptedString, Key = newProtectedUserKey.EncryptedString,
MasterPasswordHint = Hint, MasterPasswordHint = Hint,
Kdf = kdfConfig.Type.GetValueOrDefault(KdfType.PBKDF2_SHA256), Kdf = kdfConfig.Type.GetValueOrDefault(KdfType.PBKDF2_SHA256),
KdfIterations = kdfConfig.Iterations.GetValueOrDefault(Constants.Pbkdf2Iterations), KdfIterations = kdfConfig.Iterations.GetValueOrDefault(Constants.Pbkdf2Iterations),
@@ -204,19 +196,19 @@ namespace Bit.App.Pages
// Set Password and relevant information // Set Password and relevant information
await _apiService.SetPasswordAsync(request); await _apiService.SetPasswordAsync(request);
await _stateService.SetKdfConfigurationAsync(kdfConfig); await _stateService.SetKdfConfigurationAsync(kdfConfig);
await _cryptoService.SetKeyAsync(key); await _cryptoService.SetMasterKeyAsync(newMasterKey);
await _cryptoService.SetKeyHashAsync(localMasterPasswordHash); await _cryptoService.SetPasswordHashAsync(localMasterPasswordHash);
await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString); await _cryptoService.SetMasterKeyEncryptedUserKeyAsync(newProtectedUserKey.EncryptedString);
await _cryptoService.SetEncPrivateKeyAsync(keys.Item2.EncryptedString); await _cryptoService.SetPrivateKeyAsync(keys.Item2.EncryptedString);
if (ResetPasswordAutoEnroll) if (ResetPasswordAutoEnroll)
{ {
// Grab Organization Keys // Grab Organization Keys
var response = await _apiService.GetOrganizationKeysAsync(OrgId); var response = await _apiService.GetOrganizationKeysAsync(OrgId);
var publicKey = CoreHelpers.Base64UrlDecode(response.PublicKey); var publicKey = CoreHelpers.Base64UrlDecode(response.PublicKey);
// Grab user's Encryption Key and encrypt with Org Public Key // Grab User Key and encrypt with Org Public Key
var userEncKey = await _cryptoService.GetEncKeyAsync(); var userKey = await _cryptoService.GetUserKeyAsync();
var encryptedKey = await _cryptoService.RsaEncryptAsync(userEncKey.Key, publicKey); var encryptedKey = await _cryptoService.RsaEncryptAsync(userKey.Key, publicKey);
// Request // Request
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
{ {

View File

@@ -37,6 +37,8 @@ namespace Bit.App.Pages
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync()); Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync());
_vm.UpdateTempPasswordAction = _vm.UpdateTempPasswordAction =
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
_vm.StartDeviceApprovalOptionsAction =
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
_vm.CloseAction = async () => await Navigation.PopModalAsync(); _vm.CloseAction = async () => await Navigation.PopModalAsync();
DuoWebView = _duoWebView; DuoWebView = _duoWebView;
if (Device.RuntimePlatform == Device.Android) if (Device.RuntimePlatform == Device.Android)
@@ -180,6 +182,12 @@ namespace Bit.App.Pages
await Navigation.PushModalAsync(new NavigationPage(page)); await Navigation.PushModalAsync(new NavigationPage(page));
} }
private async Task StartDeviceApprovalOptionsAsync()
{
var page = new LoginApproveDevicePage();
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task TwoFactorAuthSuccessAsync() private async Task TwoFactorAuthSuccessAsync()
{ {
if (_authingWithSso) if (_authingWithSso)

View File

@@ -11,6 +11,7 @@ using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Request; using Bit.Core.Models.Request;
using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Newtonsoft.Json; using Newtonsoft.Json;
using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.ObjectModel;
@@ -33,7 +34,7 @@ namespace Bit.App.Pages
private readonly II18nService _i18nService; private readonly II18nService _i18nService;
private readonly IAppIdService _appIdService; private readonly IAppIdService _appIdService;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
private TwoFactorProviderType? _selectedProviderType; private TwoFactorProviderType? _selectedProviderType;
private string _totpInstruction; private string _totpInstruction;
private string _webVaultUrl = "https://vault.bitwarden.com"; private string _webVaultUrl = "https://vault.bitwarden.com";
@@ -55,6 +56,7 @@ namespace Bit.App.Pages
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService"); _i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService"); _appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_logger = ServiceContainer.Resolve<ILogger>(); _logger = ServiceContainer.Resolve<ILogger>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
PageTitle = AppResources.TwoStepLogin; PageTitle = AppResources.TwoStepLogin;
SubmitCommand = new Command(async () => await SubmitAsync()); SubmitCommand = new Command(async () => await SubmitAsync());
@@ -118,6 +120,7 @@ namespace Bit.App.Pages
public Command SubmitCommand { get; } public Command SubmitCommand { get; }
public ICommand MoreCommand { get; } public ICommand MoreCommand { get; }
public Action TwoFactorAuthSuccessAction { get; set; } public Action TwoFactorAuthSuccessAction { get; set; }
public Action StartDeviceApprovalOptionsAction { get; set; }
public Action StartSetPasswordAction { get; set; } public Action StartSetPasswordAction { get; set; }
public Action CloseAction { get; set; } public Action CloseAction { get; set; }
public Action UpdateTempPasswordAction { get; set; } public Action UpdateTempPasswordAction { get; set; }
@@ -315,6 +318,7 @@ namespace Bit.App.Pages
var task = Task.Run(() => _syncService.FullSyncAsync(true)); var task = Task.Run(() => _syncService.FullSyncAsync(true));
await _deviceActionService.HideLoadingAsync(); await _deviceActionService.HideLoadingAsync();
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
_messagingService.Send("listenYubiKeyOTP", false); _messagingService.Send("listenYubiKeyOTP", false);
_broadcasterService.Unsubscribe(nameof(TwoFactorPage)); _broadcasterService.Unsubscribe(nameof(TwoFactorPage));
@@ -326,6 +330,27 @@ namespace Bit.App.Pages
{ {
UpdateTempPasswordAction?.Invoke(); UpdateTempPasswordAction?.Invoke();
} }
else if (decryptOptions?.TrustedDeviceOption != null)
{
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (!decryptOptions.HasMasterPassword &&
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
{
StartSetPasswordAction?.Invoke();
}
else if (result.ForcePasswordReset)
{
UpdateTempPasswordAction?.Invoke();
}
else if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
{
TwoFactorAuthSuccessAction?.Invoke();
}
else
{
StartDeviceApprovalOptionsAction?.Invoke();
}
}
else else
{ {
TwoFactorAuthSuccessAction?.Invoke(); TwoFactorAuthSuccessAction?.Invoke();

View File

@@ -93,12 +93,12 @@ namespace Bit.App.Pages
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile)); var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
var email = await _stateService.GetEmailAsync(); var email = await _stateService.GetEmailAsync();
// Create new key and hash new password // Create new master key and hash new password
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig); var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, email, kdfConfig);
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key); var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, masterKey);
// Create new encKey for the User // Encrypt user key with new master key
var newEncKey = await _cryptoService.RemakeEncKeyAsync(key); var (userKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(masterKey);
// Initiate API action // Initiate API action
try try
@@ -108,10 +108,10 @@ namespace Bit.App.Pages
switch (_reason) switch (_reason)
{ {
case ForcePasswordResetReason.AdminForcePasswordReset: case ForcePasswordResetReason.AdminForcePasswordReset:
await UpdateTempPasswordAsync(masterPasswordHash, newEncKey.Item2.EncryptedString); await UpdateTempPasswordAsync(masterPasswordHash, newProtectedUserKey.EncryptedString);
break; break;
case ForcePasswordResetReason.WeakMasterPasswordOnLogin: case ForcePasswordResetReason.WeakMasterPasswordOnLogin:
await UpdatePasswordAsync(masterPasswordHash, newEncKey.Item2.EncryptedString); await UpdatePasswordAsync(masterPasswordHash, newProtectedUserKey.EncryptedString);
break; break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();

View File

@@ -27,7 +27,8 @@
Clicked="Clear_Clicked" Clicked="Clear_Clicked"
Order="Secondary" Order="Secondary"
x:Name="_clearItem" x:Name="_clearItem"
x:Key="clearItem" /> x:Key="clearItem"
AutomationId="ClearPasswordList" />
<ToolbarItem Icon="more_vert.png" <ToolbarItem Icon="more_vert.png"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" AutomationProperties.Name="{u:I18n Options}"
@@ -43,7 +44,8 @@
Margin="20, 0" Margin="20, 0"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center"></Label> HorizontalTextAlignment="Center"
AutomationId="NoPasswordsDisplayedLabel"></Label>
<controls:ExtendedCollectionView <controls:ExtendedCollectionView
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}" IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
ItemsSource="{Binding History}" ItemsSource="{Binding History}"
@@ -56,7 +58,8 @@
StyleClass="list-row, list-row-platform" StyleClass="list-row, list-row-platform"
Padding="10" Padding="10"
RowSpacing="0" RowSpacing="0"
ColumnSpacing="10"> ColumnSpacing="10"
AutomationId="GeneratedPasswordRow">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -71,12 +74,14 @@
Grid.Column="0" Grid.Column="0"
Grid.Row="0" Grid.Row="0"
StyleClass="list-title, list-title-platform, text-html" StyleClass="list-title, list-title-platform, text-html"
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}" /> Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}"
AutomationId="GeneratedPasswordValue" />
<Label LineBreakMode="TailTruncation" <Label LineBreakMode="TailTruncation"
Grid.Column="0" Grid.Column="0"
Grid.Row="1" Grid.Row="1"
StyleClass="list-subtitle, list-subtitle-platform" StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}" /> Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}"
AutomationId="GeneratedPasswordDateLabel" />
<controls:IconButton <controls:IconButton
StyleClass="list-row-button, list-row-button-platform" StyleClass="list-row-button, list-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Paste}}" Text="{Binding Source={x:Static core:BitwardenIcons.Paste}}"
@@ -86,7 +91,8 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyPassword}" /> AutomationProperties.Name="{u:I18n CopyPassword}"
AutomationId="CopyPasswordValueButton" />
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</CollectionView.ItemTemplate> </CollectionView.ItemTemplate>

View File

@@ -71,7 +71,8 @@
<Label <Label
Text="{u:I18n PasswordGeneratorPolicyInEffect}" Text="{u:I18n PasswordGeneratorPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold" StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center"
AutomationId="PasswordGeneratorPolicyInEffectLabel" />
</Frame> </Frame>
</Grid> </Grid>
<Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}" <Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}"
@@ -82,21 +83,24 @@
x:Name="lblPassword" x:Name="lblPassword"
StyleClass="text-lg, text-html" StyleClass="text-lg, text-html"
Text="{Binding ColoredPassword, Mode=OneWay}" Text="{Binding ColoredPassword, Mode=OneWay}"
Margin="0, 20" /> Margin="0, 20"
AutomationId="GeneratedPasswordLabel" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}" Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}" Command="{Binding CopyCommand}"
Grid.Column="1" Grid.Column="1"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyPassword}" /> AutomationProperties.Name="{u:I18n CopyPassword}"
AutomationId="CopyValueButton" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}" Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
Command="{Binding RegenerateCommand}" Command="{Binding RegenerateCommand}"
Grid.Column="2" Grid.Column="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GeneratePassword}" /> AutomationProperties.Name="{u:I18n GeneratePassword}"
AutomationId="RegenerateValueButton" />
</Grid> </Grid>
<Grid IsVisible="{Binding IsUsername}" <Grid IsVisible="{Binding IsUsername}"
StyleClass="box-row" StyleClass="box-row"
@@ -107,21 +111,24 @@
StyleClass="text-lg, text-html" StyleClass="text-lg, text-html"
Text="{Binding ColoredUsername, Mode=OneWay}" Text="{Binding ColoredUsername, Mode=OneWay}"
Margin="0, 20" Margin="0, 20"
HorizontalOptions="Start" /> HorizontalOptions="Start"
AutomationId="GeneratedPasswordLabel" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}" Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}" Command="{Binding CopyCommand}"
Grid.Column="1" Grid.Column="1"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyUsername}" /> AutomationProperties.Name="{u:I18n CopyUsername}"
AutomationId="CopyValueButton" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}" Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
Command="{Binding RegenerateUsernameCommand}" Command="{Binding RegenerateUsernameCommand}"
Grid.Column="2" Grid.Column="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GenerateUsername}" /> AutomationProperties.Name="{u:I18n GenerateUsername}"
AutomationId="RegenerateValueButton" />
</Grid> </Grid>
<BoxView StyleClass="box-row-separator"/> <BoxView StyleClass="box-row-separator"/>
<StackLayout StyleClass="box" <StackLayout StyleClass="box"
@@ -135,7 +142,8 @@
ItemsSource="{Binding GeneratorTypeOptions, Mode=OneTime}" ItemsSource="{Binding GeneratorTypeOptions, Mode=OneTime}"
SelectedItem="{Binding GeneratorTypeSelected}" SelectedItem="{Binding GeneratorTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}" ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="GeneratorTypePicker" />
</StackLayout> </StackLayout>
<Label Text="{u:I18n Options, Header=True}" <Label Text="{u:I18n Options, Header=True}"
StyleClass="box-header, box-header-platform" StyleClass="box-header, box-header-platform"
@@ -161,7 +169,8 @@
ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}" ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}"
SelectedItem="{Binding UsernameTypeSelected}" SelectedItem="{Binding UsernameTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}" ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="UsernameTypePicker" />
<Label <Label
StyleClass="box-footer-label" StyleClass="box-footer-label"
Text="{Binding UsernameTypeDescriptionLabel}" /> Text="{Binding UsernameTypeDescriptionLabel}" />
@@ -172,7 +181,8 @@
StyleClass="box-label" /> StyleClass="box-label" />
<Entry x:Name="_plusAddressedEmailEntry" <Entry x:Name="_plusAddressedEmailEntry"
Text="{Binding PlusAddressedEmail}" Text="{Binding PlusAddressedEmail}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="PlusAddressedEmailEntry" />
<Label IsVisible="{Binding ShowUsernameEmailType}" <Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n EmailType}" Text="{u:I18n EmailType}"
StyleClass="box-label" StyleClass="box-label"
@@ -203,7 +213,8 @@
<Entry <Entry
x:Name="_catchAllEmailDomainNameEntry" x:Name="_catchAllEmailDomainNameEntry"
Text="{Binding CatchAllEmailDomain}" Text="{Binding CatchAllEmailDomain}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="CatchAllEmailDomainEntry" />
<Label IsVisible="{Binding ShowUsernameEmailType}" <Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n EmailType}" Text="{u:I18n EmailType}"
StyleClass="box-label" StyleClass="box-label"
@@ -236,26 +247,27 @@
ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}" ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}"
SelectedItem="{Binding ForwardedEmailServiceSelected}" SelectedItem="{Binding ForwardedEmailServiceSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}" ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" /> StyleClass="box-value"
<!--ANONADDY OPTIONS--> AutomationId="ServiceTypePicker" />
<Grid IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}" <Grid
Grid.RowDefinitions="Auto,*" Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto"> Grid.ColumnDefinitions="*,Auto">
<Label <Label
Margin="0,10,0,0" Margin="0,10,0,0"
Text="{u:I18n APIAccessToken}" Text="{Binding ForwardedEmailApiSecretLabel}"
StyleClass="box-label"/> StyleClass="box-label"/>
<Entry <Entry
x:Name="_anonAddyApiAccessTokenEntry" Text="{Binding ForwardedEmailApiSecret}"
Text="{Binding AnonAddyApiAccessToken}" IsPassword="{Binding ShowForwardedEmailApiSecret, Converter={StaticResource inverseBool}}"
IsPassword="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool}}" Grid.Row="1"
Grid.Row="1"/> AutomationId="ForwardedEmailApiSecretEntry" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}" Text="{Binding ShowForwardedEmailApiSecret, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}" Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1" Grid.Row="1"
Grid.Column="1"/> Grid.Column="1"
AutomationId="ShowForwardedEmailApiSecretButton" />
</Grid> </Grid>
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}" <Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
Text="{u:I18n DomainNameRequiredParenthesis}" Text="{u:I18n DomainNameRequiredParenthesis}"
@@ -264,91 +276,8 @@
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}" <Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
x:Name="_anonAddyDomainNameEntry" x:Name="_anonAddyDomainNameEntry"
Text="{Binding AnonAddyDomainName}" Text="{Binding AnonAddyDomainName}"
StyleClass="box-value"/>
<!--FIREFOX RELAY OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.FirefoxRelay}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIAccessToken}"
StyleClass="box-label"/>
<Entry
x:Name="_firefoxRelayApiAccessTokenEntry"
Text="{Binding FirefoxRelayApiAccessToken}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" AutomationId="AnonAddyDomainNameEntry" />
IsPassword="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
<!--SIMPLELOGIN OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.SimpleLogin}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIKeyRequiredParenthesis}"
StyleClass="box-label"/>
<Entry
x:Name="_simpleLoginApiKeyEntry"
Text="{Binding SimpleLoginApiKey}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
<!--DUCKDUCKGO OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.DuckDuckGo}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIKeyRequiredParenthesis}"
StyleClass="box-label"/>
<Entry
x:Name="_duckDuckGoApiAccessTokenEntry"
Text="{Binding DuckDuckGoApiKey}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowDuckDuckGoApiKey, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowDuckDuckGoApiKey, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
<!--FASTMAIL OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.Fastmail}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIKeyRequiredParenthesis}"
StyleClass="box-label"/>
<Entry
x:Name="_fastmailApiAccessTokenEntry"
Text="{Binding FastmailApiKey}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowFastmailApiKey, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowFastmailApiKey, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
</StackLayout> </StackLayout>
<!--RANDOM WORD OPTIONS--> <!--RANDOM WORD OPTIONS-->
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"> <Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
@@ -359,7 +288,8 @@
<Switch <Switch
IsToggled="{Binding CapitalizeRandomWordUsername}" IsToggled="{Binding CapitalizeRandomWordUsername}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End"
AutomationId="CapitalizeRandomWordUsernameToggle" />
</Grid> </Grid>
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}" <BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
StyleClass="box-row-separator" /> StyleClass="box-row-separator" />
@@ -371,7 +301,8 @@
<Switch <Switch
IsToggled="{Binding IncludeNumberRandomWordUsername}" IsToggled="{Binding IncludeNumberRandomWordUsername}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End"
AutomationId="IncludeNumberRandomWordUsernameToggle" />
</Grid> </Grid>
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}" <BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
StyleClass="box-row-separator" /> StyleClass="box-row-separator" />
@@ -386,7 +317,8 @@
x:Name="_passwordTypePicker" x:Name="_passwordTypePicker"
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}" ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding PasswordTypeSelectedIndex}" SelectedIndex="{Binding PasswordTypeSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="PasswordTypePicker" />
</StackLayout> </StackLayout>
<StackLayout Spacing="0" <StackLayout Spacing="0"
Padding="0" Padding="0"
@@ -403,12 +335,14 @@
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="End" HorizontalTextAlignment="End"
VerticalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center" /> VerticalTextAlignment="Center"
AutomationId="NumberOfWordsLabel" />
<controls:ExtendedStepper <controls:ExtendedStepper
Value="{Binding NumWords}" Value="{Binding NumWords}"
Maximum="20" Maximum="20"
Minimum="3" Minimum="3"
Increment="1" /> Increment="1"
AutomationId="NumberOfWordsStepper" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
@@ -419,7 +353,8 @@
Text="{Binding WordSeparator}" Text="{Binding WordSeparator}"
IsSpellCheckEnabled="False" IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False" IsTextPredictionEnabled="False"
StyleClass="box-value"> StyleClass="box-value"
AutomationId="WordSeparatorEntry">
<Entry.Effects> <Entry.Effects>
<effects:NoEmojiKeyboardEffect /> <effects:NoEmojiKeyboardEffect />
</Entry.Effects> </Entry.Effects>
@@ -435,7 +370,8 @@
IsEnabled="{Binding EnforcedPolicyOptions.Capitalize, IsEnabled="{Binding EnforcedPolicyOptions.Capitalize,
Converter={StaticResource inverseBool}}" Converter={StaticResource inverseBool}}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End"
AutomationId="CapitalizePassphraseToggle" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout StyleClass="box-row, box-row-switch">
@@ -448,7 +384,8 @@
IsEnabled="{Binding EnforcedPolicyOptions.IncludeNumber, IsEnabled="{Binding EnforcedPolicyOptions.IncludeNumber,
Converter={StaticResource inverseBool}}" Converter={StaticResource inverseBool}}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End"
AutomationId="IncludeNumbersToggle" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding IsPassword}"> <StackLayout Spacing="0" Padding="0" IsVisible="{Binding IsPassword}">
@@ -462,7 +399,8 @@
StyleClass="box-sub-label" StyleClass="box-sub-label"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
HorizontalTextAlignment="End" HorizontalTextAlignment="End"
WidthRequest="50" /> WidthRequest="50"
AutomationId="PasswordLengthLabel" />
<controls:ExtendedSlider <controls:ExtendedSlider
DragCompleted="LengthSlider_DragCompleted" DragCompleted="LengthSlider_DragCompleted"
Value="{Binding Length}" Value="{Binding Length}"
@@ -471,7 +409,8 @@
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
Maximum="128" Maximum="128"
Minimum="5" /> Minimum="5"
AutomationId="PasswordLengthSlider" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout StyleClass="box-row, box-row-switch">
@@ -488,7 +427,8 @@
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" HorizontalOptions="End"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n UppercaseAtoZ}"/> AutomationProperties.Name="{u:I18n UppercaseAtoZ}"
AutomationId="UppercaseAtoZToggle" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout StyleClass="box-row, box-row-switch">
@@ -505,7 +445,8 @@
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" HorizontalOptions="End"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n LowercaseAtoZ}"/> AutomationProperties.Name="{u:I18n LowercaseAtoZ}"
AutomationId="LowercaseAtoZToggle" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout StyleClass="box-row, box-row-switch">
@@ -522,7 +463,8 @@
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" HorizontalOptions="End"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n NumbersZeroToNine}"/> AutomationProperties.Name="{u:I18n NumbersZeroToNine}"
AutomationId="NumbersZeroToNineToggle" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout StyleClass="box-row, box-row-switch">
@@ -539,7 +481,8 @@
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" HorizontalOptions="End"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n SpecialCharacters}"/> AutomationProperties.Name="{u:I18n SpecialCharacters}"
AutomationId="SpecialCharactersToggle" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-stepper"> <StackLayout StyleClass="box-row, box-row-stepper">
@@ -554,12 +497,14 @@
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="End" HorizontalTextAlignment="End"
VerticalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center" /> VerticalTextAlignment="Center"
AutomationId="MinNumberValueLabel" />
<controls:ExtendedStepper <controls:ExtendedStepper
Value="{Binding MinNumber}" Value="{Binding MinNumber}"
Maximum="5" Maximum="5"
Minimum="0" Minimum="0"
Increment="1" /> Increment="1"
AutomationId="MinNumberStepper" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-stepper"> <StackLayout StyleClass="box-row, box-row-stepper">
@@ -574,12 +519,14 @@
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="End" HorizontalTextAlignment="End"
VerticalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center" /> VerticalTextAlignment="Center"
AutomationId="MinSpecialValueLabel" />
<controls:ExtendedStepper <controls:ExtendedStepper
Value="{Binding MinSpecial}" Value="{Binding MinSpecial}"
Maximum="5" Maximum="5"
Minimum="0" Minimum="0"
Increment="1" /> Increment="1"
AutomationId="MinSpecialStepper" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout StyleClass="box-row, box-row-switch">
@@ -590,7 +537,8 @@
<Switch <Switch
IsToggled="{Binding AvoidAmbiguousChars}" IsToggled="{Binding AvoidAmbiguousChars}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End"
AutomationId="AvoidAmbiguousCharsToggle" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
</StackLayout> </StackLayout>

View File

@@ -8,6 +8,7 @@ using Bit.App.Utilities;
using Bit.Core; using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.ObjectModel;
@@ -23,7 +24,7 @@ namespace Bit.App.Pages
private readonly IUsernameGenerationService _usernameGenerationService; private readonly IUsernameGenerationService _usernameGenerationService;
private readonly ITokenService _tokenService; private readonly ITokenService _tokenService;
private readonly IDeviceActionService _deviceActionService; private readonly IDeviceActionService _deviceActionService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger"); readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
private PasswordGenerationOptions _options; private PasswordGenerationOptions _options;
private UsernameGenerationOptions _usernameOptions; private UsernameGenerationOptions _usernameOptions;
@@ -49,11 +50,7 @@ namespace Bit.App.Pages
private bool _doneIniting; private bool _doneIniting;
private bool _showTypePicker; private bool _showTypePicker;
private string _emailWebsite; private string _emailWebsite;
private bool _showFirefoxRelayApiAccessToken; private bool _showForwardedEmailApiSecret;
private bool _showAnonAddyApiAccessToken;
private bool _showSimpleLoginApiKey;
private bool _showDuckDuckGoApiKey;
private bool _showFastmailApiKey;
private bool _editMode; private bool _editMode;
public GeneratorPageViewModel() public GeneratorPageViewModel()
@@ -96,7 +93,7 @@ namespace Bit.App.Pages
UsernameTypePromptHelpCommand = new Command(UsernameTypePromptHelp); UsernameTypePromptHelpCommand = new Command(UsernameTypePromptHelp);
RegenerateCommand = new AsyncCommand(RegenerateAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false); RegenerateCommand = new AsyncCommand(RegenerateAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
RegenerateUsernameCommand = new AsyncCommand(RegenerateUsernameAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false); RegenerateUsernameCommand = new AsyncCommand(RegenerateUsernameAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
ToggleForwardedEmailHiddenValueCommand = new AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false); ToggleForwardedEmailHiddenValueCommand = new Command(() => ShowForwardedEmailApiSecret = !ShowForwardedEmailApiSecret);
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false); CopyCommand = new AsyncCommand(CopyAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
CloseCommand = new AsyncCommand(CloseAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false); CloseCommand = new AsyncCommand(CloseAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
} }
@@ -415,7 +412,6 @@ namespace Bit.App.Pages
public string UsernameTypeDescriptionLabel => GetUsernameTypeLabelDescription(UsernameTypeSelected); public string UsernameTypeDescriptionLabel => GetUsernameTypeLabelDescription(UsernameTypeSelected);
public ForwardedEmailServiceType ForwardedEmailServiceSelected public ForwardedEmailServiceType ForwardedEmailServiceSelected
{ {
get => _usernameOptions.ServiceType; get => _usernameOptions.ServiceType;
@@ -425,7 +421,11 @@ namespace Bit.App.Pages
{ {
_usernameOptions.ServiceType = value; _usernameOptions.ServiceType = value;
Username = Constants.DefaultUsernameGenerated; Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected)); TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected), new string[]
{
nameof(ForwardedEmailApiSecret),
nameof(ForwardedEmailApiSecretLabel)
});
SaveUsernameOptionsAsync(false).FireAndForget(); SaveUsernameOptionsAsync(false).FireAndForget();
} }
} }
@@ -445,27 +445,104 @@ namespace Bit.App.Pages
} }
} }
public string AnonAddyApiAccessToken public string ForwardedEmailApiSecret
{ {
get => _usernameOptions.AnonAddyApiAccessToken; get
{
switch (ForwardedEmailServiceSelected)
{
case ForwardedEmailServiceType.AnonAddy:
return _usernameOptions.AnonAddyApiAccessToken;
case ForwardedEmailServiceType.DuckDuckGo:
return _usernameOptions.DuckDuckGoApiKey;
case ForwardedEmailServiceType.Fastmail:
return _usernameOptions.FastMailApiKey;
case ForwardedEmailServiceType.FirefoxRelay:
return _usernameOptions.FirefoxRelayApiAccessToken;
case ForwardedEmailServiceType.SimpleLogin:
return _usernameOptions.SimpleLoginApiKey;
default:
return null;
}
}
set set
{ {
bool changed = false;
switch (ForwardedEmailServiceSelected)
{
case ForwardedEmailServiceType.AnonAddy:
if (_usernameOptions.AnonAddyApiAccessToken != value) if (_usernameOptions.AnonAddyApiAccessToken != value)
{ {
_usernameOptions.AnonAddyApiAccessToken = value; _usernameOptions.AnonAddyApiAccessToken = value;
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken)); changed = true;
}
break;
case ForwardedEmailServiceType.DuckDuckGo:
if (_usernameOptions.DuckDuckGoApiKey != value)
{
_usernameOptions.DuckDuckGoApiKey = value;
changed = true;
}
break;
case ForwardedEmailServiceType.Fastmail:
if (_usernameOptions.FastMailApiKey != value)
{
_usernameOptions.FastMailApiKey = value;
changed = true;
}
break;
case ForwardedEmailServiceType.FirefoxRelay:
if (_usernameOptions.FirefoxRelayApiAccessToken != value)
{
_usernameOptions.FirefoxRelayApiAccessToken = value;
changed = true;
}
break;
case ForwardedEmailServiceType.SimpleLogin:
if (_usernameOptions.SimpleLoginApiKey != value)
{
_usernameOptions.SimpleLoginApiKey = value;
changed = true;
}
break;
default:
break;
}
if (changed)
{
TriggerPropertyChanged(nameof(ForwardedEmailApiSecret));
SaveUsernameOptionsAsync(false).FireAndForget(); SaveUsernameOptionsAsync(false).FireAndForget();
} }
} }
} }
public bool ShowAnonAddyApiAccessToken public string ForwardedEmailApiSecretLabel
{ {
get get
{ {
return _showAnonAddyApiAccessToken; switch (ForwardedEmailServiceSelected)
{
case ForwardedEmailServiceType.AnonAddy:
case ForwardedEmailServiceType.FirefoxRelay:
return AppResources.APIAccessToken;
case ForwardedEmailServiceType.DuckDuckGo:
case ForwardedEmailServiceType.Fastmail:
case ForwardedEmailServiceType.SimpleLogin:
return AppResources.APIKeyRequiredParenthesis;
default:
return null;
} }
set => SetProperty(ref _showAnonAddyApiAccessToken, value); }
}
public bool ShowForwardedEmailApiSecret
{
get
{
return _showForwardedEmailApiSecret;
}
set => SetProperty(ref _showForwardedEmailApiSecret, value);
} }
public string AnonAddyDomainName public string AnonAddyDomainName
@@ -482,99 +559,6 @@ namespace Bit.App.Pages
} }
} }
public string FirefoxRelayApiAccessToken
{
get => _usernameOptions.FirefoxRelayApiAccessToken;
set
{
if (_usernameOptions.FirefoxRelayApiAccessToken != value)
{
_usernameOptions.FirefoxRelayApiAccessToken = value;
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowFirefoxRelayApiAccessToken
{
get
{
return _showFirefoxRelayApiAccessToken;
}
set => SetProperty(ref _showFirefoxRelayApiAccessToken, value);
}
public string SimpleLoginApiKey
{
get => _usernameOptions.SimpleLoginApiKey;
set
{
if (_usernameOptions.SimpleLoginApiKey != value)
{
_usernameOptions.SimpleLoginApiKey = value;
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowSimpleLoginApiKey
{
get
{
return _showSimpleLoginApiKey;
}
set => SetProperty(ref _showSimpleLoginApiKey, value);
}
public string DuckDuckGoApiKey
{
get => _usernameOptions.DuckDuckGoApiKey;
set
{
if (_usernameOptions.DuckDuckGoApiKey != value)
{
_usernameOptions.DuckDuckGoApiKey = value;
TriggerPropertyChanged(nameof(DuckDuckGoApiKey));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowDuckDuckGoApiKey
{
get
{
return _showDuckDuckGoApiKey;
}
set => SetProperty(ref _showDuckDuckGoApiKey, value);
}
public string FastmailApiKey
{
get => _usernameOptions.FastMailApiKey;
set
{
if (_usernameOptions.FastMailApiKey != value)
{
_usernameOptions.FastMailApiKey = value;
TriggerPropertyChanged(nameof(FastmailApiKey));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowFastmailApiKey
{
get
{
return _showFastmailApiKey;
}
set => SetProperty(ref _showFastmailApiKey, value);
}
public bool CapitalizeRandomWordUsername public bool CapitalizeRandomWordUsername
{ {
get => _usernameOptions.CapitalizeRandomWordUsername; get => _usernameOptions.CapitalizeRandomWordUsername;
@@ -807,12 +791,9 @@ namespace Bit.App.Pages
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected)); TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername)); TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername)); TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
TriggerPropertyChanged(nameof(SimpleLoginApiKey)); TriggerPropertyChanged(nameof(ForwardedEmailApiSecret));
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken)); TriggerPropertyChanged(nameof(ForwardedEmailApiSecretLabel));
TriggerPropertyChanged(nameof(AnonAddyDomainName)); TriggerPropertyChanged(nameof(AnonAddyDomainName));
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
TriggerPropertyChanged(nameof(DuckDuckGoApiKey));
TriggerPropertyChanged(nameof(FastmailApiKey));
TriggerPropertyChanged(nameof(CatchAllEmailDomain)); TriggerPropertyChanged(nameof(CatchAllEmailDomain));
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected)); TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
TriggerPropertyChanged(nameof(UsernameTypeSelected)); TriggerPropertyChanged(nameof(UsernameTypeSelected));
@@ -845,17 +826,25 @@ namespace Bit.App.Pages
{ {
_logger.Value.Exception(ex); _logger.Value.Exception(ex);
string message = AppResources.GenericErrorMessage;
if (IsUsername && UsernameTypeSelected == UsernameType.ForwardedEmailAlias) if (IsUsername && UsernameTypeSelected == UsernameType.ForwardedEmailAlias)
{ {
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert( if (ex is ForwardedEmailInvalidSecretException)
AppResources.AnErrorHasOccurred, string.Format(AppResources.UnknownXErrorMessage, ForwardedEmailServiceSelected), AppResources.Ok)); {
message = ForwardedEmailServiceSelected == ForwardedEmailServiceType.AnonAddy || ForwardedEmailServiceSelected == ForwardedEmailServiceType.FirefoxRelay
? AppResources.InvalidAPIToken
: AppResources.InvalidAPIKey;
} }
else else
{ {
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok)); message = string.Format(AppResources.UnknownXErrorMessage, ForwardedEmailServiceSelected);
} }
} }
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, message, AppResources.Ok));
}
private string GetUsernameTypeLabelDescription(UsernameType value) private string GetUsernameTypeLabelDescription(UsernameType value)
{ {
switch (value) switch (value)
@@ -870,27 +859,5 @@ namespace Bit.App.Pages
return string.Empty; return string.Empty;
} }
} }
private async Task ToggleForwardedEmailHiddenValueAsync()
{
switch (ForwardedEmailServiceSelected)
{
case ForwardedEmailServiceType.AnonAddy:
ShowAnonAddyApiAccessToken = !ShowAnonAddyApiAccessToken;
break;
case ForwardedEmailServiceType.FirefoxRelay:
ShowFirefoxRelayApiAccessToken = !ShowFirefoxRelayApiAccessToken;
break;
case ForwardedEmailServiceType.SimpleLogin:
ShowSimpleLoginApiKey = !ShowSimpleLoginApiKey;
break;
case ForwardedEmailServiceType.DuckDuckGo:
ShowDuckDuckGoApiKey = !ShowDuckDuckGoApiKey;
break;
case ForwardedEmailServiceType.Fastmail:
ShowFastmailApiKey = !ShowFastmailApiKey;
break;
}
}
} }
} }

View File

@@ -71,7 +71,8 @@
<Label <Label
Text="{u:I18n SendDisabledWarning}" Text="{u:I18n SendDisabledWarning}"
StyleClass="text-muted, text-sm, text-bold" StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center"
AutomationId="SendDisabledWarningMessageLabel" />
</Frame> </Frame>
<Frame <Frame
IsVisible="{Binding SendOptionsPolicyInEffect}" IsVisible="{Binding SendOptionsPolicyInEffect}"
@@ -83,7 +84,8 @@
<Label <Label
Text="{u:I18n SendOptionsPolicyInEffect}" Text="{u:I18n SendOptionsPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold" StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center"
AutomationId="SendOptionsPolicyInEffectLabel" />
</Frame> </Frame>
<StackLayout StyleClass="box-row"> <StackLayout StyleClass="box-row">
<Label <Label
@@ -93,7 +95,8 @@
x:Name="_nameEntry" x:Name="_nameEntry"
Text="{Binding Send.Name}" Text="{Binding Send.Name}"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="SendNameEntry" />
<Label <Label
Text="{u:I18n NameInfo}" Text="{u:I18n NameInfo}"
StyleClass="box-footer-label" StyleClass="box-footer-label"
@@ -123,6 +126,7 @@
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n File}" AutomationProperties.Name="{u:I18n File}"
AutomationProperties.HelpText="{Binding FileTypeAccessibilityLabel}" AutomationProperties.HelpText="{Binding FileTypeAccessibilityLabel}"
AutomationId="SendFileButton"
Grid.Column="0"> Grid.Column="0">
</Button> </Button>
<Button <Button
@@ -135,6 +139,7 @@
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Text}" AutomationProperties.Name="{u:I18n Text}"
AutomationProperties.HelpText="{Binding TextTypeAccessibilityLabel}" AutomationProperties.HelpText="{Binding TextTypeAccessibilityLabel}"
AutomationId="SendTextButton"
Grid.Column="1"> Grid.Column="1">
</Button> </Button>
</Grid> </Grid>
@@ -152,12 +157,14 @@
Text="{Binding Send.File.FileName, Mode=OneWay}" Text="{Binding Send.File.FileName, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
VerticalTextAlignment="Center" VerticalTextAlignment="Center"
HorizontalOptions="StartAndExpand" /> HorizontalOptions="StartAndExpand"
AutomationId="SendFileNameLabel" />
<Label <Label
Text="{Binding Send.File.SizeName, Mode=OneWay}" Text="{Binding Send.File.SizeName, Mode=OneWay}"
StyleClass="box-sub-label" StyleClass="box-sub-label"
HorizontalTextAlignment="End" HorizontalTextAlignment="End"
VerticalTextAlignment="Center" /> VerticalTextAlignment="Center"
AutomationId="SendFileSizeLabel" />
</StackLayout> </StackLayout>
<StackLayout <StackLayout
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}" IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
@@ -168,20 +175,23 @@
LineBreakMode="CharacterWrap" LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted" StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center"
AutomationId="SendNoFileChosenLabel" />
<Label <Label
IsVisible="{Binding FileName, Converter={StaticResource notNull}}" IsVisible="{Binding FileName, Converter={StaticResource notNull}}"
Text="{Binding FileName}" Text="{Binding FileName}"
LineBreakMode="CharacterWrap" LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted" StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center"
AutomationId="SendCurrentFileNameLabel" />
<Button <Button
Text="{u:I18n ChooseFile}" Text="{u:I18n ChooseFile}"
IsVisible="{Binding IsAddFromShare, Converter={StaticResource inverseBool}}" IsVisible="{Binding IsAddFromShare, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
StyleClass="box-button-row" StyleClass="box-button-row"
Clicked="ChooseFile_Clicked" /> Clicked="ChooseFile_Clicked"
AutomationId="SendChooseFileButton" />
<Label <Label
Margin="0, 5, 0, 0" Margin="0, 5, 0, 0"
Text="{u:I18n MaxFileSize}" Text="{u:I18n MaxFileSize}"
@@ -208,6 +218,7 @@
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
StyleClass="box-value" StyleClass="box-value"
Margin="{Binding EditorMargins}" Margin="{Binding EditorMargins}"
AutomationId="SendTextContentEntry"
effects:ScrollEnabledEffect.IsScrollEnabled="false" > effects:ScrollEnabledEffect.IsScrollEnabled="false" >
<Editor.Behaviors> <Editor.Behaviors>
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" /> <behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
@@ -235,7 +246,8 @@
IsToggled="{Binding Send.Text.Hidden}" IsToggled="{Binding Send.Text.Hidden}"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End" HorizontalOptions="End"
Margin="10,0,0,0" /> Margin="10,0,0,0"
AutomationId="SendHideTextByDefaultToggle" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
<StackLayout <StackLayout
@@ -249,7 +261,8 @@
IsToggled="{Binding ShareOnSave}" IsToggled="{Binding ShareOnSave}"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End" HorizontalOptions="End"
Margin="10,0,0,0" /> Margin="10,0,0,0"
AutomationId="SendShareSendAfterSaveToggle" />
</StackLayout> </StackLayout>
<StackLayout <StackLayout
Orientation="Horizontal" Orientation="Horizontal"
@@ -263,21 +276,24 @@
StyleClass="box-row-button" StyleClass="box-row-button"
TextColor="{DynamicResource PrimaryColor}" TextColor="{DynamicResource PrimaryColor}"
Margin="0" Margin="0"
AutomationProperties.IsInAccessibleTree="False"/> AutomationProperties.IsInAccessibleTree="False"
AutomationId="SendShowHideOptionsButton" />
<controls:IconButton <controls:IconButton
x:Name="_btnOptionsUp" x:Name="_btnOptionsUp"
Text="{Binding Source={x:Static core:BitwardenIcons.ChevronUp}}" Text="{Binding Source={x:Static core:BitwardenIcons.ChevronUp}}"
StyleClass="box-row-button" StyleClass="box-row-button"
TextColor="{DynamicResource PrimaryColor}" TextColor="{DynamicResource PrimaryColor}"
IsVisible="{Binding ShowOptions}" IsVisible="{Binding ShowOptions}"
AutomationProperties.IsInAccessibleTree="False"/> AutomationProperties.IsInAccessibleTree="False"
AutomationId="SendOptionsDisplayed" />
<controls:IconButton <controls:IconButton
x:Name="_btnOptionsDown" x:Name="_btnOptionsDown"
Text="{Binding Source={x:Static core:BitwardenIcons.AngleDown}}" Text="{Binding Source={x:Static core:BitwardenIcons.AngleDown}}"
StyleClass="box-row-button" StyleClass="box-row-button"
TextColor="{DynamicResource PrimaryColor}" TextColor="{DynamicResource PrimaryColor}"
IsVisible="{Binding ShowOptions, Converter={StaticResource inverseBool}}" IsVisible="{Binding ShowOptions, Converter={StaticResource inverseBool}}"
AutomationProperties.IsInAccessibleTree="False"/> AutomationProperties.IsInAccessibleTree="False"
AutomationId="SendOptionsHidden" />
</StackLayout> </StackLayout>
<StackLayout IsVisible="{Binding ShowOptions}"> <StackLayout IsVisible="{Binding ShowOptions}">
<StackLayout <StackLayout
@@ -294,7 +310,8 @@
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
StyleClass="box-value" StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionTime}" /> AutomationProperties.Name="{u:I18n DeletionTime}"
AutomationId="SendDeletionOptionsPicker" />
<Grid <Grid
IsVisible="{Binding ShowDeletionCustomPickers}" IsVisible="{Binding ShowDeletionCustomPickers}"
Margin="0,5,0,0"> Margin="0,5,0,0">
@@ -308,14 +325,16 @@
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionDate}" AutomationProperties.Name="{u:I18n DeletionDate}"
Grid.Column="0" /> Grid.Column="0"
AutomationId="SendCustomDeletionDatePicker" />
<controls:ExtendedTimePicker <controls:ExtendedTimePicker
NullableTime="{Binding DeletionDateTimeViewModel.Time, Mode=TwoWay}" NullableTime="{Binding DeletionDateTimeViewModel.Time, Mode=TwoWay}"
Format="t" Format="t"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionTime}" AutomationProperties.Name="{u:I18n DeletionTime}"
Grid.Column="1" /> Grid.Column="1"
AutomationId="SendCustomDeletionTimePicker" />
</Grid> </Grid>
<Label <Label
Text="{u:I18n DeletionDateInfo}" Text="{u:I18n DeletionDateInfo}"
@@ -334,7 +353,8 @@
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
StyleClass="box-value" StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ExpirationTime}" /> AutomationProperties.Name="{u:I18n ExpirationTime}"
AutomationId="SendExpirationOptionsPicker" />
<Grid <Grid
IsVisible="{Binding ShowExpirationCustomPickers}" IsVisible="{Binding ShowExpirationCustomPickers}"
Margin="0,5,0,0"> Margin="0,5,0,0">
@@ -349,7 +369,8 @@
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ExpirationDate}" AutomationProperties.Name="{u:I18n ExpirationDate}"
Grid.Column="0" /> Grid.Column="0"
AutomationId="SendCustomExpirationDatePicker" />
<controls:ExtendedTimePicker <controls:ExtendedTimePicker
NullableTime="{Binding ExpirationDateTimeViewModel.Time, Mode=TwoWay}" NullableTime="{Binding ExpirationDateTimeViewModel.Time, Mode=TwoWay}"
PlaceHolder="--:-- --" PlaceHolder="--:-- --"
@@ -357,7 +378,8 @@
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ExpirationTime}" AutomationProperties.Name="{u:I18n ExpirationTime}"
Grid.Column="1" /> Grid.Column="1"
AutomationId="SendCustomExpirationTimePicker" />
</Grid> </Grid>
<StackLayout <StackLayout
Orientation="Horizontal" Orientation="Horizontal"
@@ -374,7 +396,8 @@
FontSize="{Binding SegmentedButtonFontSize}" FontSize="{Binding SegmentedButtonFontSize}"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
StyleClass="box-row-button" StyleClass="box-row-button"
Clicked="ClearExpirationDate_Clicked" /> Clicked="ClearExpirationDate_Clicked"
AutomationId="SendClearExpirationDateButton" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
<StackLayout <StackLayout
@@ -393,13 +416,15 @@
Keyboard="Numeric" Keyboard="Numeric"
MaxLength="9" MaxLength="9"
TextChanged="OnMaxAccessCountTextChanged" TextChanged="OnMaxAccessCountTextChanged"
HorizontalOptions="FillAndExpand" /> HorizontalOptions="FillAndExpand"
AutomationId="SendMaxAccessCountEntry" />
<controls:ExtendedStepper <controls:ExtendedStepper
x:Name="_maxAccessCountStepper" x:Name="_maxAccessCountStepper"
Value="{Binding MaxAccessCount}" Value="{Binding MaxAccessCount}"
Maximum="999999999" Maximum="999999999"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
Margin="10,0,0,0" /> Margin="10,0,0,0"
AutomationId="SendMaxAccessCountStepper" />
</StackLayout> </StackLayout>
<Label <Label
Text="{u:I18n MaximumAccessCountInfo}" Text="{u:I18n MaximumAccessCountInfo}"
@@ -419,7 +444,8 @@
<Label <Label
Text="{Binding Send.AccessCount, Mode=OneWay}" Text="{Binding Send.AccessCount, Mode=OneWay}"
StyleClass="box-label" StyleClass="box-label"
VerticalTextAlignment="Center" /> VerticalTextAlignment="Center"
AutomationId="SendCurrentAccessCountLabel" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
<StackLayout <StackLayout
@@ -436,7 +462,8 @@
StyleClass="box-value" StyleClass="box-value"
IsSpellCheckEnabled="False" IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False" IsTextPredictionEnabled="False"
HorizontalOptions="FillAndExpand" /> HorizontalOptions="FillAndExpand"
AutomationId="SendNewPasswordEntry" />
<controls:IconButton <controls:IconButton
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
@@ -445,7 +472,8 @@
Margin="10,0,0,0" Margin="10,0,0,0"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" /> AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
AutomationId="SendShowHidePasswordButton" />
</StackLayout> </StackLayout>
<Label <Label
Text="{u:I18n PasswordInfo}" Text="{u:I18n PasswordInfo}"
@@ -464,7 +492,8 @@
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
StyleClass="box-value" StyleClass="box-value"
Margin="{Binding EditorMargins}" Margin="{Binding EditorMargins}"
effects:ScrollEnabledEffect.IsScrollEnabled="false" > effects:ScrollEnabledEffect.IsScrollEnabled="false"
AutomationId="SendNotesEntry">
<Editor.Behaviors> <Editor.Behaviors>
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" /> <behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
</Editor.Behaviors> </Editor.Behaviors>
@@ -492,7 +521,8 @@
IsToggled="{Binding Send.HideEmail}" IsToggled="{Binding Send.HideEmail}"
IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}" IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}"
HorizontalOptions="End" HorizontalOptions="End"
Margin="10,0,0,0" /> Margin="10,0,0,0"
AutomationId="SendHideEmailSwitch" />
</StackLayout> </StackLayout>
<StackLayout <StackLayout
StyleClass="box-row, box-row-switch" StyleClass="box-row, box-row-switch"
@@ -506,7 +536,8 @@
IsToggled="{Binding Send.Disabled}" IsToggled="{Binding Send.Disabled}"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End" HorizontalOptions="End"
Margin="10,0,0,0" /> Margin="10,0,0,0"
AutomationId="SendDeactivateSwitch" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>

View File

@@ -25,7 +25,8 @@
Priority="-2" Priority="-2"
UseOriginalImage="True" UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" /> AutomationProperties.Name="{u:I18n Account}"
AutomationId="AccountIconButton" />
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" /> <ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" />
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/> <ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/>
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>

View File

@@ -44,13 +44,15 @@
<controls:SendViewCell <controls:SendViewCell
Send="{Binding Send}" Send="{Binding Send}"
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}" ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}"
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}" /> ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}"
AutomationId="SendCell" />
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="sendGroupTemplate" <DataTemplate x:Key="sendGroupTemplate"
x:DataType="pages:SendGroupingsPageListItem"> x:DataType="pages:SendGroupingsPageListItem">
<controls:ExtendedStackLayout Orientation="Horizontal" <controls:ExtendedStackLayout Orientation="Horizontal"
StyleClass="list-row, list-row-platform"> StyleClass="list-row, list-row-platform"
AutomationId="{Binding AutomationId}">
<controls:IconLabel Text="{Binding Icon, Mode=OneWay}" <controls:IconLabel Text="{Binding Icon, Mode=OneWay}"
HorizontalOptions="Start" HorizontalOptions="Start"
VerticalOptions="Center" VerticalOptions="Center"
@@ -64,12 +66,14 @@
LineBreakMode="TailTruncation" LineBreakMode="TailTruncation"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
StyleClass="list-title" /> StyleClass="list-title"
AutomationId="SendFilterNameLabel" />
<Label Text="{Binding ItemCount, Mode=OneWay}" <Label Text="{Binding ItemCount, Mode=OneWay}"
HorizontalOptions="End" HorizontalOptions="End"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
HorizontalTextAlignment="End" HorizontalTextAlignment="End"
StyleClass="list-sub" /> StyleClass="list-sub"
AutomationId="SendFilterCountLabel" />
</controls:ExtendedStackLayout> </controls:ExtendedStackLayout>
</DataTemplate> </DataTemplate>

View File

@@ -66,5 +66,27 @@ namespace Bit.App.Pages
return _icon; return _icon;
} }
} }
public string AutomationId
{
get
{
if (_name != null)
{
return "SendItem";
}
if (Type != null)
{
switch (Type.Value)
{
case SendType.Text:
return "SendTextFilter";
case SendType.File:
return "SendFileFilter";
}
}
return null;
}
}
} }
} }

View File

@@ -59,7 +59,8 @@
Margin="20, 0" Margin="20, 0"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center"
AutomationId="NoSendDisplayedLabel" />
<controls:ExtendedCollectionView <controls:ExtendedCollectionView
IsVisible="{Binding ShowList}" IsVisible="{Binding ShowList}"
ItemsSource="{Binding Sends}" ItemsSource="{Binding Sends}"
@@ -67,13 +68,15 @@
SelectionMode="Single" SelectionMode="Single"
SelectionChanged="RowSelected" SelectionChanged="RowSelected"
StyleClass="list, list-platform" StyleClass="list, list-platform"
ExtraDataForLogging="Sends Page"> ExtraDataForLogging="Sends Page"
AutomationId="SendCellList">
<CollectionView.ItemTemplate> <CollectionView.ItemTemplate>
<DataTemplate x:DataType="views:SendView"> <DataTemplate x:DataType="views:SendView">
<controls:SendViewCell <controls:SendViewCell
Send="{Binding .}" Send="{Binding .}"
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}" ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}"
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}" /> ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}"
AutomationId="SendCell" />
</DataTemplate> </DataTemplate>
</CollectionView.ItemTemplate> </CollectionView.ItemTemplate>
</controls:ExtendedCollectionView> </controls:ExtendedCollectionView>

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.BlockAutofillUrisPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:BlockAutofillUrisPageViewModel"
Title="{u:I18n BlockAutoFill}">
<ContentPage.BindingContext>
<pages:BlockAutofillUrisPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout Orientation="Vertical">
<Image
x:Name="_emptyUrisPlaceholder"
HorizontalOptions="Center"
WidthRequest="120"
HeightRequest="120"
Margin="0,100,0,0"
IsVisible="{Binding ShowList, Converter={StaticResource inverseBool}}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ThereAreNoBlockedURIs}" />
<controls:CustomLabel
StyleClass="box-label-regular"
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
FontWeight="500"
HorizontalTextAlignment="Center"
Margin="14,10,14,0"/>
<controls:ExtendedCollectionView
ItemsSource="{Binding BlockedUris}"
IsVisible="{Binding ShowList}"
VerticalOptions="FillAndExpand"
Margin="0,5,0,0"
SelectionMode="None"
StyleClass="list, list-platform"
ExtraDataForLogging="Blocked Autofill Uris"
AutomationId="BlockedUrisCellList">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="pages:BlockAutofillUriItemViewModel">
<StackLayout
Orientation="Vertical"
AutomationId="BlockedUriCell">
<StackLayout
Orientation="Horizontal">
<controls:CustomLabel
VerticalOptions="Center"
StyleClass="box-label-regular"
Text="{Binding Uri}"
MaxLines="2"
LineBreakMode="TailTruncation"
FontWeight="500"
Margin="15,0,0,0"
HorizontalOptions="StartAndExpand"/>
<controls:IconButton
StyleClass="box-row-button-muted, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.PencilSquare}}"
Command="{Binding EditUriCommand}"
Margin="5,0,15,0"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n EditURI}"
AutomationId="EditUriButton" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</controls:ExtendedCollectionView>
<Button
Text="{u:I18n NewBlockedURI}"
Command="{Binding AddUriCommand}"
VerticalOptions="End"
HeightRequest="40"
Opacity="0.8"
Margin="14,5,14,10"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n NewBlockedURI}"
AutomationId="NewBlockedUriButton" />
</StackLayout>
</pages:BaseContentPage>

View File

@@ -0,0 +1,44 @@
using System.Threading.Tasks;
using Bit.App.Styles;
using Bit.App.Utilities;
using Bit.Core.Utilities;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class BlockAutofillUrisPage : BaseContentPage, IThemeDirtablePage
{
private readonly BlockAutofillUrisPageViewModel _vm;
public BlockAutofillUrisPage()
{
InitializeComponent();
_vm = BindingContext as BlockAutofillUrisPageViewModel;
_vm.Page = this;
}
protected override void OnAppearing()
{
base.OnAppearing();
_vm.InitAsync().FireAndForget(_ => Navigation.PopAsync());
UpdatePlaceholder();
}
public override async Task UpdateOnThemeChanged()
{
await base.UpdateOnThemeChanged();
UpdatePlaceholder();
}
private void UpdatePlaceholder()
{
MainThread.BeginInvokeOnMainThread(() =>
_emptyUrisPlaceholder.Source = ImageSource.FromFile(ThemeManager.UsingLightTheme ? "empty_uris_placeholder" : "empty_uris_placeholder_dark"));
}
}
}

View File

@@ -0,0 +1,186 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class BlockAutofillUrisPageViewModel : BaseViewModel
{
private const char URI_SEPARARTOR = ',';
private const string URI_FORMAT = "https://domain.com";
private readonly IStateService _stateService;
private readonly IDeviceActionService _deviceActionService;
public BlockAutofillUrisPageViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>();
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
AddUriCommand = new AsyncCommand(AddUriAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
EditUriCommand = new AsyncCommand<BlockAutofillUriItemViewModel>(EditUriAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
}
public ObservableRangeCollection<BlockAutofillUriItemViewModel> BlockedUris { get; set; } = new ObservableRangeCollection<BlockAutofillUriItemViewModel>();
public bool ShowList => BlockedUris.Any();
public ICommand AddUriCommand { get; }
public ICommand EditUriCommand { get; }
public async Task InitAsync()
{
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
if (blockedUrisList?.Any() != true)
{
return;
}
await MainThread.InvokeOnMainThreadAsync(() =>
{
BlockedUris.AddRange(blockedUrisList.OrderBy(uri => uri).Select(u => new BlockAutofillUriItemViewModel(u, EditUriCommand)).ToList());
TriggerPropertyChanged(nameof(ShowList));
});
}
private async Task AddUriAsync()
{
var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
{
Title = AppResources.NewUri,
Subtitle = AppResources.EnterURI,
ValueSubInfo = string.Format(AppResources.FormatXSeparateMultipleURIsWithAComma, URI_FORMAT),
OkButtonText = AppResources.Save,
ValidateText = text => ValidateUris(text, true)
});
if (response?.Text is null)
{
return;
}
await MainThread.InvokeOnMainThreadAsync(() =>
{
foreach (var uri in response.Value.Text.Split(URI_SEPARARTOR).Where(s => !string.IsNullOrEmpty(s)))
{
var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
}
BlockedUris = new ObservableRangeCollection<BlockAutofillUriItemViewModel>(BlockedUris.OrderBy(b => b.Uri));
TriggerPropertyChanged(nameof(BlockedUris));
TriggerPropertyChanged(nameof(ShowList));
});
await UpdateAutofillBlacklistedUrisAsync();
_deviceActionService.Toast(AppResources.URISaved);
}
private async Task EditUriAsync(BlockAutofillUriItemViewModel uriItemViewModel)
{
var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
{
Title = AppResources.EditURI,
Subtitle = AppResources.EnterURI,
Text = uriItemViewModel.Uri,
ValueSubInfo = string.Format(AppResources.FormatX, URI_FORMAT),
OkButtonText = AppResources.Save,
ThirdButtonText = AppResources.Remove,
ValidateText = text => ValidateUris(text, false)
});
if (response is null)
{
return;
}
if (response.Value.ExecuteThirdAction)
{
await MainThread.InvokeOnMainThreadAsync(() =>
{
BlockedUris.Remove(uriItemViewModel);
TriggerPropertyChanged(nameof(ShowList));
});
await UpdateAutofillBlacklistedUrisAsync();
_deviceActionService.Toast(AppResources.URIRemoved);
return;
}
var cleanedUri = response.Value.Text.Replace(Environment.NewLine, string.Empty).Trim();
await MainThread.InvokeOnMainThreadAsync(() =>
{
BlockedUris.Remove(uriItemViewModel);
BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
BlockedUris = new ObservableRangeCollection<BlockAutofillUriItemViewModel>(BlockedUris.OrderBy(b => b.Uri));
TriggerPropertyChanged(nameof(BlockedUris));
TriggerPropertyChanged(nameof(ShowList));
});
await UpdateAutofillBlacklistedUrisAsync();
_deviceActionService.Toast(AppResources.URISaved);
}
private string ValidateUris(string uris, bool allowMultipleUris)
{
if (string.IsNullOrWhiteSpace(uris))
{
return string.Format(AppResources.FormatX, URI_FORMAT);
}
if (!allowMultipleUris && uris.Contains(URI_SEPARARTOR))
{
return AppResources.CannotEditMultipleURIsAtOnce;
}
foreach (var uri in uris.Split(URI_SEPARARTOR).Where(u => !string.IsNullOrWhiteSpace(u)))
{
var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
if (!cleanedUri.StartsWith("http://") && !cleanedUri.StartsWith("https://") &&
!cleanedUri.StartsWith(Constants.AndroidAppProtocol))
{
return AppResources.InvalidFormatUseHttpsHttpOrAndroidApp;
}
if (!Uri.TryCreate(cleanedUri, UriKind.Absolute, out var _))
{
return AppResources.InvalidURI;
}
if (BlockedUris.Any(uriItem => uriItem.Uri == cleanedUri))
{
return string.Format(AppResources.TheURIXIsAlreadyBlocked, cleanedUri);
}
}
return null;
}
private async Task UpdateAutofillBlacklistedUrisAsync()
{
await _stateService.SetAutofillBlacklistedUrisAsync(BlockedUris.Any() ? BlockedUris.Select(bu => bu.Uri).ToList() : null);
}
}
public class BlockAutofillUriItemViewModel : ExtendedViewModel
{
public BlockAutofillUriItemViewModel(string uri, ICommand editUriCommand)
{
Uri = uri;
EditUriCommand = new Command(() => editUriCommand.Execute(this));
}
public string Uri { get; }
public ICommand EditUriCommand { get; }
}
}

View File

@@ -13,8 +13,13 @@
</ContentPage.BindingContext> </ContentPage.BindingContext>
<ContentPage.ToolbarItems> <ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" /> <ToolbarItem Text="{u:I18n Cancel}"
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" /> Clicked="Close_Clicked"
Order="Primary"
Priority="-1" />
<ToolbarItem Text="{u:I18n Save}"
Clicked="Save_Clicked"
Order="Primary" />
<ToolbarItem Text="{u:I18n Delete}" <ToolbarItem Text="{u:I18n Delete}"
Clicked="Delete_Clicked" Clicked="Delete_Clicked"
Order="Secondary" Order="Secondary"
@@ -43,7 +48,8 @@
StyleClass="box-value" StyleClass="box-value"
x:Name="_nameEntry" x:Name="_nameEntry"
ReturnType="Go" ReturnType="Go"
ReturnCommand="{Binding SubmitCommand}" /> ReturnCommand="{Binding SubmitCommand}"
AutomationId="FolderNameEntry" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>

View File

@@ -31,7 +31,8 @@
Margin="20, 0" Margin="20, 0"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center"></Label> HorizontalTextAlignment="Center"
AutomationId="NoFoldersLabel"></Label>
<controls:ExtendedCollectionView <controls:ExtendedCollectionView
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}" IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
ItemsSource="{Binding Folders}" ItemsSource="{Binding Folders}"
@@ -44,10 +45,12 @@
<DataTemplate x:DataType="views:FolderView"> <DataTemplate x:DataType="views:FolderView">
<controls:ExtendedStackLayout <controls:ExtendedStackLayout
StyleClass="list-row, list-row-platform" StyleClass="list-row, list-row-platform"
Padding="10"> Padding="10"
AutomationId="FolderCell">
<Label LineBreakMode="TailTruncation" <Label LineBreakMode="TailTruncation"
StyleClass="list-title, list-title-platform" StyleClass="list-title, list-title-platform"
Text="{Binding Name, Mode=OneWay}" /> Text="{Binding Name, Mode=OneWay}"
AutomationId="FolderName" />
</controls:ExtendedStackLayout> </controls:ExtendedStackLayout>
</DataTemplate> </DataTemplate>
</CollectionView.ItemTemplate> </CollectionView.ItemTemplate>

View File

@@ -27,7 +27,8 @@
x:Name="_themePicker" x:Name="_themePicker"
ItemsSource="{Binding ThemeOptions, Mode=OneTime}" ItemsSource="{Binding ThemeOptions, Mode=OneTime}"
SelectedIndex="{Binding ThemeSelectedIndex}" SelectedIndex="{Binding ThemeSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ThemeSelectorPicker" />
</StackLayout> </StackLayout>
<Label <Label
StyleClass="box-footer-label" StyleClass="box-footer-label"
@@ -44,7 +45,8 @@
x:Name="_autoDarkThemePicker" x:Name="_autoDarkThemePicker"
ItemsSource="{Binding AutoDarkThemeOptions, Mode=OneTime}" ItemsSource="{Binding AutoDarkThemeOptions, Mode=OneTime}"
SelectedIndex="{Binding AutoDarkThemeSelectedIndex}" SelectedIndex="{Binding AutoDarkThemeSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="DefaultDarkThemePicker" />
</StackLayout> </StackLayout>
<Label <Label
StyleClass="box-footer-label" StyleClass="box-footer-label"
@@ -59,7 +61,8 @@
x:Name="_uriMatchPicker" x:Name="_uriMatchPicker"
ItemsSource="{Binding UriMatchOptions, Mode=OneTime}" ItemsSource="{Binding UriMatchOptions, Mode=OneTime}"
SelectedIndex="{Binding UriMatchSelectedIndex}" SelectedIndex="{Binding UriMatchSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="DefaultUriMatchDetectionPicker" />
</StackLayout> </StackLayout>
<Label <Label
Text="{u:I18n DefaultUriMatchDetectionDescription}" Text="{u:I18n DefaultUriMatchDetectionDescription}"
@@ -74,7 +77,8 @@
x:Name="_clearClipboardPicker" x:Name="_clearClipboardPicker"
ItemsSource="{Binding ClearClipboardOptions, Mode=OneTime}" ItemsSource="{Binding ClearClipboardOptions, Mode=OneTime}"
SelectedIndex="{Binding ClearClipboardSelectedIndex}" SelectedIndex="{Binding ClearClipboardSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ClearClipboardPicker" />
</StackLayout> </StackLayout>
<Label <Label
Text="{u:I18n ClearClipboardDescription}" Text="{u:I18n ClearClipboardDescription}"
@@ -90,7 +94,8 @@
ItemsSource="{Binding LocalesOptions, Mode=OneTime}" ItemsSource="{Binding LocalesOptions, Mode=OneTime}"
SelectedItem="{Binding SelectedLocale}" SelectedItem="{Binding SelectedLocale}"
ItemDisplayBinding="{Binding Value}" ItemDisplayBinding="{Binding Value}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="LanguagePicker" />
</StackLayout> </StackLayout>
<Label <Label
Text="{u:I18n LanguageChangeRequiresAppRestart}" Text="{u:I18n LanguageChangeRequiresAppRestart}"
@@ -105,7 +110,8 @@
<Switch <Switch
IsToggled="{Binding AutoTotpCopy}" IsToggled="{Binding AutoTotpCopy}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End"
AutomationId="CopyTotpAutomaticallyToggle" />
</StackLayout> </StackLayout>
<Label <Label
Text="{u:I18n CopyTotpAutomaticallyDescription}" Text="{u:I18n CopyTotpAutomaticallyDescription}"
@@ -120,7 +126,8 @@
<Switch <Switch
IsToggled="{Binding Favicon}" IsToggled="{Binding Favicon}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End"
AutomationId="ShowWebsiteIconsToggle" />
</StackLayout> </StackLayout>
<Label <Label
Text="{u:I18n ShowWebsiteIconsDescription}" Text="{u:I18n ShowWebsiteIconsDescription}"
@@ -146,22 +153,14 @@
StyleClass="box-footer-label, box-footer-label-switch" /> StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}"> <StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding GoToBlockAutofillUrisCommand}" />
</StackLayout.GestureRecognizers>
<Label <Label
Text="{u:I18n AutofillBlockedUris}" Text="{u:I18n BlockAutoFill}"
StyleClass="box-label" /> StyleClass="box-label-regular" />
<Editor
x:Name="_autofillBlockedUrisEditor"
Text="{Binding AutofillBlockedUris}"
StyleClass="box-value"
AutoSize="TextChanges"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
Keyboard="Url"
Unfocused="AutofillBlockedUrisEditor_Unfocused" />
</StackLayout>
<Label <Label
Text="{u:I18n AutofillBlockedUrisDescription}" Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
StyleClass="box-footer-label" /> StyleClass="box-footer-label" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>

View File

@@ -1,6 +1,4 @@
using Bit.App.Abstractions; using Bit.Core.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.Forms; using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration; using Xamarin.Forms.PlatformConfiguration;
@@ -44,17 +42,6 @@ namespace Bit.App.Pages
await _vm.InitAsync(); await _vm.InitAsync();
} }
protected async override void OnDisappearing()
{
base.OnDisappearing();
await _vm.UpdateAutofillBlockedUris();
}
private async void AutofillBlockedUrisEditor_Unfocused(object sender, FocusEventArgs e)
{
await _vm.UpdateAutofillBlockedUris();
}
private async void Close_Clicked(object sender, System.EventArgs e) private async void Close_Clicked(object sender, System.EventArgs e)
{ {
if (DoOnce()) if (DoOnce())

View File

@@ -1,12 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
@@ -19,7 +20,6 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private bool _autofillSavePrompt; private bool _autofillSavePrompt;
private string _autofillBlockedUris;
private bool _favicon; private bool _favicon;
private bool _autoTotpCopy; private bool _autoTotpCopy;
private int _clearClipboardSelectedIndex; private int _clearClipboardSelectedIndex;
@@ -84,6 +84,10 @@ namespace Bit.App.Pages
new KeyValuePair<string, string>(null, AppResources.DefaultSystem) new KeyValuePair<string, string>(null, AppResources.DefaultSystem)
}; };
LocalesOptions.AddRange(_i18nService.LocaleNames.ToList()); LocalesOptions.AddRange(_i18nService.LocaleNames.ToList());
GoToBlockAutofillUrisCommand = new AsyncCommand(() => Page.Navigation.PushAsync(new BlockAutofillUrisPage()),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
} }
public List<KeyValuePair<int?, string>> ClearClipboardOptions { get; set; } public List<KeyValuePair<int?, string>> ClearClipboardOptions { get; set; }
@@ -192,25 +196,18 @@ namespace Bit.App.Pages
} }
} }
public string AutofillBlockedUris
{
get => _autofillBlockedUris;
set => SetProperty(ref _autofillBlockedUris, value);
}
public bool ShowAndroidAutofillSettings public bool ShowAndroidAutofillSettings
{ {
get => _showAndroidAutofillSettings; get => _showAndroidAutofillSettings;
set => SetProperty(ref _showAndroidAutofillSettings, value); set => SetProperty(ref _showAndroidAutofillSettings, value);
} }
public ICommand GoToBlockAutofillUrisCommand { get; }
public async Task InitAsync() public async Task InitAsync()
{ {
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault(); AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
AutofillBlockedUris = blockedUrisList != null ? string.Join(", ", blockedUrisList) : null;
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false); AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault(); Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
@@ -288,41 +285,6 @@ namespace Bit.App.Pages
} }
} }
public async Task UpdateAutofillBlockedUris()
{
if (_inited)
{
if (string.IsNullOrWhiteSpace(AutofillBlockedUris))
{
await _stateService.SetAutofillBlacklistedUrisAsync(null);
AutofillBlockedUris = null;
return;
}
try
{
var csv = AutofillBlockedUris;
var urisList = new List<string>();
foreach (var uri in csv.Split(','))
{
if (string.IsNullOrWhiteSpace(uri))
{
continue;
}
var cleanedUri = uri.Replace(System.Environment.NewLine, string.Empty).Trim();
if (!cleanedUri.StartsWith("http://") && !cleanedUri.StartsWith("https://") &&
!cleanedUri.StartsWith(Constants.AndroidAppProtocol))
{
continue;
}
urisList.Add(cleanedUri);
}
await _stateService.SetAutofillBlacklistedUrisAsync(urisList);
AutofillBlockedUris = string.Join(", ", urisList);
}
catch { }
}
}
private async Task UpdateCurrentLocaleAsync() private async Task UpdateCurrentLocaleAsync()
{ {
if (!_inited) if (!_inited)

View File

@@ -32,19 +32,21 @@
StyleClass="text-muted, text-sm, text-bold" StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center" />
</Frame> </Frame>
<Label IsVisible="{Binding UseFrame, Converter={StaticResource inverseBool}}" <controls:CustomLabel IsVisible="{Binding UseFrame, Converter={StaticResource inverseBool}}"
Text="{Binding Name, Mode=OneWay}" Text="{Binding Name, Mode=OneWay}"
LineBreakMode="{Binding LineBreakMode}" LineBreakMode="{Binding LineBreakMode}"
HorizontalOptions="StartAndExpand" HorizontalOptions="StartAndExpand"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
StyleClass="list-title"/> StyleClass="list-title"
<Label Text="{Binding SubLabel, Mode=OneWay}" AutomationId="{Binding AutomationIdSettingName}" />
<controls:CustomLabel Text="{Binding SubLabel, Mode=OneWay}"
IsVisible="{Binding ShowSubLabel}" IsVisible="{Binding ShowSubLabel}"
HorizontalOptions="End" HorizontalOptions="End"
HorizontalTextAlignment="End" HorizontalTextAlignment="End"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
TextColor="{Binding SubLabelColor}" TextColor="{Binding SubLabelColor}"
StyleClass="list-sub" /> StyleClass="list-sub"
AutomationId="{Binding AutomationIdSettingStatus}" />
</controls:ExtendedStackLayout> </controls:ExtendedStackLayout>
</DataTemplate> </DataTemplate>
<DataTemplate <DataTemplate
@@ -57,7 +59,8 @@
Padding="10" Padding="10"
HasShadow="False" HasShadow="False"
BackgroundColor="Transparent" BackgroundColor="Transparent"
BorderColor="{DynamicResource PrimaryColor}"> BorderColor="{DynamicResource PrimaryColor}"
AutomationId="SettingActivePolicyTextLabel">
<Label <Label
Text="{Binding Name, Mode=OneWay}" Text="{Binding Name, Mode=OneWay}"
StyleClass="text-muted, text-sm, text-bold" StyleClass="text-muted, text-sm, text-bold"
@@ -75,7 +78,8 @@
VerticalOptions="Center" VerticalOptions="Center"
FontSize="Small" FontSize="Small"
TextColor="{Binding SubLabelColor}" TextColor="{Binding SubLabelColor}"
StyleClass="list-sub" Margin="-5"/> StyleClass="list-sub" Margin="-5"
AutomationId="SettingCustomVaultTimeoutPicker" />
<controls:ExtendedStackLayout.GestureRecognizers> <controls:ExtendedStackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="ActivateTimePicker"/> <TapGestureRecognizer Tapped="ActivateTimePicker"/>
</controls:ExtendedStackLayout.GestureRecognizers> </controls:ExtendedStackLayout.GestureRecognizers>

View File

@@ -1,7 +1,9 @@
using System; using System;
using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.App.Utilities.Automation;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
@@ -22,5 +24,29 @@ namespace Bit.App.Pages
public Color SubLabelColor => SubLabelTextEnabled ? public Color SubLabelColor => SubLabelTextEnabled ?
ThemeManager.GetResourceColor("SuccessColor") : ThemeManager.GetResourceColor("SuccessColor") :
ThemeManager.GetResourceColor("MutedColor"); ThemeManager.GetResourceColor("MutedColor");
public string AutomationIdSettingName
{
get
{
return AutomationIdsHelper.AddSuffixFor(
UseFrame ? "EnabledPolicy"
: AutomationIdsHelper.ToEnglishTitleCase(Name)
, SuffixType.Cell);
}
}
public string AutomationIdSettingStatus
{
get
{
if (UseFrame)
{
return null;
}
return AutomationIdsHelper.AddSuffixFor(AutomationIdsHelper.ToEnglishTitleCase(Name), SuffixType.SettingValue);
}
}
} }
} }

View File

@@ -48,6 +48,7 @@ namespace Bit.App.Pages
private bool _reportLoggingEnabled; private bool _reportLoggingEnabled;
private bool _approvePasswordlessLoginRequests; private bool _approvePasswordlessLoginRequests;
private bool _shouldConnectToWatch; private bool _shouldConnectToWatch;
private bool _hasMasterPassword;
private readonly static List<KeyValuePair<string, int?>> VaultTimeoutOptions = private readonly static List<KeyValuePair<string, int?>> VaultTimeoutOptions =
new List<KeyValuePair<string, int?>> new List<KeyValuePair<string, int?>>
{ {
@@ -100,12 +101,17 @@ namespace Bit.App.Pages
ExecuteSettingItemCommand = new AsyncCommand<SettingsPageListItem>(item => item.ExecuteAsync(), onException: _loggerService.Exception, allowsMultipleExecutions: false); ExecuteSettingItemCommand = new AsyncCommand<SettingsPageListItem>(item => item.ExecuteAsync(), onException: _loggerService.Exception, allowsMultipleExecutions: false);
} }
private bool IsVaultTimeoutActionLockAllowed => _hasMasterPassword || _biometric || _pin;
public ObservableRangeCollection<ISettingsPageListItem> GroupedItems { get; set; } public ObservableRangeCollection<ISettingsPageListItem> GroupedItems { get; set; }
public IAsyncCommand<SettingsPageListItem> ExecuteSettingItemCommand { get; } public IAsyncCommand<SettingsPageListItem> ExecuteSettingItemCommand { get; }
public async Task InitAsync() public async Task InitAsync()
{ {
var decryptionOptions = await _stateService.GetAccountDecryptionOptions();
// set has true for backwards compatibility
_hasMasterPassword = decryptionOptions?.HasMasterPassword ?? true;
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync(); _supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
var lastSync = await _syncService.GetLastSyncAsync(); var lastSync = await _syncService.GetLastSyncAsync();
if (lastSync != null) if (lastSync != null)
@@ -124,7 +130,13 @@ namespace Bit.App.Pages
_vaultTimeoutDisplayValue = _vaultTimeoutOptions.FirstOrDefault(o => o.Value == _vaultTimeout).Key; _vaultTimeoutDisplayValue = _vaultTimeoutOptions.FirstOrDefault(o => o.Value == _vaultTimeout).Key;
_vaultTimeoutDisplayValue ??= _vaultTimeoutOptions.Where(o => o.Value == CustomVaultTimeoutValue).First().Key; _vaultTimeoutDisplayValue ??= _vaultTimeoutOptions.Where(o => o.Value == CustomVaultTimeoutValue).First().Key;
var action = await _vaultTimeoutService.GetVaultTimeoutAction() ?? VaultTimeoutAction.Lock; var savedVaultTimeoutAction = await _vaultTimeoutService.GetVaultTimeoutAction();
var action = savedVaultTimeoutAction ?? VaultTimeoutAction.Lock;
if (!_hasMasterPassword && savedVaultTimeoutAction == null)
{
action = VaultTimeoutAction.Logout;
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(_vaultTimeout, VaultTimeoutAction.Logout);
}
_vaultTimeoutActionDisplayValue = _vaultTimeoutActionOptions.FirstOrDefault(o => o.Value == action).Key; _vaultTimeoutActionDisplayValue = _vaultTimeoutActionOptions.FirstOrDefault(o => o.Value == action).Key;
if (await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout)) if (await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout))
@@ -139,7 +151,7 @@ namespace Bit.App.Pages
} }
var pinSet = await _vaultTimeoutService.IsPinLockSetAsync(); var pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
_pin = pinSet.Item1 || pinSet.Item2; _pin = pinSet != PinLockEnum.Disabled;
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync(); _biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
_screenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync(); _screenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
@@ -382,13 +394,16 @@ namespace Bit.App.Pages
public async Task VaultTimeoutActionAsync() public async Task VaultTimeoutActionAsync()
{ {
if (_vaultTimeoutPolicy != null && if (_vaultTimeoutPolicy != null &&
!string.IsNullOrEmpty(_vaultTimeoutPolicy.GetString(Policy.MINUTES_KEY))) !string.IsNullOrEmpty(_vaultTimeoutPolicy.GetString(Policy.ACTION_KEY)))
{ {
// do nothing if we have a policy set // do nothing if we have a policy set
return; return;
} }
var options = _vaultTimeoutActionOptions.Select(o =>
o.Key == _vaultTimeoutActionDisplayValue ? $"✓ {o.Key}" : o.Key).ToArray(); var options = IsVaultTimeoutActionLockAllowed
? _vaultTimeoutActionOptions.Select(o => CreateSelectableOption(o.Key, _vaultTimeoutActionDisplayValue == o.Key)).ToArray()
: _vaultTimeoutActionOptions.Where(o => o.Value == VaultTimeoutAction.Logout).Select(v => ToSelectedOption(v.Key)).ToArray();
var selection = await Page.DisplayActionSheet(AppResources.VaultTimeoutAction, var selection = await Page.DisplayActionSheet(AppResources.VaultTimeoutAction,
AppResources.Cancel, null, options); AppResources.Cancel, null, options);
if (selection == null || selection == AppResources.Cancel) if (selection == null || selection == AppResources.Cancel)
@@ -437,19 +452,20 @@ namespace Bit.App.Pages
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile)); var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
var email = await _stateService.GetEmailAsync(); var email = await _stateService.GetEmailAsync();
var pinKey = await _cryptoService.MakePinKeyAysnc(pin, email, kdfConfig); var pinKey = await _cryptoService.MakePinKeyAsync(pin, email, kdfConfig);
var key = await _cryptoService.GetKeyAsync(); var userKey = await _cryptoService.GetUserKeyAsync();
var pinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey); var pinProtectedKey = await _cryptoService.EncryptAsync(userKey.Key, pinKey);
var encPin = await _cryptoService.EncryptAsync(pin);
await _stateService.SetProtectedPinAsync(encPin.EncryptedString);
if (masterPassOnRestart) if (masterPassOnRestart)
{ {
var encPin = await _cryptoService.EncryptAsync(pin); await _stateService.SetUserKeyPinEphemeralAsync(pinProtectedKey);
await _stateService.SetProtectedPinAsync(encPin.EncryptedString);
await _stateService.SetPinProtectedKeyAsync(pinProtectedKey);
} }
else else
{ {
await _stateService.SetPinProtectedAsync(pinProtectedKey.EncryptedString); await _stateService.SetUserKeyPinAsync(pinProtectedKey);
} }
} }
else else
@@ -459,7 +475,6 @@ namespace Bit.App.Pages
} }
if (!_pin) if (!_pin)
{ {
await _cryptoService.ClearPinProtectedKeyAsync();
await _vaultTimeoutService.ClearAsync(); await _vaultTimeoutService.ClearAsync();
} }
BuildList(); BuildList();
@@ -489,9 +504,10 @@ namespace Bit.App.Pages
else else
{ {
await _stateService.SetBiometricUnlockAsync(null); await _stateService.SetBiometricUnlockAsync(null);
await UpdateVaultTimeoutActionIfNeededAsync();
} }
await _stateService.SetBiometricLockedAsync(false); await _stateService.SetBiometricLockedAsync(false);
await _cryptoService.ToggleKeyAsync(); await _cryptoService.ToggleKeysAsync();
BuildList(); BuildList();
} }
@@ -835,9 +851,11 @@ namespace Bit.App.Pages
return _vaultTimeoutOptions.FirstOrDefault(o => o.Key == key).Value; return _vaultTimeoutOptions.FirstOrDefault(o => o.Key == key).Value;
} }
private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option; private string CreateSelectableOption(string option, bool selected) => selected ? ToSelectedOption(option) : option;
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == $"✓ {compareTo}"; private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == ToSelectedOption(compareTo);
private string ToSelectedOption(string option) => $"✓ {option}";
public async Task SetScreenCaptureAllowedAsync() public async Task SetScreenCaptureAllowedAsync()
{ {
@@ -869,5 +887,17 @@ namespace Bit.App.Pages
await _watchDeviceService.SetShouldConnectToWatchAsync(_shouldConnectToWatch); await _watchDeviceService.SetShouldConnectToWatchAsync(_shouldConnectToWatch);
BuildList(); BuildList();
} }
private async Task UpdateVaultTimeoutActionIfNeededAsync()
{
if (IsVaultTimeoutActionLockAllowed)
{
return;
}
_vaultTimeoutActionDisplayValue = _vaultTimeoutActionOptions.First(o => o.Value == VaultTimeoutAction.Logout).Key;
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(_vaultTimeout, VaultTimeoutAction.Logout);
_deviceActionService.Toast(AppResources.VaultTimeoutActionChangedToLogOut);
}
} }
} }

View File

@@ -33,23 +33,25 @@
<StackLayout StyleClass="box"> <StackLayout StyleClass="box">
<StackLayout StyleClass="box-row" Padding="10, 20" <StackLayout StyleClass="box-row" Padding="10, 20"
IsVisible="{Binding HasAttachments, Converter={StaticResource inverseBool}}"> IsVisible="{Binding HasAttachments, Converter={StaticResource inverseBool}}">
<Label Text="{u:I18n NoAttachments}" HorizontalTextAlignment="Center" /> <Label Text="{u:I18n NoAttachments}" HorizontalTextAlignment="Center" AutomationId="NoAttachmentsLabel" />
</StackLayout> </StackLayout>
<controls:RepeaterView ItemsSource="{Binding Attachments}" IsVisible="{Binding HasAttachments}"> <controls:RepeaterView ItemsSource="{Binding Attachments}" IsVisible="{Binding HasAttachments}" AutomationId="AttachmentsList">
<controls:RepeaterView.ItemTemplate> <controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="views:AttachmentView"> <DataTemplate x:DataType="views:AttachmentView">
<StackLayout Spacing="0" Padding="0"> <StackLayout Spacing="0" Padding="0">
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10"> <StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10" AutomationId="AttachmentRow">
<Label <Label
Text="{Binding FileName, Mode=OneWay}" Text="{Binding FileName, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
VerticalTextAlignment="Center" VerticalTextAlignment="Center"
HorizontalOptions="StartAndExpand" /> HorizontalOptions="StartAndExpand"
AutomationId="AttachmentFileNameLabel" />
<Label <Label
Text="{Binding SizeName, Mode=OneWay}" Text="{Binding SizeName, Mode=OneWay}"
StyleClass="box-sub-label" StyleClass="box-sub-label"
HorizontalTextAlignment="End" HorizontalTextAlignment="End"
VerticalTextAlignment="Center" /> VerticalTextAlignment="Center"
AutomationId="AttachmentFileSizeLabel" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}" Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
@@ -57,7 +59,8 @@
CommandParameter="{Binding .}" CommandParameter="{Binding .}"
VerticalOptions="Center" VerticalOptions="Center"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Delete}" /> AutomationProperties.Name="{u:I18n Delete}"
AutomationId="AttachmentDeleteButton" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
</StackLayout> </StackLayout>
@@ -77,17 +80,20 @@
LineBreakMode="CharacterWrap" LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted" StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center"
AutomationId="NoFileChosenLabel" />
<Label <Label
IsVisible="{Binding FileName, Converter={StaticResource notNull}}" IsVisible="{Binding FileName, Converter={StaticResource notNull}}"
Text="{Binding FileName}" Text="{Binding FileName}"
LineBreakMode="CharacterWrap" LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted" StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center"
AutomationId="NewAttachmentNameLabel" />
</StackLayout> </StackLayout>
<Button Text="{u:I18n ChooseFile}" StyleClass="box-button-row" <Button Text="{u:I18n ChooseFile}" StyleClass="box-button-row"
Clicked="ChooseFile_Clicked"></Button> Clicked="ChooseFile_Clicked"
AutomationId="ChooseFileButton"></Button>
<Label <Label
Margin="0, 10, 0, 0" Margin="0, 10, 0, 0"
Text="{u:I18n MaxFileSize}" Text="{u:I18n MaxFileSize}"

View File

@@ -74,7 +74,7 @@ namespace Bit.App.Pages
_cipherDomain = await _cipherService.GetAsync(CipherId); _cipherDomain = await _cipherService.GetAsync(CipherId);
Cipher = await _cipherDomain.DecryptAsync(); Cipher = await _cipherDomain.DecryptAsync();
LoadAttachments(); LoadAttachments();
_hasUpdatedKey = await _cryptoService.HasEncKeyAsync(); _hasUpdatedKey = await _cryptoService.HasUserKeyAsync();
var canAccessPremium = await _stateService.CanAccessPremiumAsync(); var canAccessPremium = await _stateService.CanAccessPremiumAsync();
_canAccessAttachments = canAccessPremium || Cipher.OrganizationId != null; _canAccessAttachments = canAccessPremium || Cipher.OrganizationId != null;
if (!_canAccessAttachments) if (!_canAccessAttachments)

View File

@@ -57,16 +57,16 @@
x:Key="deleteItem" /> x:Key="deleteItem" />
<DataTemplate x:Key="TextCustomFieldDataTemplate"> <DataTemplate x:Key="TextCustomFieldDataTemplate">
<il:TextCustomFieldItemLayout /> <il:TextCustomFieldItemLayout AutomationId="TextCustomFieldItem" />
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="BooleanCustomFieldDataTemplate"> <DataTemplate x:Key="BooleanCustomFieldDataTemplate">
<il:BooleanCustomFieldItemLayout /> <il:BooleanCustomFieldItemLayout AutomationId="BooleanCustomFieldItem" />
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="HiddenCustomFieldDataTemplate"> <DataTemplate x:Key="HiddenCustomFieldDataTemplate">
<il:HiddenCustomFieldItemLayout /> <il:HiddenCustomFieldItemLayout AutomationId="HiddenCustomFieldItem" />
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="LinkedCustomFieldDataTemplate"> <DataTemplate x:Key="LinkedCustomFieldDataTemplate">
<il:LinkedCustomFieldItemLayout /> <il:LinkedCustomFieldItemLayout AutomationId="LinkedCustomFieldItem" />
</DataTemplate> </DataTemplate>
<dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector" <dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector"
@@ -100,7 +100,8 @@
<Label <Label
Text="{u:I18n PersonalOwnershipPolicyInEffect}" Text="{u:I18n PersonalOwnershipPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold" StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center"
AutomationId="PersonalOwnershipPolicyLabel"/>
</Frame> </Frame>
</Grid> </Grid>
<StackLayout StyleClass="box-row-header"> <StackLayout StyleClass="box-row-header">
@@ -116,7 +117,8 @@
x:Name="_typePicker" x:Name="_typePicker"
ItemsSource="{Binding TypeOptions, Mode=OneTime}" ItemsSource="{Binding TypeOptions, Mode=OneTime}"
SelectedIndex="{Binding TypeSelectedIndex}" SelectedIndex="{Binding TypeSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemTypePicker" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -125,7 +127,10 @@
<Entry <Entry
x:Name="_nameEntry" x:Name="_nameEntry"
Text="{Binding Cipher.Name}" Text="{Binding Cipher.Name}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Name}"
AutomationId="ItemNameEntry" />
</StackLayout> </StackLayout>
<StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0"> <StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0">
<Grid StyleClass="box-row, box-row-input" <Grid StyleClass="box-row, box-row-input"
@@ -138,7 +143,10 @@
x:Name="_loginUsernameEntry" x:Name="_loginUsernameEntry"
Text="{Binding Cipher.Login.Username}" Text="{Binding Cipher.Login.Username}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1"/> Grid.Row="1"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Username}"
AutomationId="LoginUsernameEntry" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}" Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
@@ -146,7 +154,8 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GenerateUsername}" /> AutomationProperties.Name="{u:I18n GenerateUsername}"
AutomationId="GenerateUsernameButton" />
</Grid> </Grid>
<Grid StyleClass="box-row, box-row-input"> <Grid StyleClass="box-row, box-row-input">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -174,7 +183,10 @@
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}" IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
IsSpellCheckEnabled="False" IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False" IsTextPredictionEnabled="False"
IsEnabled="{Binding Cipher.ViewPassword}"/> IsEnabled="{Binding Cipher.ViewPassword}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Password}"
AutomationId="LoginPasswordEntry" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}" Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
@@ -184,7 +196,8 @@
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CheckPassword}" AutomationProperties.Name="{u:I18n CheckPassword}"
IsVisible="{Binding Cipher.ViewPassword}" /> IsVisible="{Binding Cipher.ViewPassword}"
AutomationId="CheckPasswordButton" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}" Text="{Binding ShowPasswordIcon}"
@@ -195,7 +208,8 @@
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
IsVisible="{Binding Cipher.ViewPassword}" /> IsVisible="{Binding Cipher.ViewPassword}"
AutomationId="ViewPasswordButton" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}" Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
@@ -205,7 +219,8 @@
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GeneratePassword}" AutomationProperties.Name="{u:I18n GeneratePassword}"
IsVisible="{Binding Cipher.ViewPassword}" /> IsVisible="{Binding Cipher.ViewPassword}"
AutomationId="RegeneratePasswordButton" />
</Grid> </Grid>
<Grid StyleClass="box-row, box-row-input"> <Grid StyleClass="box-row, box-row-input">
@@ -241,7 +256,8 @@
Padding="0,15" Padding="0,15"
HorizontalOptions="Center" HorizontalOptions="Center"
VerticalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center" /> VerticalTextAlignment="Center"
AutomationId="SetupTotpButton" />
</Frame> </Frame>
<controls:MonoEntry <controls:MonoEntry
x:Name="_loginTotpEntry" x:Name="_loginTotpEntry"
@@ -254,7 +270,10 @@
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="{Binding TotpColumnSpan}" /> Grid.ColumnSpan="{Binding TotpColumnSpan}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n AuthenticatorKey}"
AutomationId="LoginTotpEntry" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}" Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -262,7 +281,10 @@
IsVisible="{Binding HasTotpValue}" IsVisible="{Binding HasTotpValue}"
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" /> Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyTotp}"
AutomationId="CopyTotpValueButton" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Camera}}" Text="{Binding Source={x:Static core:BitwardenIcons.Camera}}"
@@ -272,7 +294,8 @@
Grid.RowSpan="2" Grid.RowSpan="2"
IsVisible="{Binding HasTotpValue}" IsVisible="{Binding HasTotpValue}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ScanQrTitle}" /> AutomationProperties.Name="{u:I18n ScanQrTitle}"
/>
</Grid> </Grid>
</StackLayout> </StackLayout>
<StackLayout IsVisible="{Binding IsCard}" Spacing="0" Padding="0"> <StackLayout IsVisible="{Binding IsCard}" Spacing="0" Padding="0">
@@ -283,7 +306,8 @@
<Entry <Entry
x:Name="_cardholderNameEntry" x:Name="_cardholderNameEntry"
Text="{Binding Cipher.Card.CardholderName}" Text="{Binding Cipher.Card.CardholderName}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="CardholderNameEntry" />
</StackLayout> </StackLayout>
<Grid StyleClass="box-row, box-row-input"> <Grid StyleClass="box-row, box-row-input">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -307,7 +331,10 @@
Grid.Column="0" Grid.Column="0"
IsPassword="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}" IsPassword="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}"
IsSpellCheckEnabled="False" IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False" /> IsTextPredictionEnabled="False"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Number}"
AutomationId="CardNumberEntry" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowCardNumberIcon}" Text="{Binding ShowCardNumberIcon}"
@@ -316,7 +343,8 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" /> AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationId="ShowCardNumberButton" />
</Grid> </Grid>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -326,7 +354,8 @@
x:Name="_cardBrandPicker" x:Name="_cardBrandPicker"
ItemsSource="{Binding CardBrandOptions, Mode=OneTime}" ItemsSource="{Binding CardBrandOptions, Mode=OneTime}"
SelectedIndex="{Binding CardBrandSelectedIndex}" SelectedIndex="{Binding CardBrandSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="CardBrandPicker" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -336,7 +365,8 @@
x:Name="_cardExpMonthPicker" x:Name="_cardExpMonthPicker"
ItemsSource="{Binding CardExpMonthOptions, Mode=OneTime}" ItemsSource="{Binding CardExpMonthOptions, Mode=OneTime}"
SelectedIndex="{Binding CardExpMonthSelectedIndex}" SelectedIndex="{Binding CardExpMonthSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="CardExpirationMonthPicker" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -346,7 +376,10 @@
x:Name="_cardExpYearEntry" x:Name="_cardExpYearEntry"
Text="{Binding Cipher.Card.ExpYear}" Text="{Binding Cipher.Card.ExpYear}"
StyleClass="box-value" StyleClass="box-value"
Keyboard="Numeric" /> Keyboard="Numeric"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ExpirationYear}"
AutomationId="CardExpirationYearEntry" />
</StackLayout> </StackLayout>
<Grid StyleClass="box-row, box-row-input"> <Grid StyleClass="box-row, box-row-input">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -371,7 +404,10 @@
Keyboard="Numeric" Keyboard="Numeric"
IsPassword="{Binding ShowCardCode, Converter={StaticResource inverseBool}}" IsPassword="{Binding ShowCardCode, Converter={StaticResource inverseBool}}"
IsSpellCheckEnabled="False" IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False" /> IsTextPredictionEnabled="False"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n SecurityCode}"
AutomationId="CardSecurityCodeEntry" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowCardCodeIcon}" Text="{Binding ShowCardCodeIcon}"
@@ -380,7 +416,8 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" /> AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationId="CardShowSecurityCodeButton" />
</Grid> </Grid>
</StackLayout> </StackLayout>
<StackLayout IsVisible="{Binding IsIdentity}" Spacing="0" Padding="0"> <StackLayout IsVisible="{Binding IsIdentity}" Spacing="0" Padding="0">
@@ -392,7 +429,8 @@
x:Name="_identityTitlePicker" x:Name="_identityTitlePicker"
ItemsSource="{Binding IdentityTitleOptions, Mode=OneTime}" ItemsSource="{Binding IdentityTitleOptions, Mode=OneTime}"
SelectedIndex="{Binding IdentityTitleSelectedIndex}" SelectedIndex="{Binding IdentityTitleSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="IdentityTitlePicker" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -401,7 +439,10 @@
<Entry <Entry
x:Name="_identityFirstNameEntry" x:Name="_identityFirstNameEntry"
Text="{Binding Cipher.Identity.FirstName}" Text="{Binding Cipher.Identity.FirstName}"
StyleClass="box-value,capitalize-word-input"/> StyleClass="box-value,capitalize-word-input"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n FirstName}"
AutomationId="IdentityFirstNameEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -410,7 +451,10 @@
<Entry <Entry
x:Name="_identityMiddleNameEntry" x:Name="_identityMiddleNameEntry"
Text="{Binding Cipher.Identity.MiddleName}" Text="{Binding Cipher.Identity.MiddleName}"
StyleClass="box-value,capitalize-word-input" /> StyleClass="box-value,capitalize-word-input"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n MiddleName}"
AutomationId="IdentityMiddleNameEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -419,7 +463,10 @@
<Entry <Entry
x:Name="_identityLastNameEntry" x:Name="_identityLastNameEntry"
Text="{Binding Cipher.Identity.LastName}" Text="{Binding Cipher.Identity.LastName}"
StyleClass="box-value,capitalize-word-input" /> StyleClass="box-value,capitalize-word-input"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n LastName}"
AutomationId="IdentityLastNameEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -428,7 +475,10 @@
<Entry <Entry
x:Name="_identityUsernameEntry" x:Name="_identityUsernameEntry"
Text="{Binding Cipher.Identity.Username}" Text="{Binding Cipher.Identity.Username}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Username}"
AutomationId="IdentityUsernameEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -437,7 +487,10 @@
<Entry <Entry
x:Name="_identityCompanyEntry" x:Name="_identityCompanyEntry"
Text="{Binding Cipher.Identity.Company}" Text="{Binding Cipher.Identity.Company}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Company}"
AutomationId="IdentityCompanyEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -446,7 +499,10 @@
<Entry <Entry
x:Name="_identitySsnEntry" x:Name="_identitySsnEntry"
Text="{Binding Cipher.Identity.SSN}" Text="{Binding Cipher.Identity.SSN}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n SSN}"
AutomationId="IdentitySsnEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -455,7 +511,10 @@
<Entry <Entry
x:Name="_identityPassportNumberEntry" x:Name="_identityPassportNumberEntry"
Text="{Binding Cipher.Identity.PassportNumber}" Text="{Binding Cipher.Identity.PassportNumber}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n PassportNumber}"
AutomationId="IdentityPassportNumberEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -464,7 +523,10 @@
<Entry <Entry
x:Name="_identityLicenseNumberEntry" x:Name="_identityLicenseNumberEntry"
Text="{Binding Cipher.Identity.LicenseNumber}" Text="{Binding Cipher.Identity.LicenseNumber}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n LicenseNumber}"
AutomationId="IdentityLicenseNumberEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -474,7 +536,10 @@
x:Name="_identityEmailEntry" x:Name="_identityEmailEntry"
Keyboard="Email" Keyboard="Email"
Text="{Binding Cipher.Identity.Email}" Text="{Binding Cipher.Identity.Email}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Email}"
AutomationId="IdentityEmailEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -484,7 +549,10 @@
x:Name="_identityPhoneEntry" x:Name="_identityPhoneEntry"
Text="{Binding Cipher.Identity.Phone}" Text="{Binding Cipher.Identity.Phone}"
Keyboard="Telephone" Keyboard="Telephone"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Phone}"
AutomationId="IdentityPhoneEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -493,7 +561,10 @@
<Entry <Entry
x:Name="_identityAddress1Entry" x:Name="_identityAddress1Entry"
Text="{Binding Cipher.Identity.Address1}" Text="{Binding Cipher.Identity.Address1}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Address1}"
AutomationId="IdentityAddressOneEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -502,7 +573,10 @@
<Entry <Entry
x:Name="_identityAddress2Entry" x:Name="_identityAddress2Entry"
Text="{Binding Cipher.Identity.Address2}" Text="{Binding Cipher.Identity.Address2}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Address2}"
AutomationId="IdentityAddressTwoEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -511,7 +585,10 @@
<Entry <Entry
x:Name="_identityAddress3Entry" x:Name="_identityAddress3Entry"
Text="{Binding Cipher.Identity.Address3}" Text="{Binding Cipher.Identity.Address3}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Address3}"
AutomationId="IdentityAddressThreeEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -520,7 +597,10 @@
<Entry <Entry
x:Name="_identityCityEntry" x:Name="_identityCityEntry"
Text="{Binding Cipher.Identity.City}" Text="{Binding Cipher.Identity.City}"
StyleClass="box-value,capitalize-sentence-input" /> StyleClass="box-value,capitalize-sentence-input"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CityTown}"
AutomationId="IdentityCityEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -529,7 +609,10 @@
<Entry <Entry
x:Name="_identityStateEntry" x:Name="_identityStateEntry"
Text="{Binding Cipher.Identity.State}" Text="{Binding Cipher.Identity.State}"
StyleClass="box-value,capitalize-sentence-input" /> StyleClass="box-value,capitalize-sentence-input"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n StateProvince}"
AutomationId="IdentityStateEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -538,7 +621,10 @@
<Entry <Entry
x:Name="_identityPostalCodeEntry" x:Name="_identityPostalCodeEntry"
Text="{Binding Cipher.Identity.PostalCode}" Text="{Binding Cipher.Identity.PostalCode}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ZipPostalCode}"
AutomationId="IdentityPostalCodeEntry" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-input"> <StackLayout StyleClass="box-row, box-row-input">
<Label <Label
@@ -547,7 +633,10 @@
<Entry <Entry
x:Name="_identityCountryEntry" x:Name="_identityCountryEntry"
Text="{Binding Cipher.Identity.Country}" Text="{Binding Cipher.Identity.Country}"
StyleClass="box-value,capitalize-sentence-input" /> StyleClass="box-value,capitalize-sentence-input"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Country}"
AutomationId="IdentityCountryEntry" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
@@ -559,7 +648,7 @@
<controls:RepeaterView ItemsSource="{Binding Uris}"> <controls:RepeaterView ItemsSource="{Binding Uris}">
<controls:RepeaterView.ItemTemplate> <controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="views:LoginUriView"> <DataTemplate x:DataType="views:LoginUriView">
<Grid StyleClass="box-row, box-row-input"> <Grid StyleClass="box-row, box-row-input" AutomationId="UriListGrid" >
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
@@ -578,7 +667,10 @@
Keyboard="Url" Keyboard="Url"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" /> Grid.Column="0"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n URI}"
AutomationId="LoginUriEntry" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}" Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
@@ -588,13 +680,15 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" /> AutomationProperties.Name="{u:I18n Options}"
AutomationId="LoginUriOptionsButton" />
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</controls:RepeaterView.ItemTemplate> </controls:RepeaterView.ItemTemplate>
</controls:RepeaterView> </controls:RepeaterView>
<Button Text="{u:I18n NewUri}" StyleClass="box-button-row" <Button Text="{u:I18n NewUri}" StyleClass="box-button-row"
Clicked="NewUri_Clicked"></Button> Clicked="NewUri_Clicked"
AutomationId="LoginAddNewUriButton"></Button>
</StackLayout> </StackLayout>
<StackLayout StyleClass="box"> <StackLayout StyleClass="box">
<StackLayout StyleClass="box-row-header"> <StackLayout StyleClass="box-row-header">
@@ -609,7 +703,8 @@
x:Name="_folderPicker" x:Name="_folderPicker"
ItemsSource="{Binding FolderOptions, Mode=OneTime}" ItemsSource="{Binding FolderOptions, Mode=OneTime}"
SelectedIndex="{Binding FolderSelectedIndex}" SelectedIndex="{Binding FolderSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="FolderPicker" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout StyleClass="box-row, box-row-switch">
<Label <Label
@@ -619,7 +714,8 @@
<Switch <Switch
IsToggled="{Binding Cipher.Favorite}" IsToggled="{Binding Cipher.Favorite}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End"
AutomationId="ItemFavoriteToggle" />
</StackLayout> </StackLayout>
<StackLayout x:Name="_passwordPrompt" StyleClass="box-row, box-row-switch"> <StackLayout x:Name="_passwordPrompt" StyleClass="box-row, box-row-switch">
<Label <Label
@@ -631,13 +727,14 @@
Command="{Binding PasswordPromptHelpCommand}" Command="{Binding PasswordPromptHelpCommand}"
TextColor="{DynamicResource MutedColor}" TextColor="{DynamicResource MutedColor}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" AutomationProperties.Name="{u:I18n MasterPasswordRePromptHelp}"
HorizontalOptions="StartAndExpand" /> HorizontalOptions="StartAndExpand" />
<Switch <Switch
IsToggled="{Binding PasswordPrompt}" IsToggled="{Binding PasswordPrompt}"
Toggled="PasswordPrompt_Toggled" Toggled="PasswordPrompt_Toggled"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End"
AutomationId="MasterPasswordRepromptToggle" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
</StackLayout> </StackLayout>
@@ -652,7 +749,10 @@
AutoSize="TextChanges" AutoSize="TextChanges"
StyleClass="box-value" StyleClass="box-value"
effects:ScrollEnabledEffect.IsScrollEnabled="false" effects:ScrollEnabledEffect.IsScrollEnabled="false"
Text="{Binding Cipher.Notes}"> Text="{Binding Cipher.Notes}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Notes}"
AutomationId="ItemNotesEntry">
<Editor.Behaviors> <Editor.Behaviors>
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" /> <behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
</Editor.Behaviors> </Editor.Behaviors>
@@ -671,9 +771,11 @@
<StackLayout <StackLayout
Spacing="0" Spacing="0"
BindableLayout.ItemsSource="{Binding Fields}" BindableLayout.ItemsSource="{Binding Fields}"
BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}" /> BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}"
AutomationId="CustomFieldsList" />
<Button Text="{u:I18n NewCustomField}" StyleClass="box-button-row" <Button Text="{u:I18n NewCustomField}" StyleClass="box-button-row"
Clicked="NewField_Clicked"></Button> Clicked="NewField_Clicked"
AutomationId="NewCustomFieldButton"></Button>
</StackLayout> </StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowOwnershipOptions}"> <StackLayout StyleClass="box" IsVisible="{Binding ShowOwnershipOptions}">
<StackLayout StyleClass="box-row-header"> <StackLayout StyleClass="box-row-header">
@@ -688,7 +790,8 @@
x:Name="_ownershipPicker" x:Name="_ownershipPicker"
ItemsSource="{Binding OwnershipOptions, Mode=OneTime}" ItemsSource="{Binding OwnershipOptions, Mode=OneTime}"
SelectedIndex="{Binding OwnershipSelectedIndex}" SelectedIndex="{Binding OwnershipSelectedIndex}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemOwnershipPicker" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowCollections}"> <StackLayout StyleClass="box" IsVisible="{Binding ShowCollections}">
@@ -699,7 +802,8 @@
<StackLayout Spacing="0" Padding="0" <StackLayout Spacing="0" Padding="0"
IsVisible="{Binding HasCollections, Converter={StaticResource inverseBool}}"> IsVisible="{Binding HasCollections, Converter={StaticResource inverseBool}}">
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout StyleClass="box-row, box-row-switch">
<Label Text="{u:I18n NoCollectionsToList}" /> <Label Text="{u:I18n NoCollectionsToList}"
AutomationId="NoCollectionsToListLabel" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
</StackLayout> </StackLayout>
@@ -709,15 +813,17 @@
<controls:RepeaterView.ItemTemplate> <controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:CollectionViewModel"> <DataTemplate x:DataType="pages:CollectionViewModel">
<StackLayout Spacing="0" Padding="0"> <StackLayout Spacing="0" Padding="0">
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout StyleClass="box-row, box-row-switch" AutomationId="CollectionItemCell">
<Label <Label
Text="{Binding Collection.Name}" Text="{Binding Collection.Name}"
StyleClass="box-label-regular" StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" /> HorizontalOptions="StartAndExpand"
AutomationId="CollectionItemNameLabel" />
<Switch <Switch
IsToggled="{Binding Checked}" IsToggled="{Binding Checked}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End"
AutomationId="CollectionItemSwitch" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
</StackLayout> </StackLayout>
@@ -727,5 +833,4 @@
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
</ScrollView> </ScrollView>
</pages:BaseContentPage> </pages:BaseContentPage>

View File

@@ -49,16 +49,16 @@
x:Name="_cloneItem" x:Key="cloneItem" /> x:Name="_cloneItem" x:Key="cloneItem" />
<DataTemplate x:Key="TextCustomFieldDataTemplate"> <DataTemplate x:Key="TextCustomFieldDataTemplate">
<il:TextCustomFieldItemLayout /> <il:TextCustomFieldItemLayout AutomationId="TextCustomFieldItem" />
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="BooleanCustomFieldDataTemplate"> <DataTemplate x:Key="BooleanCustomFieldDataTemplate">
<il:BooleanCustomFieldItemLayout /> <il:BooleanCustomFieldItemLayout AutomationId="BooleanCustomFieldItem" />
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="HiddenCustomFieldDataTemplate"> <DataTemplate x:Key="HiddenCustomFieldDataTemplate">
<il:HiddenCustomFieldItemLayout /> <il:HiddenCustomFieldItemLayout AutomationId="HiddenCustomFieldItem" />
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="LinkedCustomFieldDataTemplate"> <DataTemplate x:Key="LinkedCustomFieldDataTemplate">
<il:LinkedCustomFieldItemLayout /> <il:LinkedCustomFieldItemLayout AutomationId="LinkedCustomFieldItem" />
</DataTemplate> </DataTemplate>
<dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector" <dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector"
@@ -69,23 +69,26 @@
<ScrollView x:Key="scrollView" x:Name="_scrollView"> <ScrollView x:Key="scrollView" x:Name="_scrollView">
<StackLayout Spacing="20" x:Name="_mainLayout"> <StackLayout Spacing="20" x:Name="_mainLayout">
<StackLayout StyleClass="box"> <StackLayout StyleClass="box" AutomationId="ItemInformationSection">
<StackLayout StyleClass="box-row-header"> <StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n ItemInformation, Header=True}" <Label Text="{u:I18n ItemInformation, Header=True}"
StyleClass="box-header, box-header-platform" /> StyleClass="box-header, box-header-platform" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row"> <StackLayout StyleClass="box-row" AutomationId="ItemRow">
<Label <Label
Text="{u:I18n Name}" Text="{u:I18n Name}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Name, Mode=OneWay}" Text="{Binding Cipher.Name, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
<StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0"> <StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0">
<Grid StyleClass="box-row" <Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
@@ -98,12 +101,14 @@
Text="{u:I18n Username}" Text="{u:I18n Username}"
StyleClass="box-label" StyleClass="box-label"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" /> Grid.Column="0"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Login.Username, Mode=OneWay}" Text="{Binding Cipher.Login.Username, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" /> Grid.Column="0"
AutomationId="ItemValue" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}" Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -113,12 +118,14 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyUsername}" /> AutomationProperties.Name="{u:I18n CopyUsername}"
AutomationId="CopyValueButton" />
</Grid> </Grid>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}" />
<Grid StyleClass="box-row" <Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
@@ -133,20 +140,23 @@
Text="{u:I18n Password}" Text="{u:I18n Password}"
StyleClass="box-label" StyleClass="box-label"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" /> Grid.Column="0"
AutomationId="ItemName" />
<controls:MonoLabel <controls:MonoLabel
Text="{Binding Cipher.Login.MaskedPassword, Mode=OneWay}" Text="{Binding Cipher.Login.MaskedPassword, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
IsVisible="{Binding ShowPassword, Converter={StaticResource inverseBool}}" /> IsVisible="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
AutomationId="ItemValue" />
<controls:MonoLabel <controls:MonoLabel
Text="{Binding ColoredPassword, Mode=OneWay}" Text="{Binding ColoredPassword, Mode=OneWay}"
StyleClass="box-value, text-html" StyleClass="box-value, text-html"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
LineBreakMode="CharacterWrap" LineBreakMode="CharacterWrap"
IsVisible="{Binding ShowPassword}" /> IsVisible="{Binding ShowPassword}"
AutomationId="ItemValue" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}" Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
@@ -156,7 +166,8 @@
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CheckPassword}" AutomationProperties.Name="{u:I18n CheckPassword}"
IsVisible="{Binding Cipher.ViewPassword}" /> IsVisible="{Binding Cipher.ViewPassword}"
AutomationId="CheckPasswordButton" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}" Text="{Binding ShowPasswordIcon}"
@@ -167,7 +178,8 @@
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
IsVisible="{Binding Cipher.ViewPassword}" /> IsVisible="{Binding Cipher.ViewPassword}"
AutomationId="ShowValueButton" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}" Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -178,11 +190,14 @@
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyPassword}" AutomationProperties.Name="{u:I18n CopyPassword}"
IsVisible="{Binding Cipher.ViewPassword}" /> IsVisible="{Binding Cipher.ViewPassword}"
AutomationId="CopyValueButton" />
</Grid> </Grid>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}" />
<Grid StyleClass="box-row" IsVisible="{Binding ShowTotp}"> <Grid StyleClass="box-row"
IsVisible="{Binding ShowTotp}"
AutomationId="ItemRow">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
@@ -197,7 +212,8 @@
Text="{u:I18n VerificationCodeTotp}" Text="{u:I18n VerificationCodeTotp}"
StyleClass="box-label" StyleClass="box-label"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" /> Grid.Column="0"
AutomationId="ItemName" />
<controls:MonoLabel <controls:MonoLabel
Text="{Binding TotpCodeFormatted, Mode=OneWay}" Text="{Binding TotpCodeFormatted, Mode=OneWay}"
IsVisible="{Binding ShowUpgradePremiumTotpText, Converter={StaticResource inverseBool}}" IsVisible="{Binding ShowUpgradePremiumTotpText, Converter={StaticResource inverseBool}}"
@@ -205,14 +221,16 @@
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
VerticalTextAlignment="Start" VerticalTextAlignment="Start"
VerticalOptions="Start" /> VerticalOptions="Start"
AutomationId="ItemValue" />
<controls:CircularProgressbarView <controls:CircularProgressbarView
Progress="{Binding TotpProgress}" Progress="{Binding TotpProgress}"
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" /> VerticalOptions="FillAndExpand"
AutomationId="LoginTotpProgressBar" />
<Label <Label
Text="{Binding TotpSec, Mode=OneWay}" Text="{Binding TotpSec, Mode=OneWay}"
Style="{DynamicResource textTotp}" Style="{DynamicResource textTotp}"
@@ -234,7 +252,8 @@
Grid.Column="2" Grid.Column="2"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyTotp}" /> AutomationProperties.Name="{u:I18n CopyTotp}"
AutomationId="CopyValueButton" />
<Label <Label
Text="{u:I18n PremiumSubscriptionRequired}" Text="{u:I18n PremiumSubscriptionRequired}"
StyleClass="box-footer-label" StyleClass="box-footer-label"
@@ -242,24 +261,29 @@
Margin="0,5,0,2" Margin="0,5,0,2"
Grid.Column="0" Grid.Column="0"
Grid.Row="1" Grid.Row="1"
HorizontalOptions="FillAndExpand" /> HorizontalOptions="FillAndExpand"
AutomationId="ShowUpgradePremiumTotpLabel" />
</Grid> </Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowTotp}" /> <BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowTotp}" />
</StackLayout> </StackLayout>
<StackLayout IsVisible="{Binding IsCard}" Spacing="0" Padding="0"> <StackLayout IsVisible="{Binding IsCard}" Spacing="0" Padding="0">
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Label <Label
Text="{u:I18n CardholderName}" Text="{u:I18n CardholderName}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Card.CardholderName, Mode=OneWay}" Text="{Binding Cipher.Card.CardholderName, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}" />
<Grid StyleClass="box-row" <Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
@@ -273,19 +297,22 @@
Text="{u:I18n Number}" Text="{u:I18n Number}"
StyleClass="box-label" StyleClass="box-label"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" /> Grid.Column="0"
AutomationId="ItemName" />
<controls:MonoLabel <controls:MonoLabel
Text="{Binding Cipher.Card.MaskedNumber, Mode=OneWay}" Text="{Binding Cipher.Card.MaskedNumber, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
IsVisible="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}" /> IsVisible="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}"
AutomationId="ItemValue" />
<controls:MonoLabel <controls:MonoLabel
Text="{Binding Cipher.Card.Number, Mode=OneWay}" Text="{Binding Cipher.Card.Number, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
IsVisible="{Binding ShowCardNumber}" /> IsVisible="{Binding ShowCardNumber}"
AutomationId="ItemValue" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowCardNumberIcon}" Text="{Binding ShowCardNumberIcon}"
@@ -294,7 +321,8 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" /> AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationId="ShowValueButton" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}" Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -304,34 +332,42 @@
Grid.Column="2" Grid.Column="2"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyNumber}" /> AutomationProperties.Name="{u:I18n CopyNumber}"
AutomationId="CopyValueButton" />
</Grid> </Grid>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Label <Label
Text="{u:I18n Brand}" Text="{u:I18n Brand}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Card.Brand, Mode=OneWay}" Text="{Binding Cipher.Card.Brand, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Label <Label
Text="{u:I18n Expiration}" Text="{u:I18n Expiration}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Card.Expiration, Mode=OneWay}" Text="{Binding Cipher.Card.Expiration, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}" />
<Grid StyleClass="box-row" <Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
@@ -345,19 +381,22 @@
Text="{u:I18n SecurityCode}" Text="{u:I18n SecurityCode}"
StyleClass="box-label" StyleClass="box-label"
Grid.Row="0" Grid.Row="0"
Grid.Column="0" /> Grid.Column="0"
AutomationId="ItemName" />
<controls:MonoLabel <controls:MonoLabel
Text="{Binding Cipher.Card.MaskedCode, Mode=OneWay}" Text="{Binding Cipher.Card.MaskedCode, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
IsVisible="{Binding ShowCardCode, Converter={StaticResource inverseBool}}" /> IsVisible="{Binding ShowCardCode, Converter={StaticResource inverseBool}}"
AutomationId="ItemValue" />
<controls:MonoLabel <controls:MonoLabel
Text="{Binding Cipher.Card.Code, Mode=OneWay}" Text="{Binding Cipher.Card.Code, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
IsVisible="{Binding ShowCardCode}" /> IsVisible="{Binding ShowCardCode}"
AutomationId="ItemValue" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowCardCodeIcon}" Text="{Binding ShowCardCodeIcon}"
@@ -366,7 +405,8 @@
Grid.Column="1" Grid.Column="1"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" /> AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationId="ShowValueButton" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}" Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -376,124 +416,156 @@
Grid.Column="2" Grid.Column="2"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopySecurityCode}" /> AutomationProperties.Name="{u:I18n CopySecurityCode}"
AutomationId="CopyValueButton" />
</Grid> </Grid>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}" />
</StackLayout> </StackLayout>
<StackLayout IsVisible="{Binding IsIdentity}" Spacing="0" Padding="0"> <StackLayout IsVisible="{Binding IsIdentity}" Spacing="0" Padding="0">
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Label <Label
Text="{u:I18n IdentityName}" Text="{u:I18n IdentityName}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Identity.FullName, Mode=OneWay}" Text="{Binding Cipher.Identity.FullName, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Label <Label
Text="{u:I18n Username}" Text="{u:I18n Username}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Identity.Username, Mode=OneWay}" Text="{Binding Cipher.Identity.Username, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Label <Label
Text="{u:I18n Company}" Text="{u:I18n Company}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Identity.Company, Mode=OneWay}" Text="{Binding Cipher.Identity.Company, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Label <Label
Text="{u:I18n SSN}" Text="{u:I18n SSN}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Identity.SSN, Mode=OneWay}" Text="{Binding Cipher.Identity.SSN, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Label <Label
Text="{u:I18n PassportNumber}" Text="{u:I18n PassportNumber}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Identity.PassportNumber, Mode=OneWay}" Text="{Binding Cipher.Identity.PassportNumber, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Label <Label
Text="{u:I18n LicenseNumber}" Text="{u:I18n LicenseNumber}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Identity.LicenseNumber, Mode=OneWay}" Text="{Binding Cipher.Identity.LicenseNumber, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
<Label <Label
Text="{u:I18n Email}" Text="{u:I18n Email}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Identity.Email, Mode=OneWay}" Text="{Binding Cipher.Identity.Email, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}"> IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow" >
<Label <Label
Text="{u:I18n Phone}" Text="{u:I18n Phone}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Identity.Phone, Mode=OneWay}" Text="{Binding Cipher.Identity.Phone, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="ItemValue" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" <BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}" /> IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" IsVisible="{Binding ShowIdentityAddress}"> <StackLayout StyleClass="box-row" IsVisible="{Binding ShowIdentityAddress}"
AutomationId="ItemRow">
<Label <Label
Text="{u:I18n Address}" Text="{u:I18n Address}"
StyleClass="box-label" /> StyleClass="box-label"
AutomationId="ItemName" />
<Label <Label
Text="{Binding Cipher.Identity.Address1, Mode=OneWay}" Text="{Binding Cipher.Identity.Address1, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Address1, Converter={StaticResource stringHasValue}}" IsVisible="{Binding Cipher.Identity.Address1, Converter={StaticResource stringHasValue}}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="IdentityAddressOneLabel" />
<Label <Label
Text="{Binding Cipher.Identity.Address2, Mode=OneWay}" Text="{Binding Cipher.Identity.Address2, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Address2, Converter={StaticResource stringHasValue}}" IsVisible="{Binding Cipher.Identity.Address2, Converter={StaticResource stringHasValue}}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="IdentityAddressTwoLabel" />
<Label <Label
Text="{Binding Cipher.Identity.Address3, Mode=OneWay}" Text="{Binding Cipher.Identity.Address3, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Address3, Converter={StaticResource stringHasValue}}" IsVisible="{Binding Cipher.Identity.Address3, Converter={StaticResource stringHasValue}}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="IdentityAddressThreeLabel" />
<Label <Label
Text="{Binding Cipher.Identity.FullAddressPart2, Mode=OneWay}" Text="{Binding Cipher.Identity.FullAddressPart2, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.FullAddressPart2, Converter={StaticResource stringHasValue}}" IsVisible="{Binding Cipher.Identity.FullAddressPart2, Converter={StaticResource stringHasValue}}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="IdentityFullAddressPartTwoLabel" />
<Label <Label
Text="{Binding Cipher.Identity.Country, Mode=OneWay}" Text="{Binding Cipher.Identity.Country, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Country, Converter={StaticResource stringHasValue}}" IsVisible="{Binding Cipher.Identity.Country, Converter={StaticResource stringHasValue}}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="IdentityCountryLabel" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowIdentityAddress}" /> <BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowIdentityAddress}" />
</StackLayout> </StackLayout>
@@ -503,11 +575,11 @@
<Label Text="{u:I18n URIs, Header=True}" <Label Text="{u:I18n URIs, Header=True}"
StyleClass="box-header, box-header-platform" /> StyleClass="box-header, box-header-platform" />
</StackLayout> </StackLayout>
<controls:RepeaterView ItemsSource="{Binding Cipher.Login.Uris}"> <controls:RepeaterView ItemsSource="{Binding Cipher.Login.Uris}" AutomationId="CipherUriContainer">
<controls:RepeaterView.ItemTemplate> <controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="views:LoginUriView"> <DataTemplate x:DataType="views:LoginUriView">
<StackLayout Spacing="0" Padding="0"> <StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row"> <Grid StyleClass="box-row" AutomationId="UriRow">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
@@ -533,7 +605,8 @@
Text="{Binding HostOrUri, Mode=OneWay}" Text="{Binding HostOrUri, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
Grid.Row="1" Grid.Row="1"
Grid.Column="0" /> Grid.Column="0"
AutomationId="UriValue" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.ShareSquare}}" Text="{Binding Source={x:Static core:BitwardenIcons.ShareSquare}}"
@@ -544,7 +617,8 @@
Grid.RowSpan="2" Grid.RowSpan="2"
IsVisible="{Binding CanLaunch, Mode=OneWay}" IsVisible="{Binding CanLaunch, Mode=OneWay}"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Launch}" /> AutomationProperties.Name="{u:I18n Launch}"
AutomationId="LaunchUriButton" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}" Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -554,7 +628,8 @@
Grid.Column="2" Grid.Column="2"
Grid.RowSpan="2" Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Copy}" /> AutomationProperties.Name="{u:I18n Copy}"
AutomationId="CopyUriButton" />
</Grid> </Grid>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
</StackLayout> </StackLayout>
@@ -568,14 +643,15 @@
<Label Text="{u:I18n Notes, Header=True}" <Label Text="{u:I18n Notes, Header=True}"
StyleClass="box-header, box-header-platform" /> StyleClass="box-header, box-header-platform" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-row"> <StackLayout StyleClass="box-row" AutomationId="NotesRow">
<controls:SelectableLabel <controls:SelectableLabel
Text="{Binding Cipher.Notes, Mode=OneWay}" Text="{Binding Cipher.Notes, Mode=OneWay}"
StyleClass="box-value" /> StyleClass="box-value"
AutomationId="CipherNotesLabel" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
</StackLayout> </StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding Cipher.HasFields}"> <StackLayout StyleClass="box" IsVisible="{Binding Cipher.HasFields}" AutomationId="CustomFieldsContainer">
<StackLayout StyleClass="box-row-header"> <StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n CustomFields, Header=True}" <Label Text="{u:I18n CustomFields, Header=True}"
StyleClass="box-header, box-header-platform" /> StyleClass="box-header, box-header-platform" />
@@ -590,21 +666,23 @@
<Label Text="{u:I18n Attachments, Header=True}" <Label Text="{u:I18n Attachments, Header=True}"
StyleClass="box-header, box-header-platform" /> StyleClass="box-header, box-header-platform" />
</StackLayout> </StackLayout>
<controls:RepeaterView ItemsSource="{Binding Cipher.Attachments}"> <controls:RepeaterView ItemsSource="{Binding Cipher.Attachments}" AutomationId="CipherAttachmentsContainer">
<controls:RepeaterView.ItemTemplate> <controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="views:AttachmentView"> <DataTemplate x:DataType="views:AttachmentView">
<StackLayout Spacing="0" Padding="0"> <StackLayout Spacing="0" Padding="0">
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10"> <StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10" AutomationId="CipherAttachment">
<Label <Label
Text="{Binding FileName, Mode=OneWay}" Text="{Binding FileName, Mode=OneWay}"
StyleClass="box-value" StyleClass="box-value"
VerticalTextAlignment="Center" VerticalTextAlignment="Center"
HorizontalOptions="StartAndExpand" /> HorizontalOptions="StartAndExpand"
AutomationId="CipherAttachmentFileNameLabel" />
<Label <Label
Text="{Binding SizeName, Mode=OneWay}" Text="{Binding SizeName, Mode=OneWay}"
StyleClass="box-sub-label" StyleClass="box-sub-label"
HorizontalTextAlignment="End" HorizontalTextAlignment="End"
VerticalTextAlignment="Center" /> VerticalTextAlignment="Center"
AutomationId="CipherAttachmentFileSizeLabel" />
<controls:IconButton <controls:IconButton
StyleClass="box-row-button, box-row-button-platform" StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Download}}" Text="{Binding Source={x:Static core:BitwardenIcons.Download}}"
@@ -612,7 +690,8 @@
CommandParameter="{Binding .}" CommandParameter="{Binding .}"
VerticalOptions="Center" VerticalOptions="Center"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Download}" /> AutomationProperties.Name="{u:I18n Download}"
AutomationId="CipherAttachmentDownloadButton" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
</StackLayout> </StackLayout>
@@ -622,17 +701,20 @@
</StackLayout> </StackLayout>
<StackLayout StyleClass="box-bottom"> <StackLayout StyleClass="box-bottom">
<Label FormattedText="{Binding UpdatedText}" <Label FormattedText="{Binding UpdatedText}"
StyleClass="box-footer-label" /> StyleClass="box-footer-label"
AutomationId="CipherUpdatedDateLabel" />
<Label FormattedText="{Binding PasswordUpdatedText}" <Label FormattedText="{Binding PasswordUpdatedText}"
StyleClass="box-footer-label" StyleClass="box-footer-label"
IsVisible="{Binding Cipher.PasswordRevisionDisplayDate, Converter={StaticResource notNull}}"> IsVisible="{Binding Cipher.PasswordRevisionDisplayDate, Converter={StaticResource notNull}}"
AutomationId="CipherUpdatedPasswordDateLabel">
<Label.GestureRecognizers> <Label.GestureRecognizers>
<TapGestureRecognizer Tapped="PasswordHistory_Tapped" /> <TapGestureRecognizer Tapped="PasswordHistory_Tapped" />
</Label.GestureRecognizers> </Label.GestureRecognizers>
</Label> </Label>
<Label FormattedText="{Binding PasswordHistoryText}" <Label FormattedText="{Binding PasswordHistoryText}"
StyleClass="box-footer-label" StyleClass="box-footer-label"
IsVisible="{Binding Cipher.HasPasswordHistory}"> IsVisible="{Binding Cipher.HasPasswordHistory}"
AutomationId="CipherPasswordHistoryLabel">
<Label.GestureRecognizers> <Label.GestureRecognizers>
<TapGestureRecognizer Tapped="PasswordHistory_Tapped" /> <TapGestureRecognizer Tapped="PasswordHistory_Tapped" />
</Label.GestureRecognizers> </Label.GestureRecognizers>
@@ -662,6 +744,7 @@
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize" AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n EditItem}" AutomationProperties.Name="{u:I18n EditItem}"
AutomationId="CipherEditButton"
IsVisible="{Binding CanEdit}"> IsVisible="{Binding CanEdit}">
<Button.Effects> <Button.Effects>
<effects:FabShadowEffect /> <effects:FabShadowEffect />

View File

@@ -19,7 +19,8 @@
Priority="-1" Priority="-1"
UseOriginalImage="True" UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" /> AutomationProperties.Name="{u:I18n Account}"
AutomationId="AccountIconButton" />
<ToolbarItem IconImageSource="search.png" Clicked="Search_Clicked" <ToolbarItem IconImageSource="search.png" Clicked="Search_Clicked"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Search}" /> AutomationProperties.Name="{u:I18n Search}" />

View File

@@ -41,7 +41,8 @@
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
TextChanged="SearchBar_TextChanged" TextChanged="SearchBar_TextChanged"
SearchButtonPressed="SearchBar_SearchButtonPressed" SearchButtonPressed="SearchBar_SearchButtonPressed"
Placeholder="{Binding PageTitle}" /> Placeholder="{Binding PageTitle}"
AutomationId="SearchBar" />
</StackLayout> </StackLayout>
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" <BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform"
x:Name="_separator" x:Key="separator" /> x:Name="_separator" x:Key="separator" />
@@ -91,7 +92,8 @@
Source="empty_items_state" /> Source="empty_items_state" />
<Label <Label
Text="{u:I18n ThereAreNoItemsThatMatchTheSearch}" Text="{u:I18n ThereAreNoItemsThatMatchTheSearch}"
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center"
AutomationId="NoSearchResultsLabel" />
<Button <Button
Text="{u:I18n AddAnItem}" Text="{u:I18n AddAnItem}"
Command="{Binding AddCipherCommand}" Command="{Binding AddCipherCommand}"
@@ -104,7 +106,8 @@
SelectionMode="Single" SelectionMode="Single"
SelectionChanged="RowSelected" SelectionChanged="RowSelected"
StyleClass="list, list-platform" StyleClass="list, list-platform"
ExtraDataForLogging="Ciphers Page"> ExtraDataForLogging="Ciphers Page"
AutomationId="CipherList">
<CollectionView.ItemTemplate> <CollectionView.ItemTemplate>
<DataTemplate x:DataType="views:CipherView"> <DataTemplate x:DataType="views:CipherView">
<controls:CipherViewCell <controls:CipherViewCell

View File

@@ -208,6 +208,11 @@ namespace Bit.App.Pages
} }
else if (selection == AppResources.Autofill || selection == AppResources.AutofillAndSave) else if (selection == AppResources.Autofill || selection == AppResources.AutofillAndSave)
{ {
if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync())
{
return;
}
if (selection == AppResources.AutofillAndSave) if (selection == AppResources.AutofillAndSave)
{ {
var uris = cipher.Login?.Uris?.ToList(); var uris = cipher.Login?.Uris?.ToList();

View File

@@ -25,7 +25,8 @@
Priority="-1" Priority="-1"
UseOriginalImage="True" UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" /> AutomationProperties.Name="{u:I18n Account}"
AutomationId="AccountIconButton" />
<ToolbarItem Icon="search.png" Clicked="Search_Clicked" <ToolbarItem Icon="search.png" Clicked="Search_Clicked"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Search}" /> AutomationProperties.Name="{u:I18n Search}" />
@@ -59,13 +60,14 @@
<controls:AuthenticatorViewCell <controls:AuthenticatorViewCell
Cipher="{Binding Cipher}" Cipher="{Binding Cipher}"
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}"
TotpSec="{Binding TotpSec}"/> TotpSec="{Binding TotpSec}" />
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="groupTemplate" <DataTemplate x:Key="groupTemplate"
x:DataType="pages:GroupingsPageListItem"> x:DataType="pages:GroupingsPageListItem">
<controls:ExtendedStackLayout Orientation="Horizontal" <controls:ExtendedStackLayout Orientation="Horizontal"
StyleClass="list-row, list-row-platform"> StyleClass="list-row, list-row-platform"
AutomationId="{Binding AutomationId}">
<controls:IconLabel Text="{Binding Icon, Mode=OneWay}" <controls:IconLabel Text="{Binding Icon, Mode=OneWay}"
HorizontalOptions="Start" HorizontalOptions="Start"
VerticalOptions="Center" VerticalOptions="Center"
@@ -79,12 +81,14 @@
LineBreakMode="TailTruncation" LineBreakMode="TailTruncation"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
StyleClass="list-title"/> StyleClass="list-title"
AutomationId="ItemNameLabel" />
<Label Text="{Binding ItemCount, Mode=OneWay}" <Label Text="{Binding ItemCount, Mode=OneWay}"
HorizontalOptions="End" HorizontalOptions="End"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
HorizontalTextAlignment="End" HorizontalTextAlignment="End"
StyleClass="list-sub"/> StyleClass="list-sub"
AutomationId="ItemCountLabel" />
</controls:ExtendedStackLayout> </controls:ExtendedStackLayout>
</DataTemplate> </DataTemplate>
@@ -95,7 +99,8 @@
Spacing="0" Spacing="0"
Padding="0" Padding="0"
VerticalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
StyleClass="list-row-header-container, list-row-header-container-platform"> StyleClass="list-row-header-container, list-row-header-container-platform"
AutomationId="{Binding AutomationId}">
<BoxView <BoxView
StyleClass="list-section-separator-top, list-section-separator-top-platform" /> StyleClass="list-section-separator-top, list-section-separator-top-platform" />
<StackLayout StyleClass="list-row-header, list-row-header-platform"> <StackLayout StyleClass="list-row-header, list-row-header-platform">
@@ -104,7 +109,8 @@
StyleClass="list-header, list-header-platform" /> StyleClass="list-header, list-header-platform" />
<Label <Label
Text="{Binding ItemCount}" Text="{Binding ItemCount}"
StyleClass="list-header-sub" /> StyleClass="list-header-sub"
AutomationId="SectionItemCount" />
</StackLayout> </StackLayout>
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" /> <BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" />
</StackLayout> </StackLayout>
@@ -147,7 +153,8 @@
IsVisible="{Binding ShowNoData}"> IsVisible="{Binding ShowNoData}">
<Label <Label
Text="{Binding NoDataText}" Text="{Binding NoDataText}"
HorizontalTextAlignment="Center"></Label> HorizontalTextAlignment="Center"
AutomationId="NoDataDisplayed"></Label>
<Button <Button
Text="{u:I18n AddAnItem}" Text="{u:I18n AddAnItem}"
Clicked="AddButton_Clicked" Clicked="AddButton_Clicked"

View File

@@ -1,4 +1,6 @@
namespace Bit.App.Pages using Bit.App.Utilities.Automation;
namespace Bit.App.Pages
{ {
public class GroupingsPageHeaderListItem : IGroupingsPageListItem public class GroupingsPageHeaderListItem : IGroupingsPageListItem
{ {
@@ -10,5 +12,12 @@
public string Title { get; } public string Title { get; }
public string ItemCount { get; set; } public string ItemCount { get; set; }
public string AutomationId
{
get
{
return AutomationIdsHelper.AddSuffixFor(AutomationIdsHelper.ToEnglishTitleCase(Title), SuffixType.Header);
}
}
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Bit.App.Utilities.Automation;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@@ -32,5 +33,6 @@ namespace Bit.App.Pages
public string Name { get; set; } public string Name { get; set; }
public string NameShort => string.IsNullOrWhiteSpace(Name) || Name.Length == 0 ? "-" : Name[0].ToString(); public string NameShort => string.IsNullOrWhiteSpace(Name) || Name.Length == 0 ? "-" : Name[0].ToString();
public string ItemCount { get; set; } public string ItemCount { get; set; }
public string AutomationId => AutomationIdsHelper.AddSuffixFor(NameShort, SuffixType.ListGroup);
} }
} }

View File

@@ -1,4 +1,5 @@
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities.Automation;
using Bit.Core; using Bit.Core;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.View; using Bit.Core.Models.View;
@@ -115,5 +116,38 @@ namespace Bit.App.Pages
return _icon; return _icon;
} }
} }
public string AutomationId
{
get
{
if (Type != null)
{
return AutomationIdsHelper.AddSuffixFor(System.Enum.GetName(typeof(CipherType), Type.Value), SuffixType.Filter);
}
if (IsTrash)
{
return AutomationIdsHelper.AddSuffixFor("Trash", SuffixType.Filter);
}
if (Folder != null)
{
return AutomationIdsHelper.AddSuffixFor("Folder", SuffixType.Filter);
}
if (Collection != null)
{
return AutomationIdsHelper.AddSuffixFor("Collection", SuffixType.Filter);
}
if (IsTotpCode)
{
return AutomationIdsHelper.AddSuffixFor("TOTP", SuffixType.ListItem);
}
return null;
}
}
} }
} }

View File

@@ -330,6 +330,8 @@ namespace Bit.App.Pages
items.AddRange(itemGroup); items.AddRange(itemGroup);
} }
Device.BeginInvokeOnMainThread(() =>
{
if (Device.RuntimePlatform == Device.iOS) if (Device.RuntimePlatform == Device.iOS)
{ {
// HACK: [PS-536] Fix to avoid blank list after back navigation on unlocking with previous page info // HACK: [PS-536] Fix to avoid blank list after back navigation on unlocking with previous page info
@@ -337,6 +339,7 @@ namespace Bit.App.Pages
GroupedItems.Clear(); GroupedItems.Clear();
} }
GroupedItems.ReplaceRange(items); GroupedItems.ReplaceRange(items);
});
} }
else else
{ {
@@ -356,6 +359,8 @@ namespace Bit.App.Pages
items.AddRange(itemGroup); items.AddRange(itemGroup);
} }
Device.BeginInvokeOnMainThread(() =>
{
if (groupedItems.Any()) if (groupedItems.Any())
{ {
if (Device.RuntimePlatform == Device.iOS) if (Device.RuntimePlatform == Device.iOS)
@@ -371,6 +376,7 @@ namespace Bit.App.Pages
{ {
GroupedItems.Clear(); GroupedItems.Clear();
} }
});
} }
} }
finally finally
@@ -378,9 +384,12 @@ namespace Bit.App.Pages
_doingLoad = false; _doingLoad = false;
Loaded = true; Loaded = true;
Loading = false; Loading = false;
Device.BeginInvokeOnMainThread(() =>
{
ShowNoData = (MainPage && !HasCiphers) || !groupedItems.Any(); ShowNoData = (MainPage && !HasCiphers) || !groupedItems.Any();
ShowList = !ShowNoData; ShowList = !ShowNoData;
DisableRefreshing(); DisableRefreshing();
});
} }
} }

View File

@@ -202,6 +202,24 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Biometric unlock for this account is disabled pending verification of master password..
/// </summary>
public static string AccountBiometricInvalidated {
get {
return ResourceManager.GetString("AccountBiometricInvalidated", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Autofill biometric unlock for this account is disabled pending verification of master password..
/// </summary>
public static string AccountBiometricInvalidatedExtension {
get {
return ResourceManager.GetString("AccountBiometricInvalidatedExtension", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Your new account has been created! You may now log in.. /// Looks up a localized string similar to Your new account has been created! You may now log in..
/// </summary> /// </summary>
@@ -400,6 +418,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Admin approval requested.
/// </summary>
public static string AdminApprovalRequested {
get {
return ResourceManager.GetString("AdminApprovalRequested", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to All. /// Looks up a localized string similar to All.
/// </summary> /// </summary>
@@ -544,6 +571,24 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Approve with master password.
/// </summary>
public static string ApproveWithMasterPassword {
get {
return ResourceManager.GetString("ApproveWithMasterPassword", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Approve with my other device.
/// </summary>
public static string ApproveWithMyOtherDevice {
get {
return ResourceManager.GetString("ApproveWithMyOtherDevice", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to April. /// Looks up a localized string similar to April.
/// </summary> /// </summary>
@@ -778,15 +823,6 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Auto-fill will not be offered for blocked URIs. Separate multiple URIs with a comma. For example: &quot;https://twitter.com, androidapp://com.twitter.android&quot;..
/// </summary>
public static string AutofillBlockedUrisDescription {
get {
return ResourceManager.GetString("AutofillBlockedUrisDescription", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Do you want to auto-fill or view this item?. /// Looks up a localized string similar to Do you want to auto-fill or view this item?.
/// </summary> /// </summary>
@@ -922,6 +958,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Auto-fill will not be offered for these URIs..
/// </summary>
public static string AutoFillWillNotBeOfferedForTheseURIs {
get {
return ResourceManager.GetString("AutoFillWillNotBeOfferedForTheseURIs", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Auto-fill with Bitwarden. /// Looks up a localized string similar to Auto-fill with Bitwarden.
/// </summary> /// </summary>
@@ -967,24 +1012,6 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Biometric unlock disabled pending verification of master password..
/// </summary>
public static string AccountBiometricInvalidated {
get {
return ResourceManager.GetString("AccountBiometricInvalidated", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Biometric unlock for autofill disabled pending verification of master password..
/// </summary>
public static string AccountBiometricInvalidatedExtension {
get {
return ResourceManager.GetString("AccountBiometricInvalidatedExtension", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Biometrics. /// Looks up a localized string similar to Biometrics.
/// </summary> /// </summary>
@@ -1228,6 +1255,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Block auto-fill.
/// </summary>
public static string BlockAutoFill {
get {
return ResourceManager.GetString("BlockAutoFill", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Brand. /// Looks up a localized string similar to Brand.
/// </summary> /// </summary>
@@ -1264,6 +1300,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Cannot edit multiple URIs at once.
/// </summary>
public static string CannotEditMultipleURIsAtOnce {
get {
return ResourceManager.GetString("CannotEditMultipleURIsAtOnce", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Cannot open the app &quot;{0}&quot;.. /// Looks up a localized string similar to Cannot open the app &quot;{0}&quot;..
/// </summary> /// </summary>
@@ -1750,6 +1795,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Data region.
/// </summary>
public static string DataRegion {
get {
return ResourceManager.GetString("DataRegion", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Password updated. /// Looks up a localized string similar to Password updated.
/// </summary> /// </summary>
@@ -2137,6 +2191,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Edit URI.
/// </summary>
public static string EditURI {
get {
return ResourceManager.GetString("EditURI", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Email. /// Looks up a localized string similar to Email.
/// </summary> /// </summary>
@@ -2290,6 +2353,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Enter URI.
/// </summary>
public static string EnterURI {
get {
return ResourceManager.GetString("EnterURI", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Enter the 6 digit verification code from your authenticator app.. /// Looks up a localized string similar to Enter the 6 digit verification code from your authenticator app..
/// </summary> /// </summary>
@@ -2326,6 +2398,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to EU.
/// </summary>
public static string EU {
get {
return ResourceManager.GetString("EU", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Exact. /// Looks up a localized string similar to Exact.
/// </summary> /// </summary>
@@ -2929,6 +3010,24 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Format: {0}.
/// </summary>
public static string FormatX {
get {
return ResourceManager.GetString("FormatX", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Format: {0}. Separate multiple URIs with a comma..
/// </summary>
public static string FormatXSeparateMultipleURIsWithAComma {
get {
return ResourceManager.GetString("FormatXSeparateMultipleURIsWithAComma", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Forwarded email alias. /// Looks up a localized string similar to Forwarded email alias.
/// </summary> /// </summary>
@@ -3244,6 +3343,24 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Invalid API key.
/// </summary>
public static string InvalidAPIKey {
get {
return ResourceManager.GetString("InvalidAPIKey", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Invalid API token.
/// </summary>
public static string InvalidAPIToken {
get {
return ResourceManager.GetString("InvalidAPIToken", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Invalid email address.. /// Looks up a localized string similar to Invalid email address..
/// </summary> /// </summary>
@@ -3253,6 +3370,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Invalid format. Use https://, http://, or androidapp://.
/// </summary>
public static string InvalidFormatUseHttpsHttpOrAndroidApp {
get {
return ResourceManager.GetString("InvalidFormatUseHttpsHttpOrAndroidApp", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Invalid master password. Try again.. /// Looks up a localized string similar to Invalid master password. Try again..
/// </summary> /// </summary>
@@ -3271,6 +3397,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Invalid URI.
/// </summary>
public static string InvalidURI {
get {
return ResourceManager.GetString("InvalidURI", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Invalid verification code. /// Looks up a localized string similar to Invalid verification code.
/// </summary> /// </summary>
@@ -3550,6 +3685,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Logged in!.
/// </summary>
public static string LoggedIn {
get {
return ResourceManager.GetString("LoggedIn", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Logged in as {0} on {1}.. /// Looks up a localized string similar to Logged in as {0} on {1}..
/// </summary> /// </summary>
@@ -3577,6 +3721,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Logging in as {0} on {1}.
/// </summary>
public static string LoggingInAsXOnY {
get {
return ResourceManager.GetString("LoggingInAsXOnY", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Log In. /// Looks up a localized string similar to Log In.
/// </summary> /// </summary>
@@ -3876,6 +4029,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Master password re-prompt help.
/// </summary>
public static string MasterPasswordRePromptHelp {
get {
return ResourceManager.GetString("MasterPasswordRePromptHelp", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Match detection. /// Looks up a localized string similar to Match detection.
/// </summary> /// </summary>
@@ -4173,6 +4335,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to New blocked URI.
/// </summary>
public static string NewBlockedURI {
get {
return ResourceManager.GetString("NewBlockedURI", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to New custom field. /// Looks up a localized string similar to New custom field.
/// </summary> /// </summary>
@@ -5129,6 +5300,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Region.
/// </summary>
public static string Region {
get {
return ResourceManager.GetString("Region", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Remember me. /// Looks up a localized string similar to Remember me.
/// </summary> /// </summary>
@@ -5138,6 +5318,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Remember this device.
/// </summary>
public static string RememberThisDevice {
get {
return ResourceManager.GetString("RememberThisDevice", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Remove. /// Looks up a localized string similar to Remove.
/// </summary> /// </summary>
@@ -5210,6 +5399,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Request admin approval.
/// </summary>
public static string RequestAdminApproval {
get {
return ResourceManager.GetString("RequestAdminApproval", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Request one-time password. /// Looks up a localized string similar to Request one-time password.
/// </summary> /// </summary>
@@ -5462,6 +5660,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Self-hosted.
/// </summary>
public static string SelfHosted {
get {
return ResourceManager.GetString("SelfHosted", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Self-hosted environment. /// Looks up a localized string similar to Self-hosted environment.
/// </summary> /// </summary>
@@ -6056,6 +6263,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to There are no blocked URIs.
/// </summary>
public static string ThereAreNoBlockedURIs {
get {
return ResourceManager.GetString("ThereAreNoBlockedURIs", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to There are no items in your vault that match &quot;{0}&quot;. /// Looks up a localized string similar to There are no items in your vault that match &quot;{0}&quot;.
/// </summary> /// </summary>
@@ -6074,6 +6290,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to The URI {0} is already blocked.
/// </summary>
public static string TheURIXIsAlreadyBlocked {
get {
return ResourceManager.GetString("TheURIXIsAlreadyBlocked", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to 30 days. /// Looks up a localized string similar to 30 days.
/// </summary> /// </summary>
@@ -6191,6 +6416,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Trouble logging in?.
/// </summary>
public static string TroubleLoggingIn {
get {
return ResourceManager.GetString("TroubleLoggingIn", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Try again. /// Looks up a localized string similar to Try again.
/// </summary> /// </summary>
@@ -6200,6 +6434,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Turn off using a public device.
/// </summary>
public static string TurnOffUsingPublicDevice {
get {
return ResourceManager.GetString("TurnOffUsingPublicDevice", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to 20 seconds. /// Looks up a localized string similar to 20 seconds.
/// </summary> /// </summary>
@@ -6389,6 +6632,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Unlocking may fail due to insufficient memory. Decrease your KDF memory settings to resolve..
/// </summary>
public static string UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve {
get {
return ResourceManager.GetString("UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Unlock vault. /// Looks up a localized string similar to Unlock vault.
/// </summary> /// </summary>
@@ -6515,6 +6767,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to URI removed.
/// </summary>
public static string URIRemoved {
get {
return ResourceManager.GetString("URIRemoved", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to URIs. /// Looks up a localized string similar to URIs.
/// </summary> /// </summary>
@@ -6524,6 +6785,24 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to URI saved.
/// </summary>
public static string URISaved {
get {
return ResourceManager.GetString("URISaved", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to US.
/// </summary>
public static string US {
get {
return ResourceManager.GetString("US", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Use another two-step login method. /// Looks up a localized string similar to Use another two-step login method.
/// </summary> /// </summary>
@@ -6695,6 +6974,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Vault timeout action changed to log out.
/// </summary>
public static string VaultTimeoutActionChangedToLogOut {
get {
return ResourceManager.GetString("VaultTimeoutActionChangedToLogOut", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Your organization policies have set your vault timeout action to {0}.. /// Looks up a localized string similar to Your organization policies have set your vault timeout action to {0}..
/// </summary> /// </summary>
@@ -7064,6 +7352,24 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Your request has been sent to your admin..
/// </summary>
public static string YourRequestHasBeenSentToYourAdmin {
get {
return ResourceManager.GetString("YourRequestHasBeenSentToYourAdmin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You will be notified once approved. .
/// </summary>
public static string YouWillBeNotifiedOnceApproved {
get {
return ResourceManager.GetString("YouWillBeNotifiedOnceApproved", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to To continue, hold your YubiKey NEO against the back of the device or insert your YubiKey into your device&apos;s USB port, then touch its button.. /// Looks up a localized string similar to To continue, hold your YubiKey NEO against the back of the device or insert your YubiKey into your device&apos;s USB port, then touch its button..
/// </summary> /// </summary>

View File

@@ -1753,10 +1753,10 @@ Skandering gebeur outomaties.</value>
<comment>Confirmation alert message when soft-deleting a cipher.</comment> <comment>Confirmation alert message when soft-deleting a cipher.</comment>
</data> </data>
<data name="AccountBiometricInvalidated" xml:space="preserve"> <data name="AccountBiometricInvalidated" xml:space="preserve">
<value>Biometric unlock for this account is disabled pending verification of master password.</value> <value>Biometriese ontgrendeling vir hierdie rekening is gedeaktiveer hangende bevestiging van hoofwagwoord.</value>
</data> </data>
<data name="AccountBiometricInvalidatedExtension" xml:space="preserve"> <data name="AccountBiometricInvalidatedExtension" xml:space="preserve">
<value>Autofill biometric unlock for this account is disabled pending verification of master password.</value> <value>Outovul-biometriese ontgrendeline vir hierdie rekening is gedeaktiveer hangende bevestiging van hoofwagwoord.</value>
</data> </data>
<data name="EnableSyncOnRefresh" xml:space="preserve"> <data name="EnableSyncOnRefresh" xml:space="preserve">
<value>Aktiveer sinchronisering by verfrissing</value> <value>Aktiveer sinchronisering by verfrissing</value>
@@ -2495,8 +2495,8 @@ Wil u na die rekening omskakel?</value>
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Kry hoofwagwoord wenk</value> <value>Kry hoofwagwoord wenk</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>Teken in as {0}</value> <value>Teken aan as {0} op {1}</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>Nie jy nie?</value> <value>Nie jy nie?</value>
@@ -2609,10 +2609,37 @@ Wil u na die rekening omskakel?</value>
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>Daar is geen items wat met die soekterm ooreenstem nie</value> <value>Daar is geen items wat met die soekterm ooreenstem nie</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>VS</value>
</data>
<data name="EU" xml:space="preserve">
<value>EU</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>Selghehuisves</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>Datastreek</value>
</data>
<data name="Region" xml:space="preserve">
<value>Streek</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</value> <value>U hoofwagwoord voldoen nie aan een of meer van die organisasiebeleide nie. Om toegang tot die kluis te kry, moet u nou u hoofwagwoord bywerk. Deur voort te gaan sal u van u huidige sessie afgeteken word, en u sal weer moet aanteken. Aktiewe sessies op ander toestelle kan vir tot een uur aktief bly.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>Current master password</value> <value>Huidige hoofwagwoord</value>
</data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Master password re-prompt help</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Unlocking may fail due to insufficient memory. Decrease your KDF memory settings to resolve.</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Invalid API key</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Invalid API token</value>
</data> </data>
</root> </root>

View File

@@ -1753,10 +1753,10 @@
<comment>Confirmation alert message when soft-deleting a cipher.</comment> <comment>Confirmation alert message when soft-deleting a cipher.</comment>
</data> </data>
<data name="AccountBiometricInvalidated" xml:space="preserve"> <data name="AccountBiometricInvalidated" xml:space="preserve">
<value>Biometric unlock for this account is disabled pending verification of master password.</value> <value>تم تعطيل فتح القفل الحيوي لهذا الحساب في انتظار التحقق من كلمة المرور الرئيسية.</value>
</data> </data>
<data name="AccountBiometricInvalidatedExtension" xml:space="preserve"> <data name="AccountBiometricInvalidatedExtension" xml:space="preserve">
<value>Autofill biometric unlock for this account is disabled pending verification of master password.</value> <value>إلغاء القفل الحيوي للملء التلقائي لهذا الحساب معطل في انتظار التحقق من كلمة المرور الرئيسية.</value>
</data> </data>
<data name="EnableSyncOnRefresh" xml:space="preserve"> <data name="EnableSyncOnRefresh" xml:space="preserve">
<value>تمكين المزامنة عند التحديث</value> <value>تمكين المزامنة عند التحديث</value>
@@ -2496,8 +2496,8 @@
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>احصل على تلميح كلمة المرور الرئيسية</value> <value>احصل على تلميح كلمة المرور الرئيسية</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>تسجيل الدخول كـ {0}</value> <value>تسجيل الدخول كـ {0} في {1}</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>ليس أنت؟</value> <value>ليس أنت؟</value>
@@ -2610,10 +2610,37 @@
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>لا توجد عناصر تطابق البحث</value> <value>لا توجد عناصر تطابق البحث</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>الولايات المتحدة</value>
</data>
<data name="EU" xml:space="preserve">
<value>الاتحاد الأوروبي</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>استضافة ذاتية</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>منطقة البيانات</value>
</data>
<data name="Region" xml:space="preserve">
<value>المنطقة</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>كلمة المرور الرئيسية الخاصة بك لا تفي بواحدة أو أكثر من سياسات مؤسستك. من أجل الوصول إلى الخزنة، يجب عليك تحديث كلمة المرور الرئيسية الآن. سيتم تسجيل خروجك من الجلسة الحالية، مما يتطلب منك تسجيل الدخول مرة أخرى. وقد تظل الجلسات النشطة على أجهزة أخرى نشطة لمدة تصل إلى ساعة واحدة.</value> <value>كلمة المرور الرئيسية الخاصة بك لا تفي بواحدة أو أكثر من سياسات مؤسستك. من أجل الوصول إلى الخزنة، يجب عليك تحديث كلمة المرور الرئيسية الآن. سيتم تسجيل خروجك من الجلسة الحالية، مما يتطلب منك تسجيل الدخول مرة أخرى. وقد تظل الجلسات النشطة على أجهزة أخرى نشطة لمدة تصل إلى ساعة واحدة.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>كلمة المرور الرئيسية الحالية</value> <value>كلمة المرور الرئيسية الحالية</value>
</data> </data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Master password re-prompt help</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Unlocking may fail due to insufficient memory. Decrease your KDF memory settings to resolve.</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Invalid API key</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Invalid API token</value>
</data>
</root> </root>

View File

@@ -2494,8 +2494,8 @@ Bu hesaba keçmək istəyirsiniz?</value>
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Ana parol üçün məsləhət alın</value> <value>Ana parol üçün məsləhət alın</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>{0} olaraq giriş edilir</value> <value>{1} üzərində {0} olaraq giriş edildi</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>Siz deyilsiniz?</value> <value>Siz deyilsiniz?</value>
@@ -2608,10 +2608,37 @@ Bu hesaba keçmək istəyirsiniz?</value>
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>Axtarışa uyğun gələn heç bir element yoxdur</value> <value>Axtarışa uyğun gələn heç bir element yoxdur</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>ABŞ</value>
</data>
<data name="EU" xml:space="preserve">
<value>AB</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>Öz-özünə sahiblik edən</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>Data bölgəsi</value>
</data>
<data name="Region" xml:space="preserve">
<value>Bölgə</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Anbara müraciət üçün ana parolunuzu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış etmiş və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər.</value> <value>Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Anbara müraciət üçün ana parolunuzu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış etmiş və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>Hazırkı ana parol</value> <value>Hazırkı ana parol</value>
</data> </data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Ana parolu təkrar soruş köməyi</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Yetərsiz yaddaşa görə kilid açma uğursuz ola bilər. Həll etmək üçün KDF yaddaş tənzimləmələrinizi azaldın</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Yararsız API açarı</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Yararsız API tokeni</value>
</data>
</root> </root>

View File

@@ -2495,8 +2495,8 @@
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Атрымаць падказку да асноўнага пароля</value> <value>Атрымаць падказку да асноўнага пароля</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>Увайсці як {0}</value> <value>Вы ўваходзіце як {0} у {1}</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>Не вы?</value> <value>Не вы?</value>
@@ -2609,10 +2609,37 @@
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>Адсутнічаюць элементы, якія адпавядаюць пошуку</value> <value>Адсутнічаюць элементы, якія адпавядаюць пошуку</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>ЗША</value>
</data>
<data name="EU" xml:space="preserve">
<value>ЕС</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>Уласнае размяшчэнне</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>Рэгіён даных</value>
</data>
<data name="Region" xml:space="preserve">
<value>Рэгіён</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>Ваш асноўны пароль не адпавядае адной або некалькім палітыкам арганізацыі. Для атрымання доступу да сховішча, вы павінны абнавіць яго. Працягваючы, вы выйдзіце з бягучага сеанса і вам неабходна будзе ўвайсці паўторна. Актыўныя сеансы на іншых прыладах могуць заставацца актыўнымі на працягу адной гадзіны.</value> <value>Ваш асноўны пароль не адпавядае адной або некалькім палітыкам арганізацыі. Для атрымання доступу да сховішча, вы павінны абнавіць яго. Працягваючы, вы выйдзіце з бягучага сеанса і вам неабходна будзе ўвайсці паўторна. Актыўныя сеансы на іншых прыладах могуць заставацца актыўнымі на працягу адной гадзіны.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>Бягучы асноўны пароль</value> <value>Бягучы асноўны пароль</value>
</data> </data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Дапамога з паўторным запытам асноўнага пароля</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Па прычыне недахопу памяці можа адбыцца збой разблакіроўкі. Паменшыце налады памяці KDF, каб вырашыць гэту праблему</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Памылковы ключ API</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Памылковы токен API</value>
</data>
</root> </root>

View File

@@ -2495,8 +2495,8 @@ select Add TOTP to store the key safely</value>
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Получете подсказване за главната парола</value> <value>Получете подсказване за главната парола</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>Вписване като {0}</value> <value>Влизате като {0} в {1}</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>Това не сте Вие?</value> <value>Това не сте Вие?</value>
@@ -2609,10 +2609,37 @@ select Add TOTP to store the key safely</value>
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>Няма елементи, които отговарят на търсенето</value> <value>Няма елементи, които отговарят на търсенето</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>САЩ</value>
</data>
<data name="EU" xml:space="preserve">
<value>ЕС</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>Собствен хостинг</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>Регион на данните</value>
</data>
<data name="Region" xml:space="preserve">
<value>Регион</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>Вашата главна парола не отговаря на една или повече политики на организацията Ви. За да получите достъп до трезора, трябва да промените главната си парола сега. Това означава, че ще бъдете отписан(а) от текущата си сесия и ще трябва да се впишете отново. Активните сесии на други устройства може да продължат да бъдат активни още един час.</value> <value>Вашата главна парола не отговаря на една или повече политики на организацията Ви. За да получите достъп до трезора, трябва да промените главната си парола сега. Това означава, че ще бъдете отписан(а) от текущата си сесия и ще трябва да се впишете отново. Активните сесии на други устройства може да продължат да бъдат активни още един час.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>Текуща главна парола</value> <value>Текуща главна парола</value>
</data> </data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Помощ за повторното запитване за главната парола</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Отключването може да бъде неуспешно заради недостатъчно памет. Намалете настройките на паметта за KDF, за да разрешите проблема.</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Неправилен ключ за ППИ</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Неправилен идентификатор за ППИ</value>
</data>
</root> </root>

View File

@@ -346,7 +346,7 @@
<comment>Confirmation message after successfully deleting a login.</comment> <comment>Confirmation message after successfully deleting a login.</comment>
</data> </data>
<data name="Submit" xml:space="preserve"> <data name="Submit" xml:space="preserve">
<value>Submit</value> <value>জমা দিন</value>
</data> </data>
<data name="Sync" xml:space="preserve"> <data name="Sync" xml:space="preserve">
<value>Sync</value> <value>Sync</value>
@@ -356,7 +356,7 @@
<value>ধন্যবাদ</value> <value>ধন্যবাদ</value>
</data> </data>
<data name="Tools" xml:space="preserve"> <data name="Tools" xml:space="preserve">
<value>Tools</value> <value>টুলস</value>
<comment>The title for the tools page.</comment> <comment>The title for the tools page.</comment>
</data> </data>
<data name="URI" xml:space="preserve"> <data name="URI" xml:space="preserve">
@@ -379,7 +379,7 @@
<comment>Confirmation message after successfully copying a value to the clipboard.</comment> <comment>Confirmation message after successfully copying a value to the clipboard.</comment>
</data> </data>
<data name="VerifyFingerprint" xml:space="preserve"> <data name="VerifyFingerprint" xml:space="preserve">
<value>Verify fingerprint</value> <value>আঙ্গুলের ছাপ যাচাই করুন</value>
</data> </data>
<data name="VerifyMasterPassword" xml:space="preserve"> <data name="VerifyMasterPassword" xml:space="preserve">
<value>প্রধান পাসওয়ার্ড যাচাইকরণ</value> <value>প্রধান পাসওয়ার্ড যাচাইকরণ</value>
@@ -397,7 +397,7 @@
<value>আমাদের ওয়েবসাইটে ঢু মেরে আসুন</value> <value>আমাদের ওয়েবসাইটে ঢু মেরে আসুন</value>
</data> </data>
<data name="VisitOurWebsiteDescription" xml:space="preserve"> <data name="VisitOurWebsiteDescription" xml:space="preserve">
<value>Visit our website to get help, news, email us, and/or learn more about how to use Bitwarden.</value> <value>সাহায্য পেতে, খবর পেতে, আমাদের ইমেল করতে এবং/অথবা বিটওয়ার্ডেন কীভাবে ব্যবহার করবেন সে সম্পর্কে আরও জানতে আমাদের ওয়েবসাইট দেখুন।</value>
</data> </data>
<data name="Website" xml:space="preserve"> <data name="Website" xml:space="preserve">
<value>ওয়েবসাইট</value> <value>ওয়েবসাইট</value>
@@ -413,7 +413,7 @@
<value>আপনার নতুন অ্যাকাউন্ট তৈরি করা হয়েছে! আপনি এখন লগইন করতে পারবেন।</value> <value>আপনার নতুন অ্যাকাউন্ট তৈরি করা হয়েছে! আপনি এখন লগইন করতে পারবেন।</value>
</data> </data>
<data name="AddAnItem" xml:space="preserve"> <data name="AddAnItem" xml:space="preserve">
<value>Add an Item</value> <value>একটি আইটেম যোগ করুন</value>
</data> </data>
<data name="AppExtension" xml:space="preserve"> <data name="AppExtension" xml:space="preserve">
<value>App extension</value> <value>App extension</value>
@@ -440,13 +440,13 @@
<value>Bitwarden Auto-fill Service</value> <value>Bitwarden Auto-fill Service</value>
</data> </data>
<data name="BitwardenAutofillAccessibilityServiceDescription" xml:space="preserve"> <data name="BitwardenAutofillAccessibilityServiceDescription" xml:space="preserve">
<value>Use the Bitwarden accessibility service to auto-fill your logins.</value> <value>আপনার লগইনগুলি স্বয়ংক্রিয়ভাবে পূরণ করতে বিটওয়ার্ডেন অ্যাক্সেসিবিলিটি পরিষেবা ব্যবহার করুন৷</value>
</data> </data>
<data name="ChangeEmail" xml:space="preserve"> <data name="ChangeEmail" xml:space="preserve">
<value>ইমেইল পরিবর্তন করুন</value> <value>ইমেইল পরিবর্তন করুন</value>
</data> </data>
<data name="ChangeEmailConfirmation" xml:space="preserve"> <data name="ChangeEmailConfirmation" xml:space="preserve">
<value>You can change your email address on the bitwarden.com web vault. Do you want to visit the website now?</value> <value>আপনি bitwarden.com ওয়েব ভল্টে আপনার ইমেল ঠিকানা পরিবর্তন করতে পারেন। আপনি কি এখনই ওয়েবসাইট ভিজিট করতে চান?</value>
</data> </data>
<data name="ChangeMasterPassword" xml:space="preserve"> <data name="ChangeMasterPassword" xml:space="preserve">
<value>প্রধান পাসওয়ার্ড পরিবর্তন</value> <value>প্রধান পাসওয়ার্ড পরিবর্তন</value>
@@ -511,7 +511,7 @@
<value>প্রিয়</value> <value>প্রিয়</value>
</data> </data>
<data name="Fingerprint" xml:space="preserve"> <data name="Fingerprint" xml:space="preserve">
<value>Fingerprint</value> <value>আঙুলের ছাপ</value>
</data> </data>
<data name="GeneratePassword" xml:space="preserve"> <data name="GeneratePassword" xml:space="preserve">
<value>পাসওয়ার্ড তৈরি করুন</value> <value>পাসওয়ার্ড তৈরি করুন</value>
@@ -584,7 +584,7 @@
<value>যদি আপনি আপনার পাসওয়ার্ড ভুলে যান তাহলে একটি প্রধান পাসওয়ার্ডের ইঙ্গিতটি আপনাকে মনে করাতে সাহায্য করতে পারে।</value> <value>যদি আপনি আপনার পাসওয়ার্ড ভুলে যান তাহলে একটি প্রধান পাসওয়ার্ডের ইঙ্গিতটি আপনাকে মনে করাতে সাহায্য করতে পারে।</value>
</data> </data>
<data name="MasterPasswordLengthValMessageX" xml:space="preserve"> <data name="MasterPasswordLengthValMessageX" xml:space="preserve">
<value>Master password must be at least {0} characters long.</value> <value>মাস্টার পাসওয়ার্ড কমপক্ষে {0} অক্ষরের হতে হবে।</value>
</data> </data>
<data name="MinNumbers" xml:space="preserve"> <data name="MinNumbers" xml:space="preserve">
<value>সর্বনিম্ন সংখ্যা</value> <value>সর্বনিম্ন সংখ্যা</value>
@@ -595,7 +595,7 @@
<comment>Minimum special characters for password generator settings</comment> <comment>Minimum special characters for password generator settings</comment>
</data> </data>
<data name="MoreSettings" xml:space="preserve"> <data name="MoreSettings" xml:space="preserve">
<value>More settings</value> <value>আরও সেটিংস</value>
</data> </data>
<data name="MustLogInMainApp" xml:space="preserve"> <data name="MustLogInMainApp" xml:space="preserve">
<value>You must log into the main Bitwarden app before you can use the extension.</value> <value>You must log into the main Bitwarden app before you can use the extension.</value>
@@ -684,7 +684,7 @@
<value>Item saved</value> <value>Item saved</value>
</data> </data>
<data name="Submitting" xml:space="preserve"> <data name="Submitting" xml:space="preserve">
<value>Submitting...</value> <value>জমা দেওয়া হচ্ছে...</value>
<comment>Message shown when interacting with the server</comment> <comment>Message shown when interacting with the server</comment>
</data> </data>
<data name="Syncing" xml:space="preserve"> <data name="Syncing" xml:space="preserve">
@@ -717,7 +717,7 @@
<value>Unlock with PIN code</value> <value>Unlock with PIN code</value>
</data> </data>
<data name="Validating" xml:space="preserve"> <data name="Validating" xml:space="preserve">
<value>Validating</value> <value>কার্যকর করা হচ্ছে</value>
<comment>Message shown when interacting with the server</comment> <comment>Message shown when interacting with the server</comment>
</data> </data>
<data name="VerificationCode" xml:space="preserve"> <data name="VerificationCode" xml:space="preserve">
@@ -876,7 +876,7 @@
<value>Unable to download file.</value> <value>Unable to download file.</value>
</data> </data>
<data name="UnableToOpenFile" xml:space="preserve"> <data name="UnableToOpenFile" xml:space="preserve">
<value>Your device cannot open this type of file.</value> <value>আপনার ডিভাইস এই ধরনের ফাইল খুলতে পারে না।</value>
</data> </data>
<data name="Downloading" xml:space="preserve"> <data name="Downloading" xml:space="preserve">
<value>ডাউনলোড হচ্ছে...</value> <value>ডাউনলোড হচ্ছে...</value>
@@ -2496,8 +2496,8 @@ Do you want to switch to this account?</value>
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Get master password hint</value> <value>Get master password hint</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>Logging in as {0}</value> <value>Logging in as {0} on {1}</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>Not you?</value> <value>Not you?</value>
@@ -2610,10 +2610,37 @@ Do you want to switch to this account?</value>
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>There are no items that match the search</value> <value>There are no items that match the search</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>US</value>
</data>
<data name="EU" xml:space="preserve">
<value>EU</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>Self-hosted</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>Data region</value>
</data>
<data name="Region" xml:space="preserve">
<value>Region</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</value> <value>Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>Current master password</value> <value>Current master password</value>
</data> </data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Master password re-prompt help</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Unlocking may fail due to insufficient memory. Decrease your KDF memory settings to resolve.</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Invalid API key</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Invalid API token</value>
</data>
</root> </root>

View File

@@ -2494,8 +2494,8 @@ Skeniranje će biti izvršeno automatski.</value>
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Dobijte podsjetnik glavne lozinke</value> <value>Dobijte podsjetnik glavne lozinke</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>Prijava kao {0}</value> <value>Logging in as {0} on {1}</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>Nisi ti?</value> <value>Nisi ti?</value>
@@ -2608,10 +2608,37 @@ Skeniranje će biti izvršeno automatski.</value>
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>There are no items that match the search</value> <value>There are no items that match the search</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>US</value>
</data>
<data name="EU" xml:space="preserve">
<value>EU</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>Self-hosted</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>Data region</value>
</data>
<data name="Region" xml:space="preserve">
<value>Region</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</value> <value>Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>Current master password</value> <value>Current master password</value>
</data> </data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Master password re-prompt help</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Unlocking may fail due to insufficient memory. Decrease your KDF memory settings to resolve.</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Invalid API key</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Invalid API token</value>
</data>
</root> </root>

View File

@@ -2495,8 +2495,8 @@ Voleu canviar a aquest compte?</value>
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Obteniu la pista de la contrasenya mestra</value> <value>Obteniu la pista de la contrasenya mestra</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>Connectat com {0}</value> <value>Inici de sessió com a {0} a {1}</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>No sou vosaltres?</value> <value>No sou vosaltres?</value>
@@ -2609,10 +2609,37 @@ Voleu canviar a aquest compte?</value>
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>No hi ha elements que coincidisquen amb la cerca</value> <value>No hi ha elements que coincidisquen amb la cerca</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>EUA</value>
</data>
<data name="EU" xml:space="preserve">
<value>UE</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>Autoallotjat</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>Regió de dades</value>
</data>
<data name="Region" xml:space="preserve">
<value>Regió</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>La vostra contrasenya mestra no compleix una o més de les polítiques de l'organització. Per accedir a la caixa forta, heu d'actualitzar-la ara. Si continueu, es tancarà la sessió actual i us demanarà que torneu a iniciar-la. Les sessions en altres dispositius poden continuar romanent actives fins a una hora.</value> <value>La vostra contrasenya mestra no compleix una o més de les polítiques de l'organització. Per accedir a la caixa forta, heu d'actualitzar-la ara. Si continueu, es tancarà la sessió actual i us demanarà que torneu a iniciar-la. Les sessions en altres dispositius poden continuar romanent actives fins a una hora.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>Contrasenya mestra actual</value> <value>Contrasenya mestra actual</value>
</data> </data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Ajuda per tornar a demanar la contrasenya mestra</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>El desbloqueig pot fallar a causa de memòria insuficient. Disminueix la configuració de memòria KDF per resoldre-ho</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Clau API no vàlida</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Token API no vàlid</value>
</data>
</root> </root>

View File

@@ -2494,8 +2494,8 @@ Chcete se přepnout na tento účet?</value>
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Získat nápovědu pro hlavní heslo</value> <value>Získat nápovědu pro hlavní heslo</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>Přihlášování jako {0}</value> <value>Přihlašování jako {0} na {1}</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>Nejste to Vy?</value> <value>Nejste to Vy?</value>
@@ -2608,10 +2608,37 @@ Chcete se přepnout na tento účet?</value>
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>Neexistují žádné položky, které by odpovídaly hledání</value> <value>Neexistují žádné položky, které by odpovídaly hledání</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>US</value>
</data>
<data name="EU" xml:space="preserve">
<value>EU</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>Vlastní hosting</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>Datový region</value>
</data>
<data name="Region" xml:space="preserve">
<value>Region</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>Vaše hlavní heslo nesplňuje jednu nebo více zásad Vaší organizace. Pro přístup k trezoru musíte nyní aktualizovat své hlavní heslo. Pokračování Vás odhlásí z Vaší aktuální relace a bude nutné se přihlásit. Aktivní relace na jiných zařízeních mohou zůstat aktivní až po dobu jedné hodiny.</value> <value>Vaše hlavní heslo nesplňuje jednu nebo více zásad Vaší organizace. Pro přístup k trezoru musíte nyní aktualizovat své hlavní heslo. Pokračování Vás odhlásí z Vaší aktuální relace a bude nutné se přihlásit. Aktivní relace na jiných zařízeních mohou zůstat aktivní až po dobu jedné hodiny.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>Aktuální hlavní heslo</value> <value>Aktuální hlavní heslo</value>
</data> </data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Nápověda pro znovuzeptání se na hlavní heslo</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Odemknutí může selhat z důvodu nedostatku paměti. Pro vyřešení snižte nastavení paměti KDF.</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Neplatný klíč API</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Neplatný token API</value>
</data>
</root> </root>

View File

@@ -2496,8 +2496,8 @@ Do you want to switch to this account?</value>
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Get master password hint</value> <value>Get master password hint</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>Logging in as {0}</value> <value>Logging in as {0} on {1}</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>Not you?</value> <value>Not you?</value>
@@ -2610,10 +2610,37 @@ Do you want to switch to this account?</value>
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>There are no items that match the search</value> <value>There are no items that match the search</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>US</value>
</data>
<data name="EU" xml:space="preserve">
<value>EU</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>Self-hosted</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>Data region</value>
</data>
<data name="Region" xml:space="preserve">
<value>Region</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</value> <value>Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>Current master password</value> <value>Current master password</value>
</data> </data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Master password re-prompt help</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Unlocking may fail due to insufficient memory. Decrease your KDF memory settings to resolve.</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Invalid API key</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Invalid API token</value>
</data>
</root> </root>

View File

@@ -2495,8 +2495,8 @@ Vil du skifte til denne konto?</value>
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Få hovedadgangskodetip</value> <value>Få hovedadgangskodetip</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>Logger ind som {0}</value> <value>Logger ind som {0} på {1}</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>Ikke dig?</value> <value>Ikke dig?</value>
@@ -2609,10 +2609,37 @@ Vil du skifte til denne konto?</value>
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>Ingen emner matcher søgningen</value> <value>Ingen emner matcher søgningen</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>USA</value>
</data>
<data name="EU" xml:space="preserve">
<value>EU</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>Selv-hostet</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>Dataregion</value>
</data>
<data name="Region" xml:space="preserve">
<value>Region</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>Din hovedadgangskode overholder ikke én eller flere organisationspolitikker. For at få adgang til boksen skal hovedadgangskode opdateres nu. Fortsættes, logges du ud af den nuværende session og vil skulle logger ind igen. Aktive sessioner på andre enheder kan forblive aktive i op til én time.</value> <value>Din hovedadgangskode overholder ikke én eller flere organisationspolitikker. For at få adgang til boksen skal hovedadgangskode opdateres nu. Fortsættes, logges du ud af den nuværende session og vil skulle logger ind igen. Aktive sessioner på andre enheder kan forblive aktive i op til én time.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>Aktuel hovedadgangskode</value> <value>Aktuel hovedadgangskode</value>
</data> </data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Hjælp til genanmodning om hovedadgangskode</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Oplåsning kan fejle grundet utilstrækkelig hukommelse. Reducér KDF-hukommelsesindstillinger for at afhjælpe</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Ugyldig API-nøgle</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Ugyldigt API-token</value>
</data>
</root> </root>

View File

@@ -2494,8 +2494,8 @@ Möchtest du zu diesem Konto wechseln?</value>
<data name="GetMasterPasswordwordHint" xml:space="preserve"> <data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Hinweis zum Master-Passwort erhalten</value> <value>Hinweis zum Master-Passwort erhalten</value>
</data> </data>
<data name="LoggingInAsX" xml:space="preserve"> <data name="LoggingInAsXOnY" xml:space="preserve">
<value>Anmelden als {0}</value> <value>Anmelden als {0} auf {1}</value>
</data> </data>
<data name="NotYou" xml:space="preserve"> <data name="NotYou" xml:space="preserve">
<value>Nicht du?</value> <value>Nicht du?</value>
@@ -2608,10 +2608,37 @@ Möchtest du zu diesem Konto wechseln?</value>
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve"> <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
<value>Es gibt keine Einträge, die mit der Suche übereinstimmen</value> <value>Es gibt keine Einträge, die mit der Suche übereinstimmen</value>
</data> </data>
<data name="US" xml:space="preserve">
<value>US</value>
</data>
<data name="EU" xml:space="preserve">
<value>EU</value>
</data>
<data name="SelfHosted" xml:space="preserve">
<value>Selbst gehostet</value>
</data>
<data name="DataRegion" xml:space="preserve">
<value>Datenregion</value>
</data>
<data name="Region" xml:space="preserve">
<value>Region</value>
</data>
<data name="UpdateWeakMasterPasswordWarning" xml:space="preserve"> <data name="UpdateWeakMasterPasswordWarning" xml:space="preserve">
<value>Dein Master-Passwort entspricht nicht einer oder mehreren Richtlinien deiner Organisation. Um auf den Tresor zugreifen zu können, musst du dein Master-Passwort jetzt aktualisieren. Wenn du fortfährst, wirst du von deiner aktuellen Sitzung abgemeldet und musst dich erneut anmelden. Aktive Sitzungen auf anderen Geräten können noch bis zu einer Stunde lang aktiv bleiben.</value> <value>Dein Master-Passwort entspricht nicht einer oder mehreren Richtlinien deiner Organisation. Um auf den Tresor zugreifen zu können, musst du dein Master-Passwort jetzt aktualisieren. Wenn du fortfährst, wirst du von deiner aktuellen Sitzung abgemeldet und musst dich erneut anmelden. Aktive Sitzungen auf anderen Geräten können noch bis zu einer Stunde lang aktiv bleiben.</value>
</data> </data>
<data name="CurrentMasterPassword" xml:space="preserve"> <data name="CurrentMasterPassword" xml:space="preserve">
<value>Aktuelles Master-Passwort</value> <value>Aktuelles Master-Passwort</value>
</data> </data>
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
<value>Hilfe zum erneuten Abfragen des Master-Passworts</value>
</data>
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
<value>Das Entsperren kann aufgrund von unzureichendem Arbeitsspeicher fehlschlagen. Verringere deine KDF-Speichereinstellungen, um das Problem zu beheben.</value>
</data>
<data name="InvalidAPIKey" xml:space="preserve">
<value>Ungültiger API-Schlüssel</value>
</data>
<data name="InvalidAPIToken" xml:space="preserve">
<value>Ungültiger API-Token</value>
</data>
</root> </root>

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