1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-05 23:53:33 +00:00

Compare commits

...

79 Commits

Author SHA1 Message Date
github-actions[bot]
52db1a9f6e Bumped version to 2023.10.0 (#2847)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
(cherry picked from commit 02b0265767)
2023-10-31 11:07:39 -04:00
github-actions[bot]
4dda7a6634 Autosync the updated translations (#2836)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-10-23 09:51:53 +00:00
github-actions[bot]
a1808f64b3 Autosync the updated translations (#2833)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-10-20 08:53:18 +00:00
Federico Maccaroni
142c3145f0 PM-4404 Added CreationDate to Fido2Credential objects and updated the UI bindings accordingly (#2832) 2023-10-19 17:46:26 -03:00
Federico Maccaroni
72de17bd1d PM-4314 Removed move passkey to organization duplicate check (#2828) 2023-10-17 12:36:54 -03:00
André Bispo
ed3467515e [PM-3531] Add missing automation ids. (#2814) 2023-10-14 00:39:57 +01:00
Jake Fink
21fc56457d fix isLocked logic and add comments (#2802) 2023-10-13 13:41:52 -04:00
ifernandezdiaz
bc2eb212a6 Adding missing ids (#2823)
* Adding missing ids

* Fixing repeated IDs
2023-10-13 12:12:47 -03:00
André Bispo
a1912526c2 [PM-3532] Code clean up. DeviceType delete. (#2762) 2023-10-13 14:51:19 +01:00
github-actions[bot]
9d0209751c Autosync the updated translations (#2822)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-10-13 08:19:14 +00:00
Federico Maccaroni
f2936c95fa [PM-4054] Rename Fido2Key to Fido2Credential (#2821)
* PM-4054 Renamed Fido2Key to Fido2Credential on the entire codebase

* PM-4054 Renamed file Fido2KeyApi to Fido2CredentialApi
2023-10-12 16:51:19 -03:00
mpbw2
bb2f1f0f5f [PM-3741] [PM-3750] Improvements to local storage handling (#2795)
* [PM-3741] [PM-3750] Improvements to local storage handling

* Update src/Android/MainActivity.cs

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2023-10-12 08:58:11 -04:00
github-actions[bot]
5a0c2115a1 Bumped version to 2023.9.3 (#2820)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-10-11 15:42:03 -07:00
aj-rosado
a67f50b145 Added CultureInfo in char.ToLower at GetServiceRegistrationName method (#2810) 2023-10-10 16:22:39 +01:00
github-actions[bot]
757e5ea647 Autosync the updated translations (#2805)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-10-06 07:46:57 +00:00
André Bispo
b23f29511c [PM-868] Re-open app to item could crash the app (#2813)
* [PM-868] Check for previous page before loading vault. Remove exception throw.

* [PM-868] Continue to throw exceptions
2023-10-04 15:48:04 +01:00
Federico Maccaroni
71731bb9b7 PM-2658 Removed deprecated strings resources. (#2812) 2023-10-03 15:40:55 -03:00
aj-rosado
f2be840a7d Added GetOrDeriveMasterKey to UserVerificationService (#2808) 2023-10-03 12:54:22 +01:00
André Bispo
685e0f407a [PM-2915] Fix config service null exception error bug (#2778) 2023-10-02 20:43:42 +01:00
Federico Maccaroni
bbef0f8c93 PM-4138 Remove hyphen from clip-board => clipboard on resources. (#2804) 2023-09-28 12:55:18 -04:00
Federico Maccaroni
3cdf5ccd3b [PM-115] Cipher key encryption update (#2421)
* PM-115 Added new cipher key and encryption/decryption mechanisms on cipher

* PM-115 fix format

* PM-115 removed ForceKeyRotation from new cipher encryption model given that another approach will be taken

* [PM-1690] Added minimum server version restriction to cipher key encryption (#2463)

* PM-1690 added minimum server version restriction to cipher key encryption and also change the force key rotation flag

* PM-1690 Updated min server version for new cipher encryption key and fixed configService registration

* PM-1690 removed forcekeyrotation

* PM-115 Temporarily Changed cipher key new encryption config to help testing (this change should be reseted eventually)

* PM-2456 Fix attachment encryption on new cipher item encryption model (#2556)

* PM-2531 Fix new cipher encryption on adding attachments on ciphers with no item level key (#2559)

* PM-115 Changed temporarily cipher key encryption min server version to 2023.6.0 to test

* PM-115 Reseted cipher key encryption minimum server version to 2023.5.0 and disable new cipher key on local cipher creation

* Added Key value to the cipher export model (#2628)

* Update Constants.cs

Updated minimum encryption server version to 2023.9.0 so QA can test its behavior

* PM-115 Fix file format

* PM-115 Changed new encryption off and minimum new encryption server version to 2023.8.0 for testing purposes

* PM-115 Changed CIpher key encryption minimum server version to 2023.9.0

* PM-3737 Remove suffix on client version sent to server (#2779)

* PM-115 QA testing server min version and enable new cipher key encryption

* PM-115 Disable new cipher encryption creation and change minimum server encryption version to 2023.9.1

---------

Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com>
2023-09-28 10:00:20 -03:00
Federico Maccaroni
e97a37222a [PM-2658] Settings Reorganization feature (#2702)
* [PM-2658] Settings Reorganization Init (#2697)

* PM-2658 Started settings reorganization (settings main + vault + about)

* PM-2658 Added settings controls based on templates and implemented OtherSettingsPage

* PM-2658 Fix format

* [PM-3512] Settings Appearance (#2703)

* PM-3512 Implemented new Appearance Settings

* PM-3512 Fix format

* [PM-3510] Implement Account Security Settings view (#2714)

* PM-3510 Implemented Security settings view

* PM-3510 Fix format

* PM-3510 Added empty placeholder to pending login requests and also improved a11y on security settings view.

* PM-3511 Implemented autofill settings view (#2735)

* [PM-3695] Add Connect to Watch to Other settings (#2736)

* PM-3511 Implemented autofill settings view

* PM-3695 Add Connect to watch setting to other settings view

* [PM-3693] Clear old Settings approach (#2737)

* PM-3511 Implemented autofill settings view

* PM-3693 Remove old Settings approach

* PM-3845 Fix default dark theme description verbiage (#2759)

* PM-3839 Fix allow screen capture and submit crash logs to init their state when the page appears (#2760)

* PM-3834 Fix dialogs strings on settings (#2758)

* [PM-3834] Fix import items link (#2782)

* PM-3834 Fix import items link

* PM-3834 Fix import items link, removed old link.

* [PM-4092] Fix vault timeout policies on new Settings (#2796)

* PM-4092 Fix vault timeout policy on settings for disabling controls and reset timeout when surpassing maximum

* PM-4092 Removed testing hardcoding of policy data
2023-09-27 16:26:12 -03:00
André Bispo
218a30b510 [PM-3446] User without MP, item with MP does not show on Android keyboard for autofill (#2764)
* [PM-3446] Check if user has mp and allow autofill to use items with mp re-prompt
2023-09-26 17:25:47 +01:00
github-actions[bot]
828043ec97 Bumped version to 2023.9.2 (#2797)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-09-26 11:05:07 -04:00
André Bispo
b25c8b0842 [PM-3893] Make PreLogin and Register endpoint use identity endpoints (#2772) 2023-09-25 16:28:58 +01:00
Federico Maccaroni
a4a0d31fc6 [PM-3811] Passkeys unification (#2774)
* PM-3811 Unified passkeys view and moved both inside Login as an array of FIdo2Key

* PM-3811 Passkeys unification => updated cipher details view an helpers

* PM-3811 Updated passkeys creation date time format
2023-09-22 14:55:35 +00:00
ifernandezdiaz
6ef6cf5d84 Adding missing IDs (#2786) 2023-09-22 11:24:30 -03:00
github-actions[bot]
597f629920 Autosync the updated translations (#2785)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-09-22 08:04:57 +00:00
github-actions[bot]
b8cef16711 Bumped version to 2023.9.1 (#2784)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-09-20 18:36:32 -04:00
Jake Fink
c4f6ae9077 [PM-3726] prevent legacy user login (#2769)
* [PM-3726] prevent legacy user login

* [PM-3726] prevent unlock or auto key migration if legacy user

* [PM-3726] add legacy checks to lock page and refactor

* [PM-3726] rethrow exception from pin

* formatting

* [PM-3726] add changes to LockViewController, consolidate logout calls

* formatting

* [PM-3726] pr feedback

* generate resx

* formatting
2023-09-20 15:56:51 -04:00
github-actions[bot]
8b9658d2c5 Bumped version to 2023.9.0 (#2783)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-09-20 12:34:30 -04:00
André Bispo
43bf0fbdb3 [PM-3086] Account switcher endpoint use domain string for Bitwarden production environments (#2773) 2023-09-19 10:35:37 +01:00
André Bispo
11922c6f49 [PM-3522] Keep variable value after logout. (#2761) 2023-09-19 10:33:01 +01:00
André Bispo
a6f05338c2 [PM-3393] Excessive Invalid Biometric unlock attempts should automatically log out TDE users (#2747)
* [PM-3393] Log user out on biometric exceed attempts

* [PM-3393] Move duplicated code to AppHelpers

* [PM-3393] Update copy on new pop up

* [PM-3393] Moved VaultTimeoutService to LazyResolve.

* [PM-3382] Change IVaultTimeoutService for messaging

* [PM-3393] Use default values.
2023-09-19 10:32:23 +01:00
Federico Maccaroni
b932824b5a Make dept-development-mobile default code owner (#2780) 2023-09-18 18:23:16 -03:00
github-actions[bot]
efd1671f48 Autosync the updated translations (#2771)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-09-14 14:14:00 +00:00
André Bispo
3e2005e5ed [PM-3606] TDE user with 2FA isn't able to autofill on iOS (#2723)
* [PM-3606] Fix 2FA for autofill

* [PM-3606] Fix autofill when user doesn't have a login method available.

* [PM-3606] PR fixes

* [PM-3606] Add logout logic to other extension projects

* [PM-3606] Move code to base class.

* Transform into property instead of field

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* Remove double ";"

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* [PM-3606] Fix iOS extension by changing base class of LockPasswordViewController

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2023-09-09 17:38:14 -04:00
Will Browning
382eee2ed3 [PM-3556] Change anonaddy to addy io (#2711)
* Update AppResources.af.resx

* Update AnonAddy references

* Reverted AnonAddy to AddyIo refactor, keeping text and url changes

---------

Co-authored-by: Andre Rosado <arosado@bitwarden.com>
2023-09-08 15:23:51 +01:00
github-actions[bot]
b0f1dd00ee Autosync the updated translations (#2751)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-09-08 09:33:37 +00:00
André Bispo
5961a001ab [PM-3551] Expired SSO token cached (#2718) 2023-09-08 07:48:37 +01:00
André Bispo
9026dd10e5 [PM-3593] Fix enable biometric on autofill when there is not MP (#2717) 2023-09-07 16:30:46 +01:00
ifernandezdiaz
355261679d Adding missing IDs for Set Password and Update Password pages (#2748) 2023-09-07 11:24:48 -03:00
ifernandezdiaz
7f14ec9b5d QA-508 - Build app for automation CI (#2705)
* Adding build steps for .app

* Uploading .app artifact

* Fixing ARCHIVE_PATH variable

* Fixing missing OutputPath

* Fixing Bitwarden .app file name

* Fixing wrong .app location

* Adding Fede's suggestion

* Update .github/workflows/build.yml

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>

---------

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
2023-09-06 14:25:59 -03:00
Opeyemi
0c72626916 UPDATE: all workflows (#2743) 2023-09-06 15:30:47 +01:00
André Bispo
f21fae7fea [PM-3382] User cannot select Email as a secondary 2FA option following SSO (#2719)
* [PM-3382] Update mobile client to receive and use SsoEmail2faSessionToken

* [PM-3382] Fix null 2fa email with local email on MP login.
2023-09-06 10:26:11 +01:00
github-actions[bot]
6d4792bc24 Autosync the updated translations (#2741)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-09-01 12:46:27 +00:00
mpbw2
dbadf8c56f [PM-3222] Migration of data from LiteDB to shared pref storage (#2724)
* Migration of data from LiteDB to shared pref storage

* tweaks
2023-08-30 10:55:20 -04:00
André Bispo
4d0f9d1c03 [PM-3543] [PM-3607] Fix password re-prompt when editing and on autofill. (#2713)
* [PM-3543] [PM-3507] Fix password re-prompt when editing and on autofill.
2023-08-30 09:38:46 +01:00
André Bispo
68759fc608 [PM-3547] Change logic to set user key for inactive account (#2715) 2023-08-29 10:28:51 +01:00
github-actions[bot]
47be3d6aef Bumped version to 2023.8.1 (#2730)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-08-28 16:34:38 -04:00
github-actions[bot]
7ec5c8ccfd Bumped version to 2023.8.0 (#2725)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-08-25 19:52:35 -04:00
Jake Fink
819aabb330 don't clear key needed for bio/auto migration in pin migration (#2721) 2023-08-25 09:47:37 -04:00
github-actions[bot]
9c7ff853d7 Autosync the updated translations (#2720)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-08-25 09:03:57 +00:00
mpbw2
e30f9903d1 fix for TDE pref naming collision (#2712)
* fix for TDE pref naming collision

* fix case
2023-08-22 15:51:11 -04:00
André Bispo
249406e3a8 [PM-3545] Fix biometric unlock for autofill (#2710)
* [PM-3545] Fix biometric unlock for autofill

* [PM-3545] Reuse existing method
2023-08-21 20:30:22 +01:00
github-actions[bot]
8cae840c68 Autosync the updated translations (#2704)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-08-19 14:00:16 +00:00
André Bispo
e274c04107 [PM-3513] Show error SSO policy (#2707)
* [PM-3513] Show API error when SSO policy is enforced.
2023-08-18 23:05:52 +01:00
Robyn MacCallum
7043be67dd Update CODEOWNERS (#2701)
* Update CODEOWNERS

* Update CODEOWNERS
2023-08-18 11:20:26 -04:00
Federico Maccaroni
afb8c515d6 [PM-3071] Remove share on save toggle on Send view (#2659)
* PM-3071 Removed share on save toggle on Send view and now it's done automatically, same for copy after saving from the Share extension

* PM-3071 Fix alignments on Share extension send view
2023-08-17 16:42:08 -03:00
Todd Martin
bfcfd367dd Trusted Device Encryption feature (#2656)
* [PM-1208] Add Device approval options screen. View model waiting for additional logic to be added.

* [PM-1208] Add device related api endpoint. Add AccoundDecryptOptions model and property to user Account.

* [PM-1208] Add continue button and not you option

* [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

* [PM-2583] Answer auth request with mp field as null if doesn't have it. (#2609)

* [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

* [PM-1201] Change timeout actions available based on hasMasterPassword (#2610)

* [PM-1201] Change timeout actions available based on hasMasterPassword

* [PM-2731] add user key and master key types

* [PM-2713] add new state for new keys and obsolete old ones
- UserKey
- MasterKey
- UserKeyMasterKey (enc UserKey from User Table)

* [PM-271] add UserKey and MasterKey support to crypto service

* [PM-2713] rename key hash to password hash & begin add methods to crypto service

* [PM-2713] continue organizing crypto service

* [PM-2713] more updates to crypto service

* [PM-2713] add new pin methods to state service

* [PM-2713] fix signature of GetUserKeyPin

* [PM-2713] add make user key method to crypto service

* [PM-2713] refresh pin key when setting user key

* [PM-2713] use new MakeMasterKey method

* [PM-2713] add toggle method to crypto service for keys

* [PM-2713] converting calls to new crypto service api

* [PM-2713] add migration for pin on lock screens

* [PM-2713] more conversions to new crypto service api

* [PM-2713] convert cipher service and others to crypto service api

* [PM-2713] More conversions to crypto api

* [PM-2713] use new crypto service api in auth service

* [PM-2713] remove unused cached values in crypto service

* [PM-2713] set decrypt and set user key in login helper

* fix bad merge

* Update crypto service api call to fix build

* [PM-1208] Fix app resource file

* [PM-1208] Fix merge

* [PM-1208] Fix merge

* [PM-2713] optimize async code in crypto service

* [PM-2713] rename password hash to master key hash

* [PM-2713] fix casting issues and pin

* [PM-2713] remove extra comment

* [PM-2713] remove broken casting

* [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

* [PM-2289] [PM-2293] TDE Login with device Admin Request (#2642)

* [PM-2713] deconstruct new key pair

* [PM-2713] rename PrivateKey methods to UserPrivateKey on crypto service

* [PM-2713] rename PinLockEnum to PinLockType

* [PM-2713] don't pass user key as param when encrypting

* [PM-2713] rename toggle method, don't reset enc user key

* [PM-2713] pr feedback

* [PM-2713] PR feedback

* [PM-2713] rename get pin lock type method

* [PM-2713] revert feedback for build

* [PM-2713] rename state methods

* [PM-2713] combine makeDataEncKey methods

* [PM-2713] consolidate attachment key creation
- also fix ios files missed during symbol rename

* [PM-2713] replace generic with inherited class

* rename account keys to be more descriptive

* [PM-2713] add auto unlock key to mobile

* [PM-1208] Add TDE flows for new users (#2655)

* [PM-1208] Create new user on SSO. Logout if not password is setup or has pending admin auth request.

* [PM-1208] Fix new user UserKey decryption.

* [PM-1208] Add new user continue to vault logic. Auto enrol user on continue.

* [PM-1208] Trust device only if needed

* [PM-1208] Add logic for New User SSO.

* [PM-1208] Add logic for New User SSO (missing file).

* [PM-2713] set user key on set password page

* [PM-2713] set enc user key during kc onboarding

* fix formatting

* [PM-2713] make method async again
- returning null from a task thats not async throws

* [PM-2713] clear service cache when adding new account

* Fix build after merge

* [PM-3313] Fix Android SSO Login (#2663)

* [PM-3313] Catch exception on AuthPendingRequest

* [PM-3313] Fix lock timeout action if user doesn't have a master password.

* code format

* [PM-3313] Null email in Approval Options screen (#2664)

* [PM-3313] Fix null email in approval options screen

* [PM-3320][PM-3321] Fix labels and UI tweaks (#2666)

* [PM-3320] Fix UI copy and remember me default ON.

* [PM-3321] Fix UI on Log in with device screen.

* [PM-3337] Fix admin request deny error (#2669)

* [PM-3342] Not you button logs user out. (#2672)

* [PM-3319] Check for admin request in Lock page (#2668)

* [PM-3319] Ignore admin auth request when choosing mp as decryption option.

* [PM-2289] Change header title based on auth request type (#2670)

* [PM-2289] Change header title based on auth request type

* [PM-3333] Check for purged admin auth requests (#2671)

* [PM-3333] Check for purged admin auth requests

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* [PM-3341] Vault Timeout Action not persisted correctly (#2673)

* [PM-3341] Fix timeout action change when navigating

* [PM-3357] Fix copy for Login Initiated (#2674)

* [PM-3362] Fix auth request approval (#2675)

* [PM-3362] Fix auth request approval

* [PM-3362] Add new exception type

* [PM-3102] Update Master password reprompt to be based on MP instead of Key Connector (#2653)

* PM-3102 Added check to see if a user has master password set replacing previous usage of key connector.

* PM-3102 Fix formatting

* [PM-2713] Final merge from Key Migration branch to TDE Feature branch (#2667)

* [PM-2713] add async to key connector service methods

* [PM-2713] rename ephemeral pin key

* add state for biometric key and accept UserKey instead of string for auto key

* Get UserKey from bio state on unlock

* PM-2713 Fix auto-migrating EncKeyEncrypted into MasterKey encrypted UserKey when requesting DecryptUserKeyWithMasterKeyAsync is called

* renaming bio key and fix build

* PM-3194 Fix biometrics button to be shown on upgrade when no UserKey is present yet

* revert removal of key connector service from auth service

* PM-2713 set user key when using KC

* clear enc user key after migration

* use is true for nullable bool

* PR feedback, refactor kc service

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* Fix app fresh install user login with master password. (#2676)

* [PM-3303] Fix biometric login after key migration (#2679)

* [PM-3303] Add condition to biometric unlock

* [PM-3381] Fix TDE login 2FA flow (#2678)

* [PM-3381] Check for vault lock on 2FA screen

* [PM-3381] Move logic to ViewModel

* [PM-3381] Fix null vm error

* [PM-3379] Fix key rotation on trusted device. (#2680)

* [PM-3381] Update login flows (#2683)

* [PM-3381] Update login flows

* [PM-3381] Remove _authingWithSso parameter

* PM-3385 Fix MP reprompt item level when no MP hash is stored like logging in with TDE. Also refactor code to be more maintainable (#2687)

* PM-3386 Fix MP reprompt / OTP decision to be also based on the master key hash. (#2688)

* PM-3450 Fix has master password with mp key hash check (#2689)

* [PM-3394] Fix login with device for passwordless approvals (#2686)

* set activeUserId to null when logging in a new account
- Also stop the user key from being set in inactive accounts

* get token for login with device if approving device doesn't have master key

* add comment

* simplify logic

* check for route instead of using isAuthenticated
- we don't clear the user id when logging in new account
- this means we can't trust the state service, so we have to base our logic off the route in login with device

* use authenticated auth request for tde login with device

* [PM-3394] Add authingWithSso parameter to LoginPasswordlessRequestPage.

* pr feedback

* [PM-3394] Refactor condition

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

---------

Co-authored-by: André Bispo <abispo@bitwarden.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* [PM-3462] Handle force password reset on mobile with TDE (#2694)

* [PM-3462] Handle force password reset on mobile with TDE

* [PM-3462] update references to refactored crypto method
- fix kc bug, we were sending private key instead of user key to server
- rename kc service method to be correct

* [PM-3462] Update TwoFactorPage login logic

* [PM-3462] Added pending admin request check to TwoFactorPage

* [PM-3462] Added new exception types for null keys

---------

Co-authored-by: André Bispo <abispo@bitwarden.com>

* [PM-1029] Fix Async suffix in ApiService. Add UserKeyNullExceptions.

* [PM 3513] Fix passwordless 2fa login with device on mobile (#2700)

* [PM-3513] Fix 2FA for normal login with device with users without mp

* move _userKey

---------

Co-authored-by: André Bispo <abispo@bitwarden.com>

* clear encrypted pin on logout (#2699)

---------

Co-authored-by: André Bispo <abispo@bitwarden.com>
Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2023-08-17 15:19:35 -04:00
Federico Maccaroni
a23454bc53 [PM-3508] Fix Release iPhoneSimulator configuration for iOS / Extensions (#2698)
* PM-3508 Fix Release iPhoneSimulator configuration for iOS / Extensions

* PM-3508 Fix --deep space on watch app references
2023-08-16 15:55:34 -03:00
mpbw2
6f7100ae4f lib updates (#2696) 2023-08-16 12:48:54 -04:00
ifernandezdiaz
01ac20e6e4 Adding missing AutomationIDs on LoginPasswordlessRequestListPage (#2693) 2023-08-16 11:09:07 -03:00
github-actions[bot]
8474f536ff Autosync the updated translations (#2677)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
2023-08-15 13:48:01 +00:00
Daniel James Smith
f426c0e370 Create section for crowdin sync (#2692) 2023-08-15 15:47:08 +02:00
Daniel James Smith
420dc09fd1 Update codeowners (#2691)
* Set team-leads-eng as owners for translations

This is needed to Crowdin sync PRs can be merged.

* Add team-tools as owner of the email-forwarders

* Fix unescaped whitespace

* Remove team-leads-eng from owning English resources
2023-08-15 15:32:57 +02:00
Federico Maccaroni
6d4793d592 [PM-1768] Set up CODEOWNERS file (#2464)
* PM-1768 Add CODEOWNERS file with some initial setup of folders that don't need to be moved and can have their owners assigned already.

* Update CODEOWNERS

Removed entire projects owners cause I didn't consider auth inside of the extensions.
2023-08-10 13:58:03 -03:00
Bernd Schoolmann
eea7c6b7d7 [PM-2901] Synchronize sends on send creation/update/deletion notification (#2606)
* Add sync on send create/update/delete notification

* Update send notifications to only sync sends

* Fix incorrect notification type in PushNotificationListenerService

Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com>

* Invert if to improve readability

* Simplify shouldUpdate logic in SyncUpsertSendAsync

* Further simplify SyncService code

* Fix if condition in SyncService

Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com>

* Fixed whitespace formatting

---------

Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com>
Co-authored-by: Andre Rosado <arosado@bitwarden.com>
2023-08-08 14:59:42 +01:00
Bernd Schoolmann
ec93a61275 [PM-3092] Clarify argon2 ios autofill warning (#2630)
* Clarify argon2 ios warning

* Update Argon2 insufficient memory warning message
2023-08-07 10:41:40 -04:00
Federico Maccaroni
c34d1da6e6 PM-3298 Updated region selector to be logging in on and also its options (#2657) 2023-08-04 11:44:39 -03:00
github-actions[bot]
c4e64e082b Autosync the updated translations (#2660)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-08-04 10:55:27 +00:00
Federico Maccaroni
5aaff1ea20 PM-3249 Removed back button from block autofill uris to be aligned to other views (#2654) 2023-08-02 10:54:01 -03:00
github-actions[bot]
0271a4db4c Autosync the updated translations (#2650)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-07-28 08:19:12 +00:00
Federico Maccaroni
375718f945 PM-3227 Avoid clone on discoverable passkeys. (#2648) 2023-07-27 18:35:09 -03:00
Federico Maccaroni
9eda015371 PM-3165 Finish task completion source when cancelling the dialog tapping on the background. (#2647) 2023-07-27 17:49:52 -03:00
Federico Maccaroni
ea81acb3bf [PM-1575] Display Passkeys (#2523)
* PM-1575 Added new models for Fido2Key

* PM-1575 Added discoverable passkeys and WIP non-discoverable ones

* PM-1575 Fix format

* PM-1575 Added non-discoverable passkeys to login UI

* PM-1575 Added copy application icon to Fido2Key UI

* PM-1575 Updated bwi font with the updated passkey icon

* PM-1575 For now just display Available for two-step login on non-discoverable passkey inside of a cipher login

* PM-1575 Fix non-discoverable passkey visibility

* PM-1575 remove Passkeys as a filter in the vault list

* PM-1575 Display error toast if there is a duplicate passkey when moving a cipher to an org

* Revert "PM-1575 Display error toast if there is a duplicate passkey when moving a cipher to an org"

This reverts commit 78e6353602.

* [PM-2378] Display error toast on duplicate Passkey when moving cipher to an organization (#2594)

* PM-2378 Display error toast if there is a duplicate passkey when moving a cipher to an org

* PM-3097 Fix issue when moving cipher with passkey to an org where the uniqueness should be taken into consideration on different passkeys types and also the Username (#2632)

* PM-3096 Fix non-discoverable passkey to be taken into account when encrypting a cipher which was causing the passkey to be removed when moving to an org (#2637)
2023-07-26 17:59:49 -03:00
René Wang
174549e5bc fix image alt text error (#2641) 2023-07-24 18:27:20 +00:00
Federico Maccaroni
87b1d18872 PM-2320 fix duplicated resource (#2638) 2023-07-21 15:58:28 -03:00
github-actions[bot]
ae9ba810ff Autosync the updated translations (#2634)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-07-21 09:30:20 +00:00
408 changed files with 27861 additions and 7724 deletions

31
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,31 @@
# Please sort lines alphabetically, this will ensure we don't accidentally add duplicates.
#
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# The following owners will be the default owners for everything in the repo.
# Unless a later match takes precedence
# @bitwarden/tech-leads
@bitwarden/dept-development-mobile
## Auth team files ##
## Platform team files ##
appIcons @bitwarden/team-platform-dev
build.cake @bitwarden/team-platform-dev
## Vault team files ##
src/watchOS @bitwarden/team-vault-dev
## Tools team files ##
src/Core/Services/EmailForwarders @bitwarden/team-tools-dev
## Crowdin Sync files ##
src/App/Resources @bitwarden/tech-leads
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization @bitwarden/tech-leads
## Locales ##
src/App/Resources/AppResources.Designer.cs
src/App/Resources/AppResources.resx
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization/en.lproj

View File

@@ -525,7 +525,7 @@ jobs:
submodules: 'true'
- name: Login to Azure - CI Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@@ -667,6 +667,22 @@ jobs:
$configuration = "AppStore";
$platform = "iPhone";
Write-Output "########################################"
Write-Output "##### Archive $configuration Configuration for $platform Platform"
Write-Output "########################################"
msbuild "$($env:GITHUB_WORKSPACE + "/src/iOS/iOS.csproj")" "/p:Platform=$platform" `
"/p:Configuration=$configuration" "/p:ArchiveOnBuild=true" "/t:`"Build`""
Write-Output "########################################"
Write-Output "##### Done"
Write-Output "########################################"
shell: pwsh
- name: Archive Build for Mobile Automation
run: |
$configuration = "Release";
$platform = "iPhoneSimulator";
Write-Output "########################################"
Write-Output "##### Archive $configuration Configuration for $platform Platform"
Write-Output "########################################"
@@ -689,6 +705,15 @@ jobs:
-exportOptionsPlist $EXPORT_OPTIONS_PATH
shell: bash
- name: Export .app for Automation CI
run: |
ARCHIVE_PATH="./src/iOS/bin/iPhoneSimulator/Release/BitwardeniOS.app"
EXPORT_PATH="./bitwarden-export"
zip -r -q BitwardeniOS.app.zip $ARCHIVE_PATH
mv BitwardeniOS.app.zip $EXPORT_PATH
shell: bash
- name: Copy all dSYMs files to upload
run: |
ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs"
@@ -711,6 +736,13 @@ jobs:
./bitwarden-export/dSYMs/*.*
if-no-files-found: error
- name: Upload .app file for Automation CI
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
with:
name: BitwardeniOS.app.zip
path: ./bitwarden-export/BitwardeniOS.app.zip
if-no-files-found: error
- name: Install AppCenter CLI
if: |
(github.ref == 'refs/heads/master'
@@ -779,7 +811,7 @@ jobs:
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: Login to Azure - CI Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@@ -845,7 +877,7 @@ jobs:
fi
- name: Login to Azure - CI Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
if: failure()
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

View File

@@ -18,13 +18,13 @@ jobs:
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: Login to Azure - CI Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/get-keyvault-secrets@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
with:
keyvault: "bitwarden-ci"
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"

View File

@@ -12,6 +12,6 @@ jobs:
pull-requests: write
runs-on: ubuntu-20.04
steps:
- uses: actions/labeler@ba790c862c380240c6d5e7427be5ace9a05c754b # v4.0.3
- uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0
with:
sync-labels: true

View File

@@ -38,11 +38,11 @@ jobs:
fi
- name: Checkout repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Check Release Version
id: version
uses: bitwarden/gh-actions/release-version-check@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/release-version-check@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
with:
release-type: ${{ github.event.inputs.release_type }}
project-type: xamarin
@@ -87,7 +87,7 @@ jobs:
- name: Create release
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0
with:
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
./com.x8bit.bitwarden.apk/com.x8bit.bitwarden.apk,
@@ -126,7 +126,7 @@ jobs:
if: inputs.fdroid_publish
steps:
- name: Checkout repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
@@ -147,9 +147,9 @@ jobs:
name: com.x8bit.bitwarden-fdroid.apk
- name: Set up Node
uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2.5.1
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
node-version: '10.x'
node-version: '16.x'
- name: Set up F-Droid server
run: |

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: 'Run stale action'
uses: actions/stale@3cc123766321e9f15a6676375c154ccffb12a358 # v5.0.0
uses: actions/stale@f7176fd3007623b69d27091f9b9d4ab7995f0a06 # v5.2.1
with:
stale-issue-label: 'needs-reply'
stale-pr-label: 'needs-changes'

View File

@@ -14,7 +14,7 @@ jobs:
version_number: ${{ steps.version.outputs.new-version }}
steps:
- name: Checkout Branch
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Calculate bumped version
id: version
@@ -35,7 +35,5 @@ jobs:
name: Bump version to ${{ needs.setup.outputs.version_number }}
needs: setup
uses: ./.github/workflows/version-bump.yml
secrets:
AZURE_PROD_KV_CREDENTIALS: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
with:
version_number: ${{ needs.setup.outputs.version_number }}

View File

@@ -12,9 +12,6 @@ on:
version_number:
required: true
type: string
secrets:
AZURE_PROD_KV_CREDENTIALS:
required: true
jobs:
bump_version:
@@ -22,22 +19,22 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout Branch
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Login to Azure - CI Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/get-keyvault-secrets@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@111c56156bcc6918c056dbef52164cfa583dc549 # v5.2.0
uses: crazy-max/ghaction-import-gpg@d6f3f49f3345e29369fe57596a3ca8f94c4d2ca7 # v5.4.0
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
@@ -48,31 +45,31 @@ jobs:
run: git switch -c version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - Android XML
uses: bitwarden/gh-actions/version-bump@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/version-bump@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/Android/Properties/AndroidManifest.xml"
- name: Bump Version - iOS.Autofill
uses: bitwarden/gh-actions/version-bump@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/version-bump@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/iOS.Autofill/Info.plist"
- name: Bump Version - iOS.Extension
uses: bitwarden/gh-actions/version-bump@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/version-bump@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/iOS.Extension/Info.plist"
- name: Bump Version - iOS.ShareExtension
uses: bitwarden/gh-actions/version-bump@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/version-bump@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/iOS.ShareExtension/Info.plist"
- name: Bump Version - iOS
uses: bitwarden/gh-actions/version-bump@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/version-bump@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/iOS/Info.plist"

View File

@@ -8,4 +8,4 @@ on:
jobs:
call-workflow:
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67

View File

@@ -4,7 +4,7 @@
# Bitwarden Mobile Application
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden" target="_blank"><img alt="Get it on Google Play" src="https://imgur.com/YQzmZi9.png" width="153" height="46"></a> <a href="https://mobileapp.bitwarden.com/fdroid/" target="_blank"><img alt="Get it on Google Play" src="https://i.imgur.com/HDicnzz.png" width="154" height="46"></a> <a href="https://itunes.apple.com/us/app/bitwarden-free-password-manager/id1137397744?mt=8" target="_blank"><img src="https://imgur.com/GdGqPMY.png" width="135" height="40"></a>
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden" target="_blank"><img alt="Get it on Google Play" src="https://imgur.com/YQzmZi9.png" width="153" height="46"></a> <a href="https://mobileapp.bitwarden.com/fdroid/" target="_blank"><img alt="Get it on F-Droid" src="https://i.imgur.com/HDicnzz.png" width="154" height="46"></a> <a href="https://itunes.apple.com/us/app/bitwarden-free-password-manager/id1137397744?mt=8" target="_blank"><img src="https://imgur.com/GdGqPMY.png" width="135" height="40"></a>
The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin iOS, and Xamarin Forms.

View File

@@ -77,21 +77,21 @@
<PackageReference Include="Portable.BouncyCastle">
<Version>1.9.0</Version>
</PackageReference>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1.1" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.16" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.19" />
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.10.0" />
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1.1" />
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.6.1.3" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.21" />
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.10.1.2" />
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.4.0.2" />
<PackageReference Include="Xamarin.Essentials">
<Version>1.7.5</Version>
<Version>1.8.0</Version>
</PackageReference>
<PackageReference Include="Xamarin.Firebase.Messaging">
<Version>123.1.1.1</Version>
<Version>123.1.2.2</Version>
</PackageReference>
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.8.0" />
<PackageReference Include="Xamarin.Google.Dagger" Version="2.44.2.1" />
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.9.0.2" />
<PackageReference Include="Xamarin.Google.Dagger" Version="2.46.1.2" />
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
<Version>118.0.1.3</Version>
<Version>118.0.1.5</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
@@ -245,6 +245,14 @@
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\empty_login_requests.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\empty_login_requests_dark.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\splash_screen.xml" />

Binary file not shown.

View File

@@ -20,6 +20,7 @@ using AndroidX.AutoFill.Inline.V1;
using Bit.Core.Abstractions;
using SaveFlags = Android.Service.Autofill.SaveFlags;
using Bit.Droid.Utilities;
using Bit.Core.Services;
namespace Bit.Droid.Autofill
{
@@ -152,8 +153,9 @@ namespace Bit.Droid.Autofill
"androidapp://com.oneplus.applocker",
};
public static async Task<List<FilledItem>> GetFillItemsAsync(Parser parser, ICipherService cipherService)
public static async Task<List<FilledItem>> GetFillItemsAsync(Parser parser, ICipherService cipherService, IUserVerificationService userVerificationService)
{
var userHasMasterPassword = await userVerificationService.HasMasterPasswordAsync();
if (parser.FieldCollection.FillableForLogin)
{
var ciphers = await cipherService.GetAllDecryptedByUrlAsync(parser.Uri);
@@ -161,14 +163,14 @@ namespace Bit.Droid.Autofill
{
var allCiphers = ciphers.Item1.ToList();
allCiphers.AddRange(ciphers.Item2.ToList());
var nonPromptCiphers = allCiphers.Where(cipher => cipher.Reprompt == CipherRepromptType.None);
var nonPromptCiphers = allCiphers.Where(cipher => !userHasMasterPassword || cipher.Reprompt == CipherRepromptType.None);
return nonPromptCiphers.Select(c => new FilledItem(c)).ToList();
}
}
else if (parser.FieldCollection.FillableForCard)
{
var ciphers = await cipherService.GetAllDecryptedAsync();
return ciphers.Where(c => c.Type == CipherType.Card && c.Reprompt == CipherRepromptType.None).Select(c => new FilledItem(c)).ToList();
return ciphers.Where(c => c.Type == CipherType.Card && (!userHasMasterPassword || c.Reprompt == CipherRepromptType.None)).Select(c => new FilledItem(c)).ToList();
}
return new List<FilledItem>();
}

View File

@@ -11,6 +11,7 @@ using Android.Widget;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Utilities;
namespace Bit.Droid.Autofill
@@ -26,6 +27,7 @@ namespace Bit.Droid.Autofill
private IPolicyService _policyService;
private IStateService _stateService;
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private IUserVerificationService _userVerificationService;
public async override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal,
FillCallback callback)
@@ -64,11 +66,9 @@ namespace Bit.Droid.Autofill
var locked = await _vaultTimeoutService.IsLockedAsync();
if (!locked)
{
if (_cipherService == null)
{
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
}
items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService);
_cipherService ??= ServiceContainer.Resolve<ICipherService>();
_userVerificationService ??= ServiceContainer.Resolve<IUserVerificationService>();
items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService, _userVerificationService);
}
// build response

View File

@@ -3,5 +3,11 @@
public static class Constants
{
public const string PACKAGE_NAME = "com.x8bit.bitwarden";
public const string TEMP_CAMERA_IMAGE_NAME = "temp_camera_image.jpg";
/// <summary>
/// This directory must also be declared in filepaths.xml
/// </summary>
public const string TEMP_CAMERA_IMAGE_DIR = "camera_temp";
}
}

View File

@@ -116,7 +116,7 @@ namespace Bit.Droid
{
ListenYubiKey((bool)message.Data);
}
else if (message.Command == "updatedTheme")
else if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => AppearanceAdjustments());
}
@@ -238,19 +238,23 @@ namespace Bit.Droid
Android.Net.Uri uri = null;
string fileName = null;
if (data != null && data.Data != null)
{
if (data.Data.ToString()?.Contains(Constants.PACKAGE_NAME) != true)
{
uri = data.Data;
fileName = AndroidHelpers.GetFileName(ApplicationContext, uri);
}
}
else
{
// camera
var file = new Java.IO.File(FilesDir, "temp_camera_photo.jpg");
var tmpDir = new Java.IO.File(FilesDir, Constants.TEMP_CAMERA_IMAGE_DIR);
var file = new Java.IO.File(tmpDir, Constants.TEMP_CAMERA_IMAGE_NAME);
uri = FileProvider.GetUriForFile(this, "com.x8bit.bitwarden.fileprovider", file);
fileName = $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg";
}
if (uri == null)
if (uri == null || fileName == null)
{
return;
}

View File

@@ -68,9 +68,9 @@ namespace Bit.Droid
ServiceContainer.Register<IDeleteAccountActionFlowExecutioner>("deleteAccountActionFlowExecutioner", deleteAccountActionFlowExecutioner);
var verificationActionsFlowHelper = new VerificationActionsFlowHelper(
ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService"),
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
ServiceContainer.Resolve<ICryptoService>("cryptoService"));
ServiceContainer.Resolve<ICryptoService>("cryptoService"),
ServiceContainer.Resolve<IUserVerificationService>());
ServiceContainer.Register<IVerificationActionsFlowHelper>("verificationActionsFlowHelper", verificationActionsFlowHelper);
var accountsManager = new AccountsManager(
@@ -156,10 +156,11 @@ namespace Bit.Droid
messagingService, broadcasterService);
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService,
platformUtilsService, new LazyResolve<IEventService>());
var biometricService = new BiometricService(stateService);
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService);
var biometricService = new BiometricService(stateService, cryptoService);
var userPinService = new UserPinService(stateService, cryptoService);
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
@@ -182,6 +183,7 @@ namespace Bit.Droid
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
ServiceContainer.Register<IUserPinService>(userPinService);
// Push
#if FDROID

View File

@@ -1,5 +1,5 @@
<?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.7.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.10.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />

View File

@@ -0,0 +1,41 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="200"
android:viewportHeight="143"
android:width="200dp"
android:height="143dp">
<path
android:pathData="M34 43H10C6.68629 43 4 45.6863 4 49V109C4 112.314 6.68629 115 10 115H34C37.3137 115 40 112.314 40 109V49C40 45.6863 37.3137 43 34 43ZM10 39C4.47715 39 0 43.4772 0 49V109C0 114.523 4.47715 119 10 119H34C39.5228 119 44 114.523 44 109V49C44 43.4772 39.5228 39 34 39H10Z"
android:fillType="evenOdd"
android:fillColor="#89929F" />
<path
android:pathData="M20.3701 47.809C20.3701 47.2567 20.8178 46.809 21.3701 46.809H22.6122C23.1645 46.809 23.6122 47.2567 23.6122 47.809C23.6122 48.3612 23.1645 48.809 22.6122 48.809H21.3701C20.8178 48.809 20.3701 48.3612 20.3701 47.809Z"
android:fillType="evenOdd"
android:fillColor="#89929F" />
<path
android:pathData="M68 120C68 119.448 68.4477 119 69 119H127C127.552 119 128 119.448 128 120C128 120.552 127.552 121 127 121H69C68.4477 121 68 120.552 68 120Z"
android:fillType="evenOdd"
android:fillColor="#89929F" />
<path
android:pathData="M87.7402 120V102.236H89.7402V120H87.7402Z"
android:fillType="evenOdd"
android:fillColor="#89929F" />
<path
android:pathData="M107.71 120V102.236H109.71V120H107.71Z"
android:fillType="evenOdd"
android:fillColor="#89929F" />
<path
android:pathData="M27 25C27 17.268 33.268 11 41 11H157C164.732 11 171 17.268 171 25V31H167V25C167 19.4772 162.523 15 157 15H41C35.4772 15 31 19.4772 31 25V41H27V25ZM42 99H127V103H42V99Z"
android:fillType="evenOdd"
android:fillColor="#89929F" />
<path
android:pathData="M35 26C35 22.134 38.134 19 42 19H156C159.866 19 163 22.134 163 26V31H161V26C161 23.2386 158.761 21 156 21H42C39.2386 21 37 23.2386 37 26V41H35V26ZM42 93H127V95H42V93Z"
android:fillType="evenOdd"
android:fillColor="#89929F" />
<path
android:pathData="M125 39C125 33.4771 129.477 29 135 29H188C193.523 29 198 33.4772 198 39V119C198 124.523 193.523 129 188 129H135C129.477 129 125 124.523 125 119V39ZM135 33C131.686 33 129 35.6863 129 39V119C129 122.314 131.686 125 135 125H188C191.314 125 194 122.314 194 119V39C194 35.6863 191.314 33 188 33H135Z"
android:fillType="evenOdd"
android:fillColor="#89929F" />
<path
android:pathData="M164 120C164 121.105 163.105 122 162 122C160.895 122 160 121.105 160 120C160 118.895 160.895 118 162 118C163.105 118 164 118.895 164 120Z"
android:fillColor="#89929F" />
</vector>

View File

@@ -0,0 +1,41 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="200"
android:viewportHeight="143"
android:width="200dp"
android:height="143dp">
<path
android:pathData="M34 43H10C6.68629 43 4 45.6863 4 49V109C4 112.314 6.68629 115 10 115H34C37.3137 115 40 112.314 40 109V49C40 45.6863 37.3137 43 34 43ZM10 39C4.47715 39 0 43.4772 0 49V109C0 114.523 4.47715 119 10 119H34C39.5228 119 44 114.523 44 109V49C44 43.4772 39.5228 39 34 39H10Z"
android:fillType="evenOdd"
android:fillColor="#A3A3A3" />
<path
android:pathData="M20.3701 47.809C20.3701 47.2567 20.8178 46.809 21.3701 46.809H22.6122C23.1645 46.809 23.6122 47.2567 23.6122 47.809C23.6122 48.3612 23.1645 48.809 22.6122 48.809H21.3701C20.8178 48.809 20.3701 48.3612 20.3701 47.809Z"
android:fillType="evenOdd"
android:fillColor="#A3A3A3" />
<path
android:pathData="M68 120C68 119.448 68.4477 119 69 119H127C127.552 119 128 119.448 128 120C128 120.552 127.552 121 127 121H69C68.4477 121 68 120.552 68 120Z"
android:fillType="evenOdd"
android:fillColor="#A3A3A3" />
<path
android:pathData="M87.7402 120V102.236H89.7402V120H87.7402Z"
android:fillType="evenOdd"
android:fillColor="#A3A3A3" />
<path
android:pathData="M107.71 120V102.236H109.71V120H107.71Z"
android:fillType="evenOdd"
android:fillColor="#A3A3A3" />
<path
android:pathData="M27 25C27 17.268 33.268 11 41 11H157C164.732 11 171 17.268 171 25V31H167V25C167 19.4772 162.523 15 157 15H41C35.4772 15 31 19.4772 31 25V41H27V25ZM42 99H127V103H42V99Z"
android:fillType="evenOdd"
android:fillColor="#A3A3A3" />
<path
android:pathData="M35 26C35 22.134 38.134 19 42 19H156C159.866 19 163 22.134 163 26V31H161V26C161 23.2386 158.761 21 156 21H42C39.2386 21 37 23.2386 37 26V41H35V26ZM42 93H127V95H42V93Z"
android:fillType="evenOdd"
android:fillColor="#A3A3A3" />
<path
android:pathData="M125 39C125 33.4771 129.477 29 135 29H188C193.523 29 198 33.4772 198 39V119C198 124.523 193.523 129 188 129H135C129.477 129 125 124.523 125 119V39ZM135 33C131.686 33 129 35.6863 129 39V119C129 122.314 131.686 125 135 125H188C191.314 125 194 122.314 194 119V39C194 35.6863 191.314 33 188 33H135Z"
android:fillType="evenOdd"
android:fillColor="#A3A3A3" />
<path
android:pathData="M164 120C164 121.105 163.105 122 162 122C160.895 122 160 121.105 160 120C160 118.895 160.895 118 162 118C163.105 118 164 118.895 164 120Z"
android:fillColor="#A3A3A3" />
</vector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cache" path="." />
<files-path name="internal" path="." />
<files-path name="temp_camera_images" path="camera_temp/" />
</paths>

View File

@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Android.OS;
using Android.Security.Keystore;
using Bit.App.Services;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Java.Security;
@@ -9,10 +10,8 @@ using Javax.Crypto;
namespace Bit.Droid.Services
{
public class BiometricService : IBiometricService
public class BiometricService : BaseBiometricService
{
private readonly IStateService _stateService;
private const string KeyName = "com.8bit.bitwarden.biometric_integrity";
private const string KeyStoreName = "AndroidKeyStore";
@@ -24,14 +23,14 @@ namespace Bit.Droid.Services
private readonly KeyStore _keystore;
public BiometricService(IStateService stateService)
public BiometricService(IStateService stateService, ICryptoService cryptoService)
: base(stateService, cryptoService)
{
_stateService = stateService;
_keystore = KeyStore.GetInstance(KeyStoreName);
_keystore.Load(null);
}
public async Task<bool> SetupBiometricAsync(string bioIntegritySrcKey = null)
public override async Task<bool> SetupBiometricAsync(string bioIntegritySrcKey = null)
{
if (Build.VERSION.SdkInt >= BuildVersionCodes.M)
{
@@ -41,7 +40,7 @@ namespace Bit.Droid.Services
return true;
}
public async Task<bool> IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null)
public override async Task<bool> IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null)
{
if (Build.VERSION.SdkInt < BuildVersionCodes.M)
{

View File

@@ -3,7 +3,6 @@ using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Media;
using Android.Nfc;
using Android.OS;
using Android.Provider;
@@ -18,14 +17,9 @@ using Bit.App.Utilities;
using Bit.App.Utilities.Prompts;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Droid.Utilities;
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;
namespace Bit.Droid.Services
{
@@ -267,6 +261,7 @@ namespace Bit.Droid.Services
var result = new TaskCompletionSource<ValidatablePromptResponse?>();
alertBuilder.SetOnCancelListener(new BasicDialogWithResultCancelListener(result));
alertBuilder.SetPositiveButton(config.OkButtonText ?? AppResources.Ok, listener: null);
alertBuilder.SetNegativeButton(config.CancelButtonText ?? AppResources.Cancel, (sender, args) => result.TrySetResult(null));
if (!string.IsNullOrEmpty(config.ThirdButtonText))
@@ -552,6 +547,12 @@ namespace Bit.Droid.Services
return true;
}
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;
public bool SupportsDrawOver() => Build.VERSION.SdkInt >= BuildVersionCodes.M;
private Intent RateIntentForUrl(string url, Activity activity)
{
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
@@ -606,6 +607,38 @@ namespace Bit.Droid.Services
throw new NotImplementedException();
}
public string GetAutofillAccessibilityDescription()
{
if (Build.VERSION.SdkInt <= BuildVersionCodes.LollipopMr1)
{
return AppResources.AccessibilityDescription;
}
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
{
return AppResources.AccessibilityDescription2;
}
if (Build.VERSION.SdkInt <= BuildVersionCodes.NMr1)
{
return AppResources.AccessibilityDescription3;
}
return AppResources.AccessibilityDescription4;
}
public string GetAutofillDrawOverDescription()
{
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
{
return AppResources.DrawOverDescription;
}
if (Build.VERSION.SdkInt <= BuildVersionCodes.NMr1)
{
return AppResources.DrawOverDescription2;
}
return AppResources.DrawOverDescription3;
}
private void SetNumericKeyboardTo(EditText editText)
{
editText.InputType = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned;
@@ -614,4 +647,20 @@ namespace Bit.Droid.Services
#pragma warning restore CS0618 // Type or member is obsolete
}
}
class BasicDialogWithResultCancelListener : Java.Lang.Object, IDialogInterfaceOnCancelListener
{
private readonly TaskCompletionSource<ValidatablePromptResponse?> _taskCompletionSource;
public BasicDialogWithResultCancelListener(TaskCompletionSource<ValidatablePromptResponse?> taskCompletionSource)
{
_taskCompletionSource = taskCompletionSource;
}
public void OnCancel(IDialogInterface dialog)
{
_taskCompletionSource?.TrySetResult(null);
dialog?.Dismiss();
}
}
}

View File

@@ -190,7 +190,8 @@ namespace Bit.Droid.Services
{
try
{
var file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
var tmpDir = new Java.IO.File(activity.FilesDir, Constants.TEMP_CAMERA_IMAGE_DIR);
var file = new Java.IO.File(tmpDir, Constants.TEMP_CAMERA_IMAGE_NAME);
if (!file.Exists())
{
file.ParentFile.Mkdirs();

View File

@@ -28,6 +28,9 @@ namespace Bit.App.Abstractions
bool SupportsNfc();
bool SupportsCamera();
bool SupportsFido2();
bool SupportsAutofillServices();
bool SupportsInlineAutofill();
bool SupportsDrawOver();
bool LaunchApp(string appName);
void RateApp();
@@ -41,5 +44,7 @@ namespace Bit.App.Abstractions
Task SetScreenCaptureAllowedAsync();
void OpenAppSettings();
void CloseExtensionPopUp();
string GetAutofillAccessibilityDescription();
string GetAutofillDrawOverDescription();
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Bit.Core.Enums;
namespace Bit.App.Abstractions
{
@@ -6,10 +7,8 @@ namespace Bit.App.Abstractions
{
string[] ProtectedFields { get; }
Task<bool> ShowPasswordPromptAsync();
Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
Task<bool> Enabled();
}
}

View File

@@ -16,9 +16,9 @@
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.88.3" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.6" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.5" />
<PackageReference Include="Xamarin.Essentials" Version="1.8.0" />
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2578" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2612" />
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
<PackageReference Include="MessagePack" Version="2.4.59" />
@@ -59,9 +59,6 @@
<Compile Update="Pages\Settings\ExtensionPage.xaml.cs">
<DependentUpon>ExtensionPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Settings\AutofillServicesPage.xaml.cs">
<DependentUpon>AutofillServicesPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Settings\FolderAddEditPage.xaml.cs">
<DependentUpon>FolderAddEditPage.xaml</DependentUpon>
</Compile>
@@ -71,12 +68,6 @@
<Compile Update="Pages\Settings\ExportVaultPage.xaml.cs">
<DependentUpon>ExportVaultPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Settings\OptionsPage.xaml.cs">
<DependentUpon>OptionsPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Settings\SyncPage.xaml.cs">
<DependentUpon>SyncPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Vault\AttachmentsPage.xaml.cs">
<DependentUpon>AttachmentsPage.xaml</DependentUpon>
</Compile>
@@ -147,6 +138,7 @@
<Folder Include="Controls\PasswordStrengthProgressBar\" />
<Folder Include="Utilities\Automation\" />
<Folder Include="Utilities\Prompts\" />
<Folder Include="Controls\Settings\" />
</ItemGroup>
<ItemGroup>
@@ -444,5 +436,6 @@
<None Remove="Controls\PasswordStrengthProgressBar\" />
<None Remove="Utilities\Automation\" />
<None Remove="Utilities\Prompts\" />
<None Remove="Controls\Settings\" />
</ItemGroup>
</Project>

View File

@@ -91,7 +91,7 @@ namespace Bit.App
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
});
}
else if (message.Command == "resumed")
else if (message.Command == AppHelpers.RESUMED_MESSAGE_COMMAND)
{
if (Device.RuntimePlatform == Device.iOS)
{
@@ -365,7 +365,7 @@ namespace Bit.App
await Device.InvokeOnMainThreadAsync(() =>
{
ThemeManager.SetTheme(Current.Resources);
_messagingService.Send("updatedTheme");
_messagingService.Send(ThemeManager.UPDATED_THEME_MESSAGE_KEY);
});
}

View File

@@ -36,7 +36,7 @@ namespace Bit.App.Controls
public bool ShowHostname
{
get => !string.IsNullOrWhiteSpace(AccountView.Hostname) && AccountView.Hostname != "vault.bitwarden.com";
get => !string.IsNullOrWhiteSpace(AccountView.Hostname);
}
public bool IsActive

View File

@@ -31,7 +31,7 @@ namespace Bit.App.Controls
public bool ShowIconImage
{
get => WebsiteIconsEnabled
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
&& !string.IsNullOrWhiteSpace(Cipher.LaunchUri)
&& IconImageSource != null;
}
@@ -41,7 +41,7 @@ namespace Bit.App.Controls
{
if (_iconImageSource == string.Empty) // default value since icon source can return null
{
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
_iconImageSource = IconImageHelper.GetIconImage(Cipher);
}
return _iconImageSource;
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:Class="Bit.App.Controls.ExternalLinkItemView"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:Name="_contentView">
<ContentView.GestureRecognizers>
<TapGestureRecognizer Command="{Binding GoToLinkCommand, Mode=OneWay, Source={x:Reference _contentView}}" />
</ContentView.GestureRecognizers>
<StackLayout
Orientation="Horizontal">
<controls:CustomLabel
Text="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}"
HorizontalOptions="StartAndExpand"
LineBreakMode="TailTruncation" />
<controls:IconLabel
Text="{Binding Source={x:Static core:BitwardenIcons.ShareSquare}}"
TextColor="{DynamicResource TextColor}"
HorizontalOptions="End"
VerticalOptions="Center"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}" />
</StackLayout>
</ContentView>

View File

@@ -0,0 +1,31 @@
using System.Windows.Input;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class ExternalLinkItemView : ContentView
{
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
nameof(Title), typeof(string), typeof(ExternalLinkItemView), null, BindingMode.OneWay);
public static readonly BindableProperty GoToLinkCommandProperty = BindableProperty.Create(
nameof(GoToLinkCommand), typeof(ICommand), typeof(ExternalLinkItemView));
public ExternalLinkItemView()
{
InitializeComponent();
}
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public ICommand GoToLinkCommand
{
get => GetValue(GoToLinkCommandProperty) as ICommand;
set => SetValue(GoToLinkCommandProperty, value);
}
}
}

View File

@@ -0,0 +1,25 @@
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class BaseSettingItemView : ContentView
{
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
nameof(Title), typeof(string), typeof(SwitchItemView), null, BindingMode.OneWay);
public static readonly BindableProperty SubtitleProperty = BindableProperty.Create(
nameof(Subtitle), typeof(string), typeof(SwitchItemView), null, BindingMode.OneWay);
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public string Subtitle
{
get { return (string)GetValue(SubtitleProperty); }
set { SetValue(SubtitleProperty, value); }
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<controls:BaseSettingItemView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:Class="Bit.App.Controls.SettingChooserItemView"
x:Name="_contentView"
ControlTemplate="{StaticResource SettingControlTemplate}">
<controls:BaseSettingItemView.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ChooseCommand, Mode=OneWay, Source={x:Reference _contentView}}" />
</controls:BaseSettingItemView.GestureRecognizers>
<controls:CustomLabel
Text="{Binding DisplayValue, Source={x:Reference _contentView}}"
HorizontalTextAlignment="End"
TextColor="{DynamicResource MutedColor}"
StyleClass="list-sub" />
</controls:BaseSettingItemView>

View File

@@ -0,0 +1,31 @@
using System.Windows.Input;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class SettingChooserItemView : BaseSettingItemView
{
public static readonly BindableProperty DisplayValueProperty = BindableProperty.Create(
nameof(DisplayValue), typeof(string), typeof(SettingChooserItemView), null, BindingMode.OneWay);
public static readonly BindableProperty ChooseCommandProperty = BindableProperty.Create(
nameof(ChooseCommand), typeof(ICommand), typeof(ExternalLinkItemView));
public string DisplayValue
{
get { return (string)GetValue(DisplayValueProperty); }
set { SetValue(DisplayValueProperty, value); }
}
public SettingChooserItemView()
{
InitializeComponent();
}
public ICommand ChooseCommand
{
get => GetValue(ChooseCommandProperty) as ICommand;
set => SetValue(ChooseCommandProperty, value);
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<controls:BaseSettingItemView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:Class="Bit.App.Controls.SwitchItemView"
x:Name="_contentView"
ControlTemplate="{StaticResource SettingControlTemplate}">
<controls:BaseSettingItemView.GestureRecognizers>
<TapGestureRecognizer Tapped="ContentView_Tapped" />
</controls:BaseSettingItemView.GestureRecognizers>
<Switch
x:Name="_switch"
HeightRequest="20"
Scale="{OnPlatform iOS=0.8, Android=1}"
IsToggled="{Binding IsToggled, Mode=TwoWay, Source={x:Reference _contentView}}"
AutomationId="{Binding SwitchAutomationId, Mode=OneWay, Source={x:Reference _contentView}}"/>
</controls:BaseSettingItemView>

View File

@@ -0,0 +1,45 @@
using System.Windows.Input;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class SwitchItemView : BaseSettingItemView
{
public static readonly BindableProperty IsToggledProperty = BindableProperty.Create(
nameof(IsToggled), typeof(bool), typeof(SwitchItemView), null, BindingMode.TwoWay);
public static readonly BindableProperty SwitchAutomationIdProperty = BindableProperty.Create(
nameof(SwitchAutomationId), typeof(string), typeof(SwitchItemView), null, BindingMode.OneWay);
public static readonly BindableProperty ToggleSwitchCommandProperty = BindableProperty.Create(
nameof(ToggleSwitchCommand), typeof(ICommand), typeof(ExternalLinkItemView));
public SwitchItemView()
{
InitializeComponent();
}
public bool IsToggled
{
get { return (bool)GetValue(IsToggledProperty); }
set { SetValue(IsToggledProperty, value); }
}
public string SwitchAutomationId
{
get { return (string)GetValue(SwitchAutomationIdProperty); }
set { SetValue(SwitchAutomationIdProperty, value); }
}
public ICommand ToggleSwitchCommand
{
get => GetValue(ToggleSwitchCommandProperty) as ICommand;
set => SetValue(ToggleSwitchCommandProperty, value);
}
void ContentView_Tapped(System.Object sender, System.EventArgs e)
{
_switch.IsToggled = !_switch.IsToggled;
}
}
}

View File

@@ -64,7 +64,7 @@ namespace Bit.App.Pages
}
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
{
if (message.Command == "updatedTheme")
if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
{
Device.BeginInvokeOnMainThread(() =>
{

View File

@@ -7,17 +7,17 @@ using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Response;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class HomeViewModel : BaseViewModel
{
private const string LOGGING_IN_ON_US = "bitwarden.com";
private const string LOGGING_IN_ON_EU = "bitwarden.eu";
private readonly IStateService _stateService;
private readonly IMessagingService _messagingService;
private readonly IPlatformUtilsService _platformUtilsService;
@@ -30,8 +30,6 @@ namespace Bit.App.Pages
private bool _rememberEmail;
private string _email;
private string _selectedEnvironmentName;
private bool _isEmailEnabled;
private bool _canLogin;
private bool _displayEuEnvironment;
public HomeViewModel()
@@ -86,7 +84,7 @@ namespace Bit.App.Pages
set => SetProperty(ref _selectedEnvironmentName, value);
}
public string RegionText => $"{AppResources.Region}:";
public string RegionText => $"{AppResources.LoggingInOn}:";
public bool CanContinue => !string.IsNullOrEmpty(Email);
public FormattedString CreateAccountText
@@ -167,12 +165,12 @@ namespace Bit.App.Pages
{
_displayEuEnvironment = await _configService.GetFeatureFlagBoolAsync(Constants.DisplayEuEnvironmentFlag);
var options = _displayEuEnvironment
? new string[] { AppResources.US, AppResources.EU, AppResources.SelfHosted }
: new string[] { AppResources.US, AppResources.SelfHosted };
? new string[] { LOGGING_IN_ON_US, LOGGING_IN_ON_EU, AppResources.SelfHosted }
: new string[] { LOGGING_IN_ON_US, AppResources.SelfHosted };
await Device.InvokeOnMainThreadAsync(async () =>
{
var result = await Page.DisplayActionSheet(AppResources.DataRegion, AppResources.Cancel, null, options);
var result = await Page.DisplayActionSheet(AppResources.LoggingInOn, AppResources.Cancel, null, options);
if (result is null || result == AppResources.Cancel)
{
@@ -185,7 +183,7 @@ namespace Bit.App.Pages
return;
}
await _environmentService.SetUrlsAsync(result == AppResources.EU ? EnvironmentUrlData.DefaultEU : EnvironmentUrlData.DefaultUS);
await _environmentService.SetUrlsAsync(result == LOGGING_IN_ON_EU ? EnvironmentUrlData.DefaultEU : EnvironmentUrlData.DefaultUS);
await _configService.GetAsync(true);
SelectedEnvironmentName = result;
});
@@ -198,17 +196,17 @@ namespace Bit.App.Pages
{
await _environmentService.SetUrlsAsync(EnvironmentUrlData.DefaultUS);
environmentsSaved = EnvironmentUrlData.DefaultUS;
SelectedEnvironmentName = AppResources.US;
SelectedEnvironmentName = LOGGING_IN_ON_US;
return;
}
if (environmentsSaved.Base == EnvironmentUrlData.DefaultUS.Base)
{
SelectedEnvironmentName = AppResources.US;
SelectedEnvironmentName = LOGGING_IN_ON_US;
}
else if (environmentsSaved.Base == EnvironmentUrlData.DefaultEU.Base)
{
SelectedEnvironmentName = AppResources.EU;
SelectedEnvironmentName = LOGGING_IN_ON_EU;
}
else
{

View File

@@ -46,7 +46,7 @@
<StackLayout StyleClass="box">
<Grid
StyleClass="box-row"
IsVisible="{Binding PinLock}"
IsVisible="{Binding PinEnabled}"
Padding="0, 10, 0, 0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -89,7 +89,7 @@
<Grid
x:Name="_passwordGrid"
StyleClass="box-row"
IsVisible="{Binding PinLock, Converter={StaticResource inverseBool}}"
IsVisible="{Binding PinEnabled, Converter={StaticResource inverseBool}}"
Padding="0, 10, 0, 0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />

View File

@@ -20,13 +20,14 @@ namespace Bit.App.Pages
private bool _promptedAfterResume;
private bool _appeared;
public LockPage(AppOptions appOptions = null, bool autoPromptBiometric = true)
public LockPage(AppOptions appOptions = null, bool autoPromptBiometric = true, bool checkPendingAuthRequests = true)
{
_appOptions = appOptions;
_autoPromptBiometric = autoPromptBiometric;
InitializeComponent();
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
_vm = BindingContext as LockPageViewModel;
_vm.CheckPendingAuthRequests = checkPendingAuthRequests;
_vm.Page = this;
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
@@ -44,7 +45,7 @@ namespace Bit.App.Pages
{
get
{
if (_vm?.PinLock ?? false)
if (_vm?.PinEnabled ?? false)
{
return _pin;
}
@@ -54,7 +55,7 @@ namespace Bit.App.Pages
public async Task PromptBiometricAfterResumeAsync()
{
if (_vm.BiometricLock)
if (_vm.BiometricEnabled)
{
await Task.Delay(500);
if (!_promptedAfterResume)
@@ -91,13 +92,13 @@ namespace Bit.App.Pages
_vm.FocusSecretEntry += PerformFocusSecretEntry;
if (!_vm.BiometricLock)
if (!_vm.BiometricEnabled)
{
RequestFocus(SecretEntry);
}
else
{
if (_vm.UsingKeyConnector && !_vm.PinLock)
if (!_vm.HasMasterPassword && !_vm.PinEnabled)
{
_passwordGrid.IsVisible = false;
_unlockButton.IsVisible = false;

View File

@@ -7,6 +7,7 @@ using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Services;
@@ -27,27 +28,27 @@ namespace Bit.App.Pages
private readonly IEnvironmentService _environmentService;
private readonly IStateService _stateService;
private readonly IBiometricService _biometricService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly IUserVerificationService _userVerificationService;
private readonly ILogger _logger;
private readonly IWatchDeviceService _watchDeviceService;
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
private readonly IPolicyService _policyService;
private readonly IPasswordGenerationService _passwordGenerationService;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
private readonly ISyncService _syncService;
private string _email;
private string _masterPassword;
private string _pin;
private bool _showPassword;
private bool _pinLock;
private bool _biometricLock;
private PinLockType _pinStatus;
private bool _pinEnabled;
private bool _biometricEnabled;
private bool _biometricIntegrityValid = true;
private bool _biometricButtonVisible;
private bool _usingKeyConnector;
private bool _hasMasterPassword;
private string _biometricButtonText;
private string _loggedInAsText;
private string _lockedVerifyText;
private bool _isPinProtected;
private bool _isPinProtectedWithKey;
public LockPageViewModel()
{
@@ -60,17 +61,20 @@ namespace Bit.App.Pages
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
_logger = ServiceContainer.Resolve<ILogger>("logger");
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
_policyService = ServiceContainer.Resolve<IPolicyService>();
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
_syncService = ServiceContainer.Resolve<ISyncService>();
PageTitle = AppResources.VerifyMasterPassword;
TogglePasswordCommand = new Command(TogglePassword);
SubmitCommand = new Command(async () => await SubmitAsync());
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
AccountSwitchingOverlayViewModel =
new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
{
AllowAddAccountRow = true,
AllowActiveAccountSelection = true
@@ -100,21 +104,21 @@ namespace Bit.App.Pages
});
}
public bool PinLock
public bool PinEnabled
{
get => _pinLock;
set => SetProperty(ref _pinLock, value);
get => _pinEnabled;
set => SetProperty(ref _pinEnabled, value);
}
public bool UsingKeyConnector
public bool HasMasterPassword
{
get => _usingKeyConnector;
get => _hasMasterPassword;
}
public bool BiometricLock
public bool BiometricEnabled
{
get => _biometricLock;
set => SetProperty(ref _biometricLock, value);
get => _biometricEnabled;
set => SetProperty(ref _biometricEnabled, value);
}
public bool BiometricIntegrityValid
@@ -147,12 +151,18 @@ namespace Bit.App.Pages
set => SetProperty(ref _lockedVerifyText, value);
}
public bool CheckPendingAuthRequests { get; set; }
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public Command SubmitCommand { get; }
public Command TogglePasswordCommand { get; }
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 Action UnlockedAction { get; set; }
public event Action<int?> FocusSecretEntry
{
@@ -162,18 +172,33 @@ namespace Bit.App.Pages
public async Task InitAsync()
{
(_isPinProtected, _isPinProtectedWithKey) = await _vaultTimeoutService.IsPinLockSetAsync();
PinLock = (_isPinProtected && await _stateService.GetPinProtectedKeyAsync() != null) ||
_isPinProtectedWithKey;
BiometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _cryptoService.HasKeyAsync();
// Users with key connector and without biometric or pin has no MP to unlock with
_usingKeyConnector = await _keyConnectorService.GetUsesKeyConnector();
if (_usingKeyConnector && !(BiometricLock || PinLock))
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
if (pendingRequest != null && CheckPendingAuthRequests)
{
await _vaultTimeoutService.LogOutAsync();
return;
}
_pinStatus = await _vaultTimeoutService.GetPinLockTypeAsync();
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
?? await _stateService.GetPinProtectedKeyAsync();
PinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) ||
_pinStatus == PinLockType.Persistent;
BiometricEnabled = await IsBiometricsEnabledAsync();
// Users without MP and without biometric or pin has no MP to unlock with
_hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync();
if (await _stateService.IsAuthenticatedAsync()
&& !_hasMasterPassword
&& !BiometricEnabled
&& !PinEnabled)
{
await _vaultTimeoutService.LogOutAsync();
return;
}
_email = await _stateService.GetEmailAsync();
if (string.IsNullOrWhiteSpace(_email))
{
@@ -188,26 +213,20 @@ namespace Bit.App.Pages
}
var webVaultHostname = CoreHelpers.GetHostname(webVault);
LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, webVaultHostname);
if (PinLock)
if (PinEnabled)
{
PageTitle = AppResources.VerifyPIN;
LockedVerifyText = AppResources.VaultLockedPIN;
}
else
{
if (_usingKeyConnector)
{
PageTitle = AppResources.UnlockVault;
LockedVerifyText = AppResources.VaultLockedIdentity;
}
else
{
PageTitle = AppResources.VerifyMasterPassword;
LockedVerifyText = AppResources.VaultLockedMasterPassword;
}
PageTitle = _hasMasterPassword ? AppResources.VerifyMasterPassword : AppResources.UnlockVault;
LockedVerifyText = _hasMasterPassword
? AppResources.VaultLockedMasterPassword
: AppResources.VaultLockedIdentity;
}
if (BiometricLock)
if (BiometricEnabled)
{
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
if (!_biometricIntegrityValid)
@@ -223,59 +242,98 @@ namespace Bit.App.Pages
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
AppResources.UseFingerprintToUnlock;
}
}
}
public async Task SubmitAsync()
{
if (PinLock && string.IsNullOrWhiteSpace(Pin))
ShowPassword = false;
try
{
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
if (PinEnabled)
{
await UnlockWithPinAsync(kdfConfig);
}
else
{
await UnlockWithMasterPasswordAsync(kdfConfig);
}
}
catch (LegacyUserException)
{
await HandleLegacyUserAsync();
}
}
private async Task UnlockWithPinAsync(KdfConfig kdfConfig)
{
if (PinEnabled && string.IsNullOrWhiteSpace(Pin))
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
string.Format(AppResources.ValidationFieldRequired, AppResources.PIN),
AppResources.Ok);
return;
}
if (!PinLock && string.IsNullOrWhiteSpace(MasterPassword))
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
AppResources.Ok);
return;
}
ShowPassword = false;
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
if (PinLock)
{
var failed = true;
try
{
if (_isPinProtected)
EncString userKeyPin;
EncString oldPinProtected;
switch (_pinStatus)
{
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email,
case PinLockType.Persistent:
{
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
break;
}
case PinLockType.Transient:
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
break;
case PinLockType.Disabled:
default:
throw new Exception("Pin is disabled");
}
UserKey userKey;
if (oldPinProtected != null)
{
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
_pinStatus == PinLockType.Transient,
Pin,
_email,
kdfConfig,
await _stateService.GetPinProtectedKeyAsync());
var encKey = await _cryptoService.GetEncKeyAsync(key);
oldPinProtected
);
}
else
{
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
Pin,
_email,
kdfConfig,
userKeyPin
);
}
var protectedPin = await _stateService.GetProtectedPinAsync();
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
failed = decPin != Pin;
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
failed = decryptedPin != Pin;
if (!failed)
{
Pin = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await SetKeyAndContinueAsync(key);
await SetUserKeyAndContinueAsync(userKey);
}
}
else
catch (LegacyUserException)
{
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email, kdfConfig);
failed = false;
Pin = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await SetKeyAndContinueAsync(key);
}
throw;
}
catch
{
@@ -293,21 +351,38 @@ namespace Bit.App.Pages
AppResources.AnErrorHasOccurred);
}
}
else
private async Task UnlockWithMasterPasswordAsync(KdfConfig kdfConfig)
{
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdfConfig);
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword))
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
AppResources.Ok);
return;
}
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig);
if (await _cryptoService.IsLegacyUserAsync(masterKey))
{
throw new LegacyUserException();
}
var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync();
var passwordValid = false;
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
if (storedKeyHash != null)
{
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, key);
// Offline unlock possible
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey);
}
else
{
// Online unlock required
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey,
HashPurpose.ServerAuthorization);
var request = new PasswordVerificationRequest();
request.MasterPasswordHash = keyHash;
@@ -316,8 +391,9 @@ namespace Bit.App.Pages
var response = await _apiService.PostAccountVerifyPasswordAsync(request);
enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
passwordValid = true;
var localKeyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
await _cryptoService.SetKeyHashAsync(localKeyHash);
var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey,
HashPurpose.LocalAuthorization);
await _cryptoService.SetMasterKeyHashAsync(localKeyHash);
}
catch (Exception e)
{
@@ -325,17 +401,9 @@ namespace Bit.App.Pages
}
await _deviceActionService.HideLoadingAsync();
}
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))
{
// Save the ForcePasswordResetReason to force a password reset after unlock
@@ -345,10 +413,13 @@ namespace Bit.App.Pages
MasterPassword = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await SetKeyAndContinueAsync(key);
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
await _cryptoService.SetMasterKeyAsync(masterKey);
await SetUserKeyAndContinueAsync(userKey);
// Re-enable biometrics
if (BiometricLock & !BiometricIntegrityValid)
if (BiometricEnabled & !BiometricIntegrityValid)
{
await _biometricService.SetupBiometricAsync();
}
@@ -365,7 +436,6 @@ namespace Bit.App.Pages
AppResources.AnErrorHasOccurred);
}
}
}
/// <summary>
/// Checks if the master password requires updating to meet the enforced policy requirements
@@ -425,44 +495,82 @@ namespace Bit.App.Pages
public void TogglePassword()
{
ShowPassword = !ShowPassword;
var secret = PinLock ? Pin : MasterPassword;
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
var secret = PinEnabled ? Pin : MasterPassword;
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length,
nameof(FocusSecretEntry));
}
public async Task PromptBiometricAsync()
{
try
{
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
BiometricButtonVisible = BiometricIntegrityValid;
if (!BiometricLock || !BiometricIntegrityValid)
if (!BiometricEnabled || !BiometricIntegrityValid)
{
return;
}
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
PinLock ? AppResources.PIN : AppResources.MasterPassword,
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)),
!PinEnabled && !HasMasterPassword);
await _stateService.SetBiometricLockedAsync(!success);
if (success)
{
await DoContinueAsync();
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
await SetUserKeyAndContinueAsync(userKey);
}
}
catch (LegacyUserException)
{
await HandleLegacyUserAsync();
}
}
private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key)
private async Task SetUserKeyAndContinueAsync(UserKey key)
{
var hasKey = await _cryptoService.HasKeyAsync();
var hasKey = await _cryptoService.HasUserKeyAsync();
if (!hasKey)
{
await _cryptoService.SetKeyAsync(key);
await _cryptoService.SetUserKeyAsync(key);
}
await _deviceTrustCryptoService.TrustDeviceIfNeededAsync();
await DoContinueAsync();
}
private async Task DoContinueAsync()
{
_syncService.FullSyncAsync(false).FireAndForget();
await _stateService.SetBiometricLockedAsync(false);
_watchDeviceService.SyncDataToWatchAsync().FireAndForget();
_messagingService.Send("unlocked");
UnlockedAction?.Invoke();
}
private async Task<bool> IsBiometricsEnabledAsync()
{
try
{
return await _vaultTimeoutService.IsBiometricLockSetAsync() &&
await _biometricService.CanUseBiometricsUnlockAsync();
}
catch (LegacyUserException)
{
await HandleLegacyUserAsync();
}
return false;
}
private async Task HandleLegacyUserAsync()
{
// Legacy users must migrate on web vault.
await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong,
AppResources.AnErrorHasOccurred,
AppResources.Ok);
await _vaultTimeoutService.LogOutAsync();
}
}
}

View File

@@ -0,0 +1,80 @@
<?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}"
AutomationId="RememberThisDeviceSwitch"
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 IsNewUser}"
AutomationId="ContinueButton" />
<Button
x:Name="_approveWithMyOtherDevice"
Text="{u:I18n ApproveWithMyOtherDevice}"
StyleClass="btn-primary"
Command="{Binding ApproveWithMyOtherDeviceCommand}"
IsVisible="{Binding ApproveWithMyOtherDeviceEnabled}"
AutomationId="ApproveWithMyOtherDeviceButton" />
<Button
x:Name="_requestAdminApproval"
Text="{u:I18n RequestAdminApproval}"
StyleClass="box-button-row"
Command="{Binding RequestAdminApprovalCommand}"
IsVisible="{Binding RequestAdminApprovalEnabled}"
AutomationId="RequestAdminApprovalButton" />
<Button
x:Name="_approveWithMasterPassword"
Text="{u:I18n ApproveWithMasterPassword}"
StyleClass="box-button-row"
Command="{Binding ApproveWithMasterPasswordCommand}"
IsVisible="{Binding ApproveWithMasterPasswordEnabled}"
AutomationId="ApproveWithMasterPasswordButton" />
<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 Command="{Binding LogoutCommand}" />
</Label.GestureRecognizers>
</Label>
</StackLayout>
</StackLayout>
</pages:BaseContentPage>

View File

@@ -0,0 +1,64 @@
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 = () => StartLogInWithMasterPasswordAsync().FireAndForget();
_vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget();
_vm.RequestAdminApprovalAction = () => RequestAdminApprovalAsync().FireAndForget();
_vm.ContinueToVaultAction = () => ContinueToVaultAsync().FireAndForget();
_vm.Page = this;
_appOptions = appOptions;
}
protected override void OnAppearing()
{
_vm.InitAsync();
}
private async Task ContinueToVaultAsync()
{
if (AppHelpers.SetAlternateMainPage(_appOptions))
{
return;
}
var previousPage = await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
private async Task StartLogInWithMasterPasswordAsync()
{
var page = new LockPage(_appOptions, checkPendingAuthRequests: false);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task StartLoginWithDeviceAsync()
{
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions, true);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task RequestAdminApprovalAsync()
{
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AdminApproval, _appOptions, true);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
}

View File

@@ -0,0 +1,150 @@
using System;
using System.Security.Cryptography.X509Certificates;
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.Domain;
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 string _email;
private readonly IStateService _stateService;
private readonly IApiService _apiService;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
private readonly IAuthService _authService;
private readonly ISyncService _syncService;
private readonly IMessagingService _messagingService;
public ICommand ApproveWithMyOtherDeviceCommand { get; }
public ICommand RequestAdminApprovalCommand { get; }
public ICommand ApproveWithMasterPasswordCommand { get; }
public ICommand ContinueCommand { get; }
public ICommand LogoutCommand { get; }
public Action LogInWithMasterPasswordAction { get; set; }
public Action LogInWithDeviceAction { get; set; }
public Action RequestAdminApprovalAction { get; set; }
public Action ContinueToVaultAction { get; set; }
public LoginApproveDeviceViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>();
_apiService = ServiceContainer.Resolve<IApiService>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
_authService = ServiceContainer.Resolve<IAuthService>();
_syncService = ServiceContainer.Resolve<ISyncService>();
_messagingService = ServiceContainer.Resolve<IMessagingService>();
PageTitle = AppResources.LogInInitiated;
RememberThisDevice = true;
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(CreateNewSsoUserAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
LogoutCommand = new Command(() => _messagingService.Send(AccountsManagerMessageCommands.LOGOUT));
}
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,
additionalPropertyNames: new[] { nameof(IsNewUser) });
}
public bool ApproveWithMasterPasswordEnabled
{
get => _approveWithMasterPasswordEnabled;
set => SetProperty(ref _approveWithMasterPasswordEnabled, value,
additionalPropertyNames: new[] { nameof(IsNewUser) });
}
public bool IsNewUser => !RequestAdminApprovalEnabled && !ApproveWithMasterPasswordEnabled;
public string Email
{
get => _email;
set => SetProperty(ref _email, value, additionalPropertyNames:
new string[] {
nameof(LoggingInAsText)
});
}
public async Task InitAsync()
{
try
{
Email = await _stateService.GetActiveUserEmailAsync();
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
RequestAdminApprovalEnabled = decryptOptions?.TrustedDeviceOption?.HasAdminApproval ?? false;
ApproveWithMasterPasswordEnabled = decryptOptions?.HasMasterPassword ?? false;
ApproveWithMyOtherDeviceEnabled = decryptOptions?.TrustedDeviceOption?.HasLoginApprovingDevice ?? false;
}
catch (Exception ex)
{
HandleException(ex);
}
}
public async Task CreateNewSsoUserAsync()
{
await _authService.CreateNewSsoUserAsync(await _stateService.GetRememberedOrgIdentifierAsync());
if (RememberThisDevice)
{
await _deviceTrustCryptoService.TrustDeviceAsync();
}
_syncService.FullSyncAsync(true).FireAndForget();
await Device.InvokeOnMainThreadAsync(ContinueToVaultAction);
}
private async Task SetDeviceTrustAndInvokeAsync(Action action)
{
await _deviceTrustCryptoService.SetShouldTrustDeviceAsync(RememberThisDevice);
await Device.InvokeOnMainThreadAsync(action);
}
}
}

View File

@@ -4,6 +4,7 @@ using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
@@ -135,7 +136,7 @@ namespace Bit.App.Pages
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));
}

View File

@@ -248,6 +248,14 @@ namespace Bit.App.Pages
await _deviceActionService.HideLoadingAsync();
if (response.RequiresEncryptionKeyMigration)
{
// Legacy users must migrate on web vault.
await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong, AppResources.AnErrorHasOccurred,
AppResources.Ok);
return;
}
if (response.TwoFactor)
{
StartTwoFactorAction?.Invoke();

View File

@@ -14,59 +14,61 @@
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1" x:Name="_closeItem"/>
<ToolbarItem Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1" x:Name="_closeItem" />
</ContentPage.ToolbarItems>
<ScrollView>
<StackLayout
Padding="7, 0, 7, 20">
<Label
Text="{u:I18n LogInInitiated}"
Text="{Binding Title}"
FontSize="Title"
FontAttributes="Bold"
Margin="0,14,0,21"
AutomationId="LogInInitiatedLabel" />
<Label
Text="{u:I18n ANotificationHasBeenSentToYourDevice}"
Text="{Binding SubTitle}"
FontSize="Small"
Margin="0,0,0,10"/>
Margin="0,0,0,10"
AutomationId="SubTitleLabel" />
<Label
Text="{u:I18n PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice}"
Text="{Binding Description}"
FontSize="Small"
Margin="0,0,0,24"/>
Margin="0,0,0,24"
AutomationId="DescriptionLabel" />
<Label
Text="{u:I18n FingerprintPhrase}"
FontSize="Small"
FontAttributes="Bold"/>
FontAttributes="Bold" />
<controls:MonoLabel
FormattedText="{Binding FingerprintPhrase}"
FontSize="Medium"
FontSize="Small"
TextColor="{DynamicResource FingerprintPhrase}"
AutomationId="FingerprintPhraseValue" />
<Label
Text="{u:I18n ResendNotification}"
StyleClass="text-md"
IsVisible="{Binding ResendNotificationVisible}"
StyleClass="text-sm"
FontAttributes="Bold"
HorizontalOptions="Start"
Margin="0,40,0,0"
Margin="0,24,0,0"
TextColor="{DynamicResource HyperlinkColor}"
AutomationId="ResendNotificationButton">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CreatePasswordlessLoginCommand}" />
</Label.GestureRecognizers>
</Label>
<StackLayout
Orientation="Horizontal"
Margin="0,30,0,0">
<BoxView
HeightRequest="1"
Margin="0,24,0,24"
Color="{DynamicResource DisabledIconColor}" />
<Label
Text="{u:I18n NeedAnotherOption}"
FontSize="Small"
VerticalTextAlignment="End"/>
Text="{Binding OtherOptions}"
FontSize="Small" />
<Label
Text="{u:I18n ViewAllLoginOptions}"
StyleClass="text-md"
VerticalTextAlignment="End"
VerticalOptions="CenterAndExpand"
Margin="5, 0"
StyleClass="text-sm"
FontAttributes="Bold"
TextColor="{DynamicResource HyperlinkColor}"
AutomationId="ViewAllLoginOptionsButton">
<Label.GestureRecognizers>
@@ -74,7 +76,5 @@
</Label.GestureRecognizers>
</Label>
</StackLayout>
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View File

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

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -11,7 +12,9 @@ using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Response;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
@@ -32,6 +35,9 @@ namespace Bit.App.Pages
private IPlatformUtilsService _platformUtilsService;
private IEnvironmentService _environmentService;
private ILogger _logger;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly ICryptoService _cryptoService;
protected override II18nService i18nService => _i18nService;
protected override IEnvironmentService environmentService => _environmentService;
@@ -44,6 +50,7 @@ namespace Bit.App.Pages
private string _email;
private string _requestId;
private string _requestAccessCode;
private AuthRequestType _authRequestType;
// Item1 publicKey, Item2 privateKey
private Tuple<byte[], byte[]> _requestKeyPair;
@@ -57,8 +64,9 @@ namespace Bit.App.Pages
_i18nService = ServiceContainer.Resolve<II18nService>();
_stateService = ServiceContainer.Resolve<IStateService>();
_logger = ServiceContainer.Resolve<ILogger>();
PageTitle = AppResources.LogInWithAnotherDevice;
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>();
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
CreatePasswordlessLoginCommand = new AsyncCommand(CreatePasswordlessLoginAsync,
onException: ex => HandleException(ex),
@@ -73,10 +81,91 @@ namespace Bit.App.Pages
public Action LogInSuccessAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
public Action CloseAction { get; set; }
public bool AuthingWithSso { get; set; }
public ICommand CreatePasswordlessLoginCommand { get; }
public ICommand CloseCommand { get; }
public string HeaderTitle
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.LogInWithDevice;
case AuthRequestType.AdminApproval:
return AppResources.LogInInitiated;
default:
return string.Empty;
};
}
}
public string Title
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.LogInInitiated;
case AuthRequestType.AdminApproval:
return AppResources.AdminApprovalRequested;
default:
return string.Empty;
};
}
}
public string SubTitle
{
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.LogInWithDeviceMustBeSetUpInTheSettingsOfTheBitwardenAppNeedAnotherOption;
case AuthRequestType.AdminApproval:
return AppResources.TroubleLoggingIn;
default:
return string.Empty;
};
}
}
public string FingerprintPhrase
{
get => _fingerprintPhrase;
@@ -89,6 +178,25 @@ namespace Bit.App.Pages
set => SetProperty(ref _email, value);
}
public AuthRequestType AuthRequestType
{
get => _authRequestType;
set
{
SetProperty(ref _authRequestType, value, additionalPropertyNames: new string[]
{
nameof(Title),
nameof(SubTitle),
nameof(Description),
nameof(OtherOptions),
nameof(ResendNotificationVisible)
});
PageTitle = HeaderTitle;
}
}
public bool ResendNotificationVisible => AuthRequestType == AuthRequestType.AuthenticateAndUnlock;
public void StartCheckLoginRequestStatus()
{
try
@@ -119,25 +227,39 @@ namespace Bit.App.Pages
private async Task CheckLoginRequestStatus()
{
if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_requestAccessCode))
if (string.IsNullOrEmpty(_requestId))
{
return;
}
try
{
var response = await _authService.GetPasswordlessLoginResponseAsync(_requestId, _requestAccessCode);
PasswordlessLoginResponse response = null;
if (AuthingWithSso)
{
response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId);
}
else
{
response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode);
}
if (response.RequestApproved == null || !response.RequestApproved.Value)
if (response?.RequestApproved != true)
{
return;
}
StopCheckLoginRequestStatus();
var authResult = await _authService.LogInPasswordlessAsync(Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
var authResult = await _authService.LogInPasswordlessAsync(AuthingWithSso, Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
if (authResult == null && await _stateService.IsAuthenticatedAsync())
{
await HandleLoginCompleteAsync();
return;
}
if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus))
{
return;
@@ -153,10 +275,13 @@ namespace Bit.App.Pages
}
else
{
_syncService.FullSyncAsync(true).FireAndForget();
LogInSuccessAction?.Invoke();
await HandleLoginCompleteAsync();
}
}
catch (ApiException ex) when (ex.Error?.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
HandleException(ex);
}
catch (Exception ex)
{
StartCheckLoginRequestStatus();
@@ -164,31 +289,66 @@ namespace Bit.App.Pages
}
}
private async Task HandleLoginCompleteAsync()
{
await _stateService.SetPendingAdminAuthRequestAsync(null);
_syncService.FullSyncAsync(true).FireAndForget();
LogInSuccessAction?.Invoke();
}
private async Task CreatePasswordlessLoginAsync()
{
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
var response = await _authService.PasswordlessCreateLoginRequestAsync(_email);
if (response != null)
PasswordlessLoginResponse 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;
response = 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;
_requestId = response.Id;
_requestAccessCode = response.RequestAccessCode;
_requestKeyPair = response.RequestKeyPair;
}
await _deviceActionService.HideLoadingAsync();
}
private void HandleException(Exception ex)
{
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
}).FireAndForget();
_logger.Exception(ex);
}
}
}

View File

@@ -21,7 +21,7 @@
<StackLayout StyleClass="box">
<Label Text="{u:I18n LogInSsoSummary}"
StyleClass="text-md"
HorizontalTextAlignment="Start"></Label>
HorizontalTextAlignment="Start" />
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n OrgIdentifier}"
@@ -32,13 +32,15 @@
Keyboard="Default"
StyleClass="box-value"
ReturnType="Go"
ReturnCommand="{Binding LogInCommand}" />
ReturnCommand="{Binding LogInCommand}"
AutomationId="OrgIdentifierEntry" />
</StackLayout>
</StackLayout>
<StackLayout Padding="10, 0">
<Button Text="{u:I18n LogIn}"
StyleClass="btn-primary"
Clicked="LogIn_Clicked"></Button>
Clicked="LogIn_Clicked"
AutomationId="LogInButton" />
</StackLayout>
</StackLayout>
</ScrollView>

View File

@@ -29,6 +29,8 @@ namespace Bit.App.Pages
_vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync());
_vm.UpdateTempPasswordAction =
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
_vm.StartDeviceApprovalOptionsAction =
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
_vm.CloseAction = async () =>
{
await Navigation.PopModalAsync();
@@ -106,10 +108,17 @@ namespace Bit.App.Pages
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task StartDeviceApprovalOptionsAsync()
{
var page = new LoginApproveDevicePage();
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task SsoAuthSuccessAsync()
{
RestoreAppOptionsFromCopy();
await AppHelpers.ClearPreviousPage();
if (await _vaultTimeoutService.IsLockedAsync())
{
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.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
@@ -29,8 +30,11 @@ namespace Bit.App.Pages
private readonly IStateService _stateService;
private readonly ILogger _logger;
private readonly IOrganizationService _organizationService;
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
private readonly ICryptoService _cryptoService;
private string _orgIdentifier;
private bool _useEphemeralWebBrowserSession;
public LoginSsoPageViewModel()
{
@@ -45,7 +49,8 @@ namespace Bit.App.Pages
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
_organizationService = ServiceContainer.Resolve<IOrganizationService>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
PageTitle = AppResources.Bitwarden;
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
@@ -61,6 +66,7 @@ namespace Bit.App.Pages
public Action StartTwoFactorAction { get; set; }
public Action StartSetPasswordAction { get; set; }
public Action SsoAuthSuccessAction { get; set; }
public Action StartDeviceApprovalOptionsAction { get; set; }
public Action CloseAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
@@ -109,7 +115,7 @@ namespace Bit.App.Pages
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
var response = await _apiService.PreValidateSso(OrgIdentifier);
var response = await _apiService.PreValidateSsoAsync(OrgIdentifier);
if (string.IsNullOrWhiteSpace(response?.Token))
{
@@ -140,10 +146,12 @@ namespace Bit.App.Pages
"ssoToken=" + Uri.EscapeDataString(ssoToken);
WebAuthenticatorResult authResult = null;
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
new Uri(REDIRECT_URI));
authResult = await WebAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions()
{
CallbackUrl = new Uri(REDIRECT_URI),
Url = new Uri(url),
PrefersEphemeralWebBrowserSession = _useEphemeralWebBrowserSession,
});
var code = GetResultCode(authResult, state);
if (!string.IsNullOrEmpty(code))
@@ -168,6 +176,8 @@ namespace Bit.App.Pages
{
// user canceled
await _deviceActionService.HideLoadingAsync();
// Workaroung for cached expired sso token PM-3551
_useEphemeralWebBrowserSession = true;
}
catch (Exception ex)
{
@@ -197,28 +207,93 @@ namespace Bit.App.Pages
try
{
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
await _deviceActionService.HideLoadingAsync();
if (response.TwoFactor)
{
StartTwoFactorAction?.Invoke();
return;
}
else if (response.ResetMasterPassword)
// Trusted device option is sent regardless if this is a trusted device or not
// If it is trusted, it will have the necessary keys
if (decryptOptions?.TrustedDeviceOption != null)
{
if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
{
// If we have a device key but no keys on server, we need to remove the device key
if (decryptOptions.TrustedDeviceOption.EncryptedPrivateKey == null && decryptOptions.TrustedDeviceOption.EncryptedUserKey == null)
{
await _deviceTrustCryptoService.RemoveTrustedDeviceAsync();
StartDeviceApprovalOptionsAction?.Invoke();
return;
}
// 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();
return;
}
else if (response.ForcePasswordReset)
// Update temp password only if the device is trusted and therefore has a decrypted User Key set
if (response.ForcePasswordReset)
{
UpdateTempPasswordAction?.Invoke();
return;
}
else
// Device is trusted and has keys, so we can decrypt
_syncService.FullSyncAsync(true).FireAndForget();
SsoAuthSuccessAction?.Invoke();
return;
}
// Check for pending Admin Auth requests before navigating to device approval options
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
if (pendingRequest != null)
{
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
var authRequest = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
if (authRequest?.RequestApproved == true)
{
var authResult = await _authService.LogInPasswordlessAsync(true, await _stateService.GetActiveUserEmailAsync(), authRequest.RequestAccessCode, pendingRequest.Id, pendingRequest.PrivateKey, authRequest.Key, authRequest.MasterPasswordHash);
if (authResult == null && await _stateService.IsAuthenticatedAsync())
{
await Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(
() => _platformUtilsService.ShowToast("info", null, AppResources.LoginApproved));
await _stateService.SetPendingAdminAuthRequestAsync(null);
_syncService.FullSyncAsync(true).FireAndForget();
SsoAuthSuccessAction?.Invoke();
}
}
catch (Exception e)
else
{
await _stateService.SetPendingAdminAuthRequestAsync(null);
StartDeviceApprovalOptionsAction?.Invoke();
}
}
else
{
StartDeviceApprovalOptionsAction?.Invoke();
}
return;
}
// In the standard, non TDE case, a user must set password if they don't
// have one and they aren't using key connector.
// Note: TDE & Key connector are mutually exclusive org config options.
if (response.ResetMasterPassword || (decryptOptions?.RequireSetPassword == true))
{
// TODO: We need to look into how to handle this when Org removes TDE
// Will we have the User Key by now to set a new password?
StartSetPasswordAction?.Invoke();
return;
}
_syncService.FullSyncAsync(true).FireAndForget();
SsoAuthSuccessAction?.Invoke();
}
catch (Exception)
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,

View File

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

View File

@@ -30,14 +30,14 @@ namespace Bit.App.Pages
public async Task Init()
{
Organization = await _keyConnectorService.GetManagingOrganization();
Organization = await _keyConnectorService.GetManagingOrganizationAsync();
}
public async Task MigrateAccount()
{
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
await _keyConnectorService.MigrateUser();
await _keyConnectorService.MigrateUserAsync();
await _syncService.FullSyncAsync(true);
await _deviceActionService.HideLoadingAsync();
@@ -47,7 +47,7 @@ namespace Bit.App.Pages
{
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
await _apiService.PostLeaveOrganization(Organization.Id);
await _apiService.PostLeaveOrganizationAsync(Organization.Id);
await _syncService.FullSyncAsync(true);
await _deviceActionService.HideLoadingAsync();

View File

@@ -51,7 +51,8 @@
<Label
Text="{u:I18n ResetPasswordAutoEnrollInviteWarning}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Start" />
HorizontalTextAlignment="Start"
AutomationId="ResetPasswordAutoEnrollInviteWarningLabel" />
</Frame>
</Grid>
<Grid IsVisible="{Binding IsPolicyInEffect}"
@@ -73,7 +74,8 @@
<Label
Text="{Binding PolicySummary}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Start" />
HorizontalTextAlignment="Start"
AutomationId="PolicyInEffectLabel" />
</Frame>
</Grid>
<Grid StyleClass="box-row">
@@ -98,7 +100,8 @@
IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
Grid.Row="1"
Grid.Column="0" />
Grid.Column="0"
AutomationId="MasterPasswordField" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
@@ -108,7 +111,8 @@
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
AutomationId="ToggleMasterPasswordVisibilityButton" />
</Grid>
<Label
Text="{u:I18n MasterPasswordDescription}"
@@ -137,7 +141,8 @@
IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
Grid.Row="1"
Grid.Column="0" />
Grid.Column="0"
AutomationId="RetypePasswordField" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
@@ -147,7 +152,8 @@
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
AutomationId="ToggleRetypePasswordVisibilityButton" />
</Grid>
<StackLayout StyleClass="box-row">
<Label
@@ -158,7 +164,8 @@
Text="{Binding Hint}"
StyleClass="box-value"
ReturnType="Go"
ReturnCommand="{Binding SubmitCommand}" />
ReturnCommand="{Binding SubmitCommand}"
AutomationId="MasterPasswordHintLabel" />
</StackLayout>
<Label
Text="{u:I18n MasterPasswordHintDescription}"

View File

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

View File

@@ -4,6 +4,7 @@ using Bit.App.Controls;
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -17,26 +18,29 @@ namespace Bit.App.Pages
private TwoFactorPageViewModel _vm;
private bool _inited;
private bool _authingWithSso;
private string _orgIdentifier;
public TwoFactorPage(bool? authingWithSso = false, AppOptions appOptions = null, string orgIdentifier = null)
{
InitializeComponent();
SetActivityIndicator();
_authingWithSso = authingWithSso ?? false;
_appOptions = appOptions;
_orgIdentifier = orgIdentifier;
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_vm = BindingContext as TwoFactorPageViewModel;
_vm.Page = this;
_vm.AuthingWithSso = authingWithSso ?? false;
_vm.StartSetPasswordAction = () =>
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
_vm.TwoFactorAuthSuccessAction = () =>
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync());
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessToMainAsync());
_vm.LockAction = () =>
Device.BeginInvokeOnMainThread(TwoFactorAuthSuccessWithSSOLocked);
_vm.UpdateTempPasswordAction =
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
_vm.StartDeviceApprovalOptionsAction =
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
_vm.CloseAction = async () => await Navigation.PopModalAsync();
DuoWebView = _duoWebView;
if (Device.RuntimePlatform == Device.Android)
@@ -180,13 +184,18 @@ namespace Bit.App.Pages
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task TwoFactorAuthSuccessAsync()
private async Task StartDeviceApprovalOptionsAsync()
{
if (_authingWithSso)
var page = new LoginApproveDevicePage();
await Navigation.PushModalAsync(new NavigationPage(page));
}
private void TwoFactorAuthSuccessWithSSOLocked()
{
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
}
else
private async Task TwoFactorAuthSuccessToMainAsync()
{
if (AppHelpers.SetAlternateMainPage(_appOptions))
{
@@ -195,7 +204,6 @@ namespace Bit.App.Pages
var previousPage = await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
}
private void Token_TextChanged(object sender, TextChangedEventArgs e)
{

View File

@@ -11,6 +11,7 @@ using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Request;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Newtonsoft.Json;
using Xamarin.CommunityToolkit.ObjectModel;
@@ -32,12 +33,12 @@ namespace Bit.App.Pages
private readonly IStateService _stateService;
private readonly II18nService _i18nService;
private readonly IAppIdService _appIdService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ILogger _logger;
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
private TwoFactorProviderType? _selectedProviderType;
private string _totpInstruction;
private string _webVaultUrl = "https://vault.bitwarden.com";
private bool _authingWithSso = false;
private bool _enableContinue = false;
private bool _showContinue = true;
@@ -54,7 +55,9 @@ namespace Bit.App.Pages
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
_logger = ServiceContainer.Resolve<ILogger>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
PageTitle = AppResources.TwoStepLogin;
SubmitCommand = new Command(async () => await SubmitAsync());
@@ -69,6 +72,8 @@ namespace Bit.App.Pages
public bool Remember { get; set; }
public bool AuthingWithSso { get; set; }
public string Token { get; set; }
public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo ||
@@ -118,6 +123,8 @@ namespace Bit.App.Pages
public Command SubmitCommand { get; }
public ICommand MoreCommand { get; }
public Action TwoFactorAuthSuccessAction { get; set; }
public Action LockAction { get; set; }
public Action StartDeviceApprovalOptionsAction { get; set; }
public Action StartSetPasswordAction { get; set; }
public Action CloseAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
@@ -136,8 +143,6 @@ namespace Bit.App.Pages
return;
}
_authingWithSso = _authService.AuthingWithSso();
if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl))
{
_webVaultUrl = _environmentService.BaseUrl;
@@ -315,22 +320,85 @@ namespace Bit.App.Pages
var task = Task.Run(() => _syncService.FullSyncAsync(true));
await _deviceActionService.HideLoadingAsync();
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
_messagingService.Send("listenYubiKeyOTP", false);
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
if (_authingWithSso && result.ResetMasterPassword)
if (decryptOptions?.TrustedDeviceOption != null)
{
if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
{
// If we have a device key but no keys on server, we need to remove the device key
if (decryptOptions.TrustedDeviceOption.EncryptedPrivateKey == null && decryptOptions.TrustedDeviceOption.EncryptedUserKey == null)
{
await _deviceTrustCryptoService.RemoveTrustedDeviceAsync();
StartDeviceApprovalOptionsAction?.Invoke();
return;
}
// 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();
return;
}
else if (result.ForcePasswordReset)
// Update temp password only if the device is trusted and therefore has a decrypted User Key set
if (result.ForcePasswordReset)
{
UpdateTempPasswordAction?.Invoke();
return;
}
// Device is trusted and has keys, so we can decrypt
_syncService.FullSyncAsync(true).FireAndForget();
await TwoFactorAuthSuccessAsync();
return;
}
// Check for pending Admin Auth requests before navigating to device approval options
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
if (pendingRequest != null)
{
var authRequest = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
if (authRequest?.RequestApproved == true)
{
var authResult = await _authService.LogInPasswordlessAsync(true, await _stateService.GetActiveUserEmailAsync(), authRequest.RequestAccessCode, pendingRequest.Id, pendingRequest.PrivateKey, authRequest.Key, authRequest.MasterPasswordHash);
if (authResult == null && await _stateService.IsAuthenticatedAsync())
{
await Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(
() => _platformUtilsService.ShowToast("info", null, AppResources.LoginApproved));
await _stateService.SetPendingAdminAuthRequestAsync(null);
_syncService.FullSyncAsync(true).FireAndForget();
await TwoFactorAuthSuccessAsync();
}
}
else
{
TwoFactorAuthSuccessAction?.Invoke();
await _stateService.SetPendingAdminAuthRequestAsync(null);
StartDeviceApprovalOptionsAction?.Invoke();
}
}
else
{
StartDeviceApprovalOptionsAction?.Invoke();
}
return;
}
// In the standard, non TDE case, a user must set password if they don't
// have one and they aren't using key connector.
// Note: TDE & Key connector are mutually exclusive org config options.
if (result.ResetMasterPassword || (decryptOptions?.RequireSetPassword ?? false))
{
// TODO: We need to look into how to handle this when Org removes TDE
// Will we have the User Key by now to set a new password?
StartSetPasswordAction?.Invoke();
return;
}
_syncService.FullSyncAsync(true).FireAndForget();
await TwoFactorAuthSuccessAsync();
}
catch (ApiException e)
{
_captchaToken = null;
@@ -398,7 +466,8 @@ namespace Bit.App.Pages
{
Email = _authService.Email,
MasterPasswordHash = _authService.MasterPasswordHash,
DeviceIdentifier = await _appIdService.GetAppIdAsync()
DeviceIdentifier = await _appIdService.GetAppIdAsync(),
SsoEmail2FaSessionToken = _authService.SsoEmail2FaSessionToken
};
await _apiService.PostTwoFactorEmailAsync(request);
if (showLoading)
@@ -422,5 +491,17 @@ namespace Bit.App.Pages
return false;
}
}
public async Task TwoFactorAuthSuccessAsync()
{
if (AuthingWithSso && await _vaultTimeoutService.IsLockedAsync())
{
LockAction?.Invoke();
}
else
{
TwoFactorAuthSuccessAction?.Invoke();
}
}
}
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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"
@@ -48,7 +48,8 @@
<Label
Text="{Binding UpdateMasterPasswordWarningText }"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
HorizontalTextAlignment="Center"
AutomationId="UpdatePasswordWarningLabel" />
</Frame>
</Grid>
<Grid IsVisible="{Binding IsPolicyInEffect}"
@@ -71,7 +72,8 @@
<Label
Text="{Binding PolicySummary}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Start" />
HorizontalTextAlignment="Start"
AutomationId="PolicySummaryLabel" />
</Frame>
</Grid>
<Grid StyleClass="box-row" IsVisible="{Binding RequireCurrentPassword }">
@@ -96,7 +98,8 @@
IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
Grid.Row="1"
Grid.Column="0" />
Grid.Column="0"
AutomationId="MasterPasswordField" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
@@ -106,7 +109,8 @@
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
AutomationId="ToggleMasterPasswordVisibilityButton" />
</Grid>
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
@@ -130,7 +134,8 @@
IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
Grid.Row="1"
Grid.Column="0" />
Grid.Column="0"
AutomationId="NewPasswordField" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
@@ -140,7 +145,8 @@
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
AutomationId="NewPasswordVisibilityButton" />
</Grid>
</StackLayout>
<StackLayout StyleClass="box">
@@ -166,7 +172,8 @@
IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
Grid.Row="1"
Grid.Column="0" />
Grid.Column="0"
AutomationId="RetypePasswordField" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
@@ -176,7 +183,8 @@
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
AutomationId="ToggleRetypePasswordVisibilityButton" />
</Grid>
<StackLayout StyleClass="box-row">
<Label
@@ -187,7 +195,8 @@
Text="{Binding Hint}"
StyleClass="box-value"
ReturnType="Go"
ReturnCommand="{Binding SubmitCommand}" />
ReturnCommand="{Binding SubmitCommand}"
AutomationId="MasterPasswordHintLabel" />
</StackLayout>
<Label
Text="{u:I18n MasterPasswordHintDescription}"

View File

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

View File

@@ -150,6 +150,12 @@ namespace Bit.App.Pages
private async Task SaveActivityAsync()
{
SetServices();
if (await _stateService.GetActiveUserIdAsync() == null)
{
// Fresh install and/or all users logged out won't have an active user, skip saving last active time
return;
}
await _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime());
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace Bit.App.Pages
{
public class BaseModalContentPage : BaseContentPage
{
public BaseModalContentPage()
{
}
protected void PopModal_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
Navigation.PopModalAsync();
}
}
}
}

View File

@@ -1,11 +1,14 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -47,5 +50,24 @@ namespace Bit.App.Pages
_logger.Value.Exception(ex);
}
protected AsyncCommand CreateDefaultAsyncCommnad(Func<Task> execute, Func<object, bool> canExecute = null)
{
return new AsyncCommand(execute,
canExecute,
ex => HandleException(ex),
allowsMultipleExecutions: false);
}
protected async Task<bool> HasConnectivityAsync()
{
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await _platformUtilsService.Value.ShowDialogAsync(
AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return false;
}
return true;
}
}
}

View File

@@ -275,7 +275,7 @@
Margin="0,10,0,0"/>
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
x:Name="_anonAddyDomainNameEntry"
Text="{Binding AnonAddyDomainName}"
Text="{Binding AddyIoDomainName}"
StyleClass="box-value"
AutomationId="AnonAddyDomainNameEntry" />
</StackLayout>

View File

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Styles;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -79,7 +80,7 @@ namespace Bit.App.Pages
_broadcasterService.Subscribe(nameof(GeneratorPage), (message) =>
{
if (message.Command == "updatedTheme")
if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
{
Device.BeginInvokeOnMainThread(() => _vm.RedrawPassword());
}

View File

@@ -545,7 +545,7 @@ namespace Bit.App.Pages
set => SetProperty(ref _showForwardedEmailApiSecret, value);
}
public string AnonAddyDomainName
public string AddyIoDomainName
{
get => _usernameOptions.AnonAddyDomainName;
set
@@ -553,7 +553,7 @@ namespace Bit.App.Pages
if (_usernameOptions.AnonAddyDomainName != value)
{
_usernameOptions.AnonAddyDomainName = value;
TriggerPropertyChanged(nameof(AnonAddyDomainName));
TriggerPropertyChanged(nameof(AddyIoDomainName));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
@@ -793,7 +793,7 @@ namespace Bit.App.Pages
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
TriggerPropertyChanged(nameof(ForwardedEmailApiSecret));
TriggerPropertyChanged(nameof(ForwardedEmailApiSecretLabel));
TriggerPropertyChanged(nameof(AnonAddyDomainName));
TriggerPropertyChanged(nameof(AddyIoDomainName));
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
TriggerPropertyChanged(nameof(UsernameTypeSelected));

View File

@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
namespace Bit.App.Pages
{
public class PickerViewModel<TKey> : ExtendedViewModel
{
const string SELECTED_CHARACTER = "✓";
private readonly IDeviceActionService _deviceActionService;
private readonly ILogger _logger;
private readonly Func<TKey, Task<bool>> _onSelectionChangingAsync;
private readonly string _title;
public Dictionary<TKey, string> _items;
private TKey _selectedKey;
private TKey _defaultSelectedKeyIfFailsToFind;
private Func<TKey, Task> _afterSelectionChangedAsync;
public PickerViewModel(IDeviceActionService deviceActionService,
ILogger logger,
Func<TKey, Task<bool>> onSelectionChangingAsync,
string title,
Func<object, bool> canExecuteSelectOptionCommand = null,
Action<Exception> onSelectOptionCommandException = null)
{
_deviceActionService = deviceActionService;
_logger = logger;
_onSelectionChangingAsync = onSelectionChangingAsync;
_title = title;
SelectOptionCommand = new AsyncCommand(SelectOptionAsync, canExecuteSelectOptionCommand, onSelectOptionCommandException, allowsMultipleExecutions: false);
}
public AsyncCommand SelectOptionCommand { get; }
public TKey SelectedKey => _selectedKey;
public string SelectedValue
{
get
{
if (_items.TryGetValue(_selectedKey, out var option))
{
return option;
}
_selectedKey = _defaultSelectedKeyIfFailsToFind;
return _items[_selectedKey];
}
}
public void Init(Dictionary<TKey, string> items, TKey currentSelectedKey, TKey defaultSelectedKeyIfFailsToFind, bool logIfKeyNotFound = true)
{
_items = items;
_defaultSelectedKeyIfFailsToFind = defaultSelectedKeyIfFailsToFind;
Select(currentSelectedKey, logIfKeyNotFound);
}
public void Select(TKey key, bool logIfKeyNotFound = true)
{
if (!_items.ContainsKey(key))
{
if (logIfKeyNotFound)
{
_logger.Error($"There is no {_title} options for key: {key}");
}
key = _defaultSelectedKeyIfFailsToFind;
}
_selectedKey = key;
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(SelectedValue)));
}
private async Task SelectOptionAsync()
{
var selection = await _deviceActionService.DisplayActionSheetAsync(_title,
AppResources.Cancel,
null,
_items.Select(o => CreateSelectableOption(o.Value, EqualityComparer<TKey>.Default.Equals(o.Key, _selectedKey)))
.ToArray()
);
if (selection == null || selection == AppResources.Cancel)
{
return;
}
var sanitizedSelection = selection.Replace($"{SELECTED_CHARACTER} ", string.Empty);
var optionKey = _items.First(o => o.Value == sanitizedSelection).Key;
if (EqualityComparer<TKey>.Default.Equals(optionKey, _selectedKey)
||
!await _onSelectionChangingAsync(optionKey))
{
return;
}
_selectedKey = optionKey;
TriggerPropertyChanged(nameof(SelectedValue));
if (_afterSelectionChangedAsync != null)
{
await _afterSelectionChangedAsync(_selectedKey);
}
}
public void SetAfterSelectionChanged(Func<TKey, Task> afterSelectionChangedAsync) => _afterSelectionChangedAsync = afterSelectionChangedAsync;
private string CreateSelectableOption(string option, bool selected) => selected ? ToSelectedOption(option) : option;
private string ToSelectedOption(string option) => $"{SELECTED_CHARACTER} {option}";
}
}

View File

@@ -175,7 +175,7 @@
LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center"
HorizontalTextAlignment="Start"
AutomationId="SendNoFileChosenLabel" />
<Label
IsVisible="{Binding FileName, Converter={StaticResource notNull}}"
@@ -183,7 +183,7 @@
LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center"
HorizontalTextAlignment="Start"
AutomationId="SendCurrentFileNameLabel" />
<Button
Text="{u:I18n ChooseFile}"
@@ -197,7 +197,7 @@
Text="{u:I18n MaxFileSize}"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
HorizontalTextAlignment="Start" />
</StackLayout>
<Label
Text="{u:I18n TypeFileInfo}"
@@ -250,20 +250,6 @@
AutomationId="SendHideTextByDefaultToggle" />
</StackLayout>
</StackLayout>
<StackLayout
StyleClass="box-row, box-row-switch">
<Label
Text="{Binding ShareOnSaveText}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding ShareOnSave}"
IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End"
Margin="10,0,0,0"
AutomationId="SendShareSendAfterSaveToggle" />
</StackLayout>
<StackLayout
Orientation="Horizontal"
Spacing="0"

View File

@@ -127,10 +127,9 @@ namespace Bit.App.Pages
public SendType? Type { get; set; }
public byte[] FileData { get; set; }
public string NewPassword { get; set; }
public bool ShareOnSave { get; set; }
public bool DisableHideEmailControl { get; set; }
public bool IsAddFromShare { get; set; }
public string ShareOnSaveText => CopyInsteadOfShareAfterSaving ? AppResources.CopySendLinkOnSave : AppResources.ShareOnSave;
public bool CopyInsteadOfShareAfterSaving { get; set; }
public string OptionsAccessilibityText => ShowOptions ? AppResources.OptionsExpanded : AppResources.OptionsCollapsed;
public List<KeyValuePair<string, SendType>> TypeOptions { get; }
public List<KeyValuePair<string, string>> DeletionTypeOptions { get; }
@@ -184,15 +183,6 @@ namespace Bit.App.Pages
}
}
}
public bool CopyInsteadOfShareAfterSaving
{
get => _copyInsteadOfShareAfterSaving;
set
{
SetProperty(ref _copyInsteadOfShareAfterSaving, value);
TriggerPropertyChanged(nameof(ShareOnSaveText));
}
}
public SendView Send
{
get => _send;
@@ -412,19 +402,11 @@ namespace Bit.App.Pages
_messagingService.Send("sendUpdated");
}
if (!ShareOnSave)
{
_platformUtilsService.ShowToast("success", null,
EditMode ? AppResources.SendUpdated : AppResources.NewSendCreated);
}
if (!CopyInsteadOfShareAfterSaving)
{
await CloseAsync();
}
if (ShareOnSave)
{
var savedSend = await _sendService.GetAsync(sendId);
if (savedSend != null)
{
@@ -441,7 +423,6 @@ namespace Bit.App.Pages
await AppHelpers.ShareSendUrlAsync(savedSendView);
}
}
}
if (CopyInsteadOfShareAfterSaving)
{

View File

@@ -94,13 +94,13 @@
LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
HorizontalTextAlignment="Start" />
<Label
Margin="0, 5, 0, 0"
Text="{u:I18n MaxFileSize}"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
HorizontalTextAlignment="Start" />
</StackLayout>
</StackLayout>
<StackLayout
@@ -145,19 +145,6 @@
Margin="10,0,0,0" />
</StackLayout>
</StackLayout>
<StackLayout
StyleClass="box-row, box-row-switch">
<Label
Text="{Binding ShareOnSaveText}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding ShareOnSave}"
IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
<StackLayout
Orientation="Horizontal"
Spacing="0"

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"
xmlns:pages="clr-namespace:Bit.App.Pages"
x:DataType="pages:AboutSettingsPageViewModel"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:Class="Bit.App.Pages.AboutSettingsPage"
Title="{u:I18n About}">
<ContentPage.BindingContext>
<pages:AboutSettingsPageViewModel />
</ContentPage.BindingContext>
<StackLayout>
<controls:SwitchItemView
Title="{u:I18n SubmitCrashLogs}"
IsToggled="{Binding ShouldSubmitCrashLogs, Mode=TwoWay}"
AutomationId="SubmitCrashLogsSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<controls:ExternalLinkItemView
Title="{u:I18n BitwardenHelpCenter}"
GoToLinkCommand="{Binding GoToHelpCenterCommand}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<controls:ExternalLinkItemView
Title="{u:I18n ContactBitwardenSupport}"
GoToLinkCommand="{Binding ContactBitwardenSupportCommand}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<controls:ExternalLinkItemView
Title="{u:I18n WebVault}"
GoToLinkCommand="{Binding GoToWebVaultCommand}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<controls:ExternalLinkItemView
Title="{u:I18n LearnOrg}"
GoToLinkCommand="{Binding GoToLearnAboutOrgsCommand}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<controls:ExternalLinkItemView
Title="{u:I18n RateTheApp}"
GoToLinkCommand="{Binding RateTheAppCommand}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<StackLayout
Padding="16,12"
Orientation="Horizontal">
<controls:CustomLabel
Text="{Binding AppInfo}"
MaxLines="10"
StyleClass="box-footer-label"
HorizontalOptions="StartAndExpand"
LineBreakMode="TailTruncation" />
<controls:IconLabel
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
TextColor="Black"
HorizontalOptions="End"
VerticalOptions="Center"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyAppInformation}">
<controls:IconLabel.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CopyAppInfoCommand}" />
</controls:IconLabel.GestureRecognizers>
</controls:IconLabel>
</StackLayout>
</StackLayout>
</pages:BaseContentPage>

View File

@@ -0,0 +1,37 @@
using System;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
namespace Bit.App.Pages
{
public partial class AboutSettingsPage : BaseContentPage
{
private readonly AboutSettingsPageViewModel _vm;
public AboutSettingsPage()
{
InitializeComponent();
_vm = BindingContext as AboutSettingsPageViewModel;
_vm.Page = this;
}
protected async override void OnAppearing()
{
base.OnAppearing();
try
{
await _vm.InitAsync();
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
Navigation.PopAsync().FireAndForget();
}
}
}
}

View File

@@ -0,0 +1,159 @@
using System;
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;
namespace Bit.App.Pages
{
public class AboutSettingsPageViewModel : BaseViewModel
{
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IDeviceActionService _deviceActionService;
private readonly ILogger _logger;
private bool _inited;
private bool _shouldSubmitCrashLogs;
public AboutSettingsPageViewModel()
{
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
_logger = ServiceContainer.Resolve<ILogger>();
var environmentService = ServiceContainer.Resolve<IEnvironmentService>();
var clipboardService = ServiceContainer.Resolve<IClipboardService>();
ToggleSubmitCrashLogsCommand = CreateDefaultAsyncCommnad(ToggleSubmitCrashLogsAsync);
GoToHelpCenterCommand = CreateDefaultAsyncCommnad(
() => LaunchUriAsync(AppResources.LearnMoreAboutHowToUseBitwardenOnTheHelpCenter,
AppResources.ContinueToHelpCenter,
ExternalLinksConstants.HELP_CENTER));
ContactBitwardenSupportCommand = CreateDefaultAsyncCommnad(
() => LaunchUriAsync(AppResources.ContactSupportDescriptionLong,
AppResources.ContinueToContactSupport,
ExternalLinksConstants.CONTACT_SUPPORT));
GoToWebVaultCommand = CreateDefaultAsyncCommnad(
() => LaunchUriAsync(AppResources.ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp,
AppResources.ContinueToWebApp,
environmentService.GetWebVaultUrl()));
GoToLearnAboutOrgsCommand = CreateDefaultAsyncCommnad(
() => LaunchUriAsync(AppResources.LearnAboutOrganizationsDescriptionLong,
string.Format(AppResources.ContinueToX, ExternalLinksConstants.BITWARDEN_WEBSITE),
ExternalLinksConstants.HELP_ABOUT_ORGANIZATIONS));
RateTheAppCommand = CreateDefaultAsyncCommnad(RateAppAsync);
CopyAppInfoCommand = CreateDefaultAsyncCommnad(
() => clipboardService.CopyTextAsync(AppInfo));
}
public bool ShouldSubmitCrashLogs
{
get => _shouldSubmitCrashLogs;
set
{
SetProperty(ref _shouldSubmitCrashLogs, value);
((ICommand)ToggleSubmitCrashLogsCommand).Execute(null);
}
}
public string AppInfo
{
get
{
var appInfo = string.Format("{0}: {1} ({2})",
AppResources.Version,
_platformUtilsService.GetApplicationVersion(),
_deviceActionService.GetBuildNumber());
return $"© Bitwarden Inc. 2015-{DateTime.Now.Year}\n\n{appInfo}";
}
}
public AsyncCommand ToggleSubmitCrashLogsCommand { get; }
public ICommand GoToHelpCenterCommand { get; }
public ICommand ContactBitwardenSupportCommand { get; }
public ICommand GoToWebVaultCommand { get; }
public ICommand GoToLearnAboutOrgsCommand { get; }
public ICommand RateTheAppCommand { get; }
public ICommand CopyAppInfoCommand { get; }
public async Task InitAsync()
{
_shouldSubmitCrashLogs = await _logger.IsEnabled();
_inited = true;
MainThread.BeginInvokeOnMainThread(() =>
{
TriggerPropertyChanged(nameof(ShouldSubmitCrashLogs));
ToggleSubmitCrashLogsCommand.RaiseCanExecuteChanged();
});
}
private async Task ToggleSubmitCrashLogsAsync()
{
await _logger.SetEnabled(ShouldSubmitCrashLogs);
_shouldSubmitCrashLogs = await _logger.IsEnabled();
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(ShouldSubmitCrashLogs)));
}
private async Task LaunchUriAsync(string dialogText, string dialogTitle, string uri)
{
if (await _platformUtilsService.ShowDialogAsync(dialogText, dialogTitle, AppResources.Continue, AppResources.Cancel))
{
_platformUtilsService.LaunchUri(uri);
}
}
private async Task RateAppAsync()
{
if (await _platformUtilsService.ShowDialogAsync(AppResources.RateAppDescriptionLong, AppResources.ContinueToAppStore, AppResources.Continue, AppResources.Cancel))
{
await MainThread.InvokeOnMainThreadAsync(_deviceActionService.RateApp);
}
}
/// INFO: Left here in case we need to debug push notifications
/// <summary>
/// Sets up app info plus debugging information for push notifications.
/// Useful when trying to solve problems regarding push notifications.
/// </summary>
/// <example>
/// Add an IniAsync() method to be called on view appearing, change the AppInfo to be a normal property with setter
/// and set the result of this method in the main thread to that property to show that in the UI.
/// </example>
// public async Task<string> GetAppInfoForPushNotificationsDebugAsync()
// {
// var stateService = ServiceContainer.Resolve<IStateService>();
// var appInfo = string.Format("{0}: {1} ({2})", AppResources.Version,
// _platformUtilsService.GetApplicationVersion(), _deviceActionService.GetBuildNumber());
//#if DEBUG
// var pushNotificationsRegistered = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService").IsRegisteredForPush;
// var pnServerRegDate = await stateService.GetPushLastRegistrationDateAsync();
// var pnServerError = await stateService.GetPushInstallationRegistrationErrorAsync();
// var pnServerRegDateMessage = default(DateTime) == pnServerRegDate ? "-" : $"{pnServerRegDate.GetValueOrDefault().ToShortDateString()}-{pnServerRegDate.GetValueOrDefault().ToShortTimeString()} UTC";
// var errorMessage = string.IsNullOrEmpty(pnServerError) ? string.Empty : $"Push Notifications Server Registration error: {pnServerError}";
// var text = string.Format("© Bitwarden Inc. 2015-{0}\n\n{1}\nPush Notifications registered:{2}\nPush Notifications Server Last Date :{3}\n{4}", DateTime.Now.Year, appInfo, pushNotificationsRegistered, pnServerRegDateMessage, errorMessage);
//#else
// var text = string.Format("© Bitwarden Inc. 2015-{0}\n\n{1}", DateTime.Now.Year, appInfo);
//#endif
// return text;
// }
}
}

View File

@@ -0,0 +1,58 @@
<?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"
xmlns:pages="clr-namespace:Bit.App.Pages"
x:DataType="pages:AppearanceSettingsPageViewModel"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:Class="Bit.App.Pages.AppearanceSettingsPage"
Title="{u:I18n Appearance}">
<ContentPage.BindingContext>
<pages:AppearanceSettingsPageViewModel />
</ContentPage.BindingContext>
<ScrollView Padding="0, 0, 0, 20">
<StackLayout Padding="0" Spacing="5">
<controls:SettingChooserItemView
Title="{u:I18n Language}"
Subtitle="{u:I18n LanguageChangeRequiresAppRestart}"
DisplayValue="{Binding LanguagePickerViewModel.SelectedValue}"
ChooseCommand="{Binding LanguagePickerViewModel.SelectOptionCommand}"
AutomationId="LanguageChooser"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand"/>
<controls:SettingChooserItemView
Title="{u:I18n Theme}"
Subtitle="{u:I18n ThemeDescription}"
DisplayValue="{Binding ThemePickerViewModel.SelectedValue}"
ChooseCommand="{Binding ThemePickerViewModel.SelectOptionCommand}"
AutomationId="ThemeChooser"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand"/>
<controls:SettingChooserItemView
Title="{u:I18n DefaultDarkTheme}"
Subtitle="{u:I18n DefaultDarkThemeDescriptionLong}"
DisplayValue="{Binding DefaultDarkThemePickerViewModel.SelectedValue}"
ChooseCommand="{Binding DefaultDarkThemePickerViewModel.SelectOptionCommand}"
IsVisible="{Binding ShowDefaultDarkThemePicker}"
AutomationId="DefaultDarkThemeChooser"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand"/>
<controls:SwitchItemView
Title="{u:I18n ShowWebsiteIcons}"
Subtitle="{u:I18n ShowWebsiteIconsDescription}"
IsToggled="{Binding ShowWebsiteIcons}"
IsEnabled="{Binding IsShowWebsiteIconsEnabled}"
AutomationId="ShowWebsiteIconsSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View File

@@ -0,0 +1,44 @@
using System;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
namespace Bit.App.Pages
{
public partial class AppearanceSettingsPage : BaseContentPage
{
private readonly AppearanceSettingsPageViewModel _vm;
public AppearanceSettingsPage()
{
InitializeComponent();
_vm = BindingContext as AppearanceSettingsPageViewModel;
_vm.Page = this;
}
protected async override void OnAppearing()
{
base.OnAppearing();
try
{
_vm.SubscribeEvents();
await _vm.InitAsync();
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
Navigation.PopModalAsync().FireAndForget();
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_vm.UnsubscribeEvents();
}
}
}

View File

@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class AppearanceSettingsPageViewModel : BaseViewModel
{
private readonly IStateService _stateService;
private readonly ILogger _logger;
private readonly II18nService _i18nService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IMessagingService _messagingService;
private bool _inited;
private bool _showWebsiteIcons;
public AppearanceSettingsPageViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>();
_logger = ServiceContainer.Resolve<ILogger>();
_i18nService = ServiceContainer.Resolve<II18nService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_messagingService = ServiceContainer.Resolve<IMessagingService>();
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
LanguagePickerViewModel = new PickerViewModel<string>(
deviceActionService,
_logger,
OnLanguageChangingAsync,
AppResources.Language,
_ => _inited,
ex => HandleException(ex));
ThemePickerViewModel = new PickerViewModel<string>(
deviceActionService,
_logger,
key => OnThemeChangingAsync(key, DefaultDarkThemePickerViewModel.SelectedKey),
AppResources.Theme,
_ => _inited,
ex => HandleException(ex));
ThemePickerViewModel.SetAfterSelectionChanged(_ =>
MainThread.InvokeOnMainThreadAsync(() =>
{
TriggerPropertyChanged(nameof(ShowDefaultDarkThemePicker));
}));
DefaultDarkThemePickerViewModel = new PickerViewModel<string>(
deviceActionService,
_logger,
key => OnThemeChangingAsync(ThemePickerViewModel.SelectedKey, key),
AppResources.DefaultDarkTheme,
_ => _inited,
ex => HandleException(ex));
ToggleShowWebsiteIconsCommand = CreateDefaultAsyncCommnad(ToggleShowWebsiteIconsAsync, _ => _inited);
}
public PickerViewModel<string> LanguagePickerViewModel { get; }
public PickerViewModel<string> ThemePickerViewModel { get; }
public PickerViewModel<string> DefaultDarkThemePickerViewModel { get; }
public bool ShowDefaultDarkThemePicker => ThemePickerViewModel.SelectedKey == string.Empty;
public bool ShowWebsiteIcons
{
get => _showWebsiteIcons;
set
{
if (SetProperty(ref _showWebsiteIcons, value))
{
((ICommand)ToggleShowWebsiteIconsCommand).Execute(null);
}
}
}
public bool IsShowWebsiteIconsEnabled => ToggleShowWebsiteIconsCommand.CanExecute(null);
public AsyncCommand ToggleShowWebsiteIconsCommand { get; }
public async Task InitAsync()
{
_showWebsiteIcons = !(await _stateService.GetDisableFaviconAsync() ?? false);
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(ShowWebsiteIcons)));
InitLanguagePicker();
await InitThemePickerAsync();
await InitDefaultDarkThemePickerAsync();
_inited = true;
MainThread.BeginInvokeOnMainThread(() =>
{
ToggleShowWebsiteIconsCommand.RaiseCanExecuteChanged();
LanguagePickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
ThemePickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
DefaultDarkThemePickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
});
}
private void InitLanguagePicker()
{
var options = new Dictionary<string, string>
{
[string.Empty] = AppResources.DefaultSystem
};
_i18nService.LocaleNames
.ToList()
.ForEach(pair => options[pair.Key] = pair.Value);
var selectedKey = _stateService.GetLocale() ?? string.Empty;
LanguagePickerViewModel.Init(options, selectedKey, string.Empty);
}
private async Task InitThemePickerAsync()
{
var options = new Dictionary<string, string>
{
[string.Empty] = AppResources.ThemeDefault,
[ThemeManager.Light] = AppResources.Light,
[ThemeManager.Dark] = AppResources.Dark,
[ThemeManager.Black] = AppResources.Black,
[ThemeManager.Nord] = AppResources.Nord,
[ThemeManager.SolarizedDark] = AppResources.SolarizedDark
};
var selectedKey = await _stateService.GetThemeAsync() ?? string.Empty;
ThemePickerViewModel.Init(options, selectedKey, string.Empty);
TriggerPropertyChanged(nameof(ShowDefaultDarkThemePicker));
}
private async Task InitDefaultDarkThemePickerAsync()
{
var options = new Dictionary<string, string>
{
[ThemeManager.Dark] = AppResources.Dark,
[ThemeManager.Black] = AppResources.Black,
[ThemeManager.Nord] = AppResources.Nord,
[ThemeManager.SolarizedDark] = AppResources.SolarizedDark
};
var selectedKey = await _stateService.GetAutoDarkThemeAsync() ?? ThemeManager.Dark;
DefaultDarkThemePickerViewModel.Init(options, selectedKey, ThemeManager.Dark);
}
private async Task<bool> OnLanguageChangingAsync(string selectedLanguage)
{
_stateService.SetLocale(selectedLanguage == string.Empty ? (string)null : selectedLanguage);
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.LanguageChangeXDescription, LanguagePickerViewModel.SelectedValue), AppResources.Language, AppResources.Ok);
return true;
}
private async Task<bool> OnThemeChangingAsync(string selectedTheme, string selectedDefaultDarkTheme)
{
await _stateService.SetThemeAsync(selectedTheme == string.Empty ? (string)null : selectedTheme);
await _stateService.SetAutoDarkThemeAsync(selectedDefaultDarkTheme == string.Empty ? (string)null : selectedDefaultDarkTheme);
await MainThread.InvokeOnMainThreadAsync(() =>
{
ThemeManager.SetTheme(Application.Current.Resources);
_messagingService.Send(ThemeManager.UPDATED_THEME_MESSAGE_KEY);
});
return true;
}
private async Task ToggleShowWebsiteIconsAsync()
{
// TODO: [PS-961] Fix negative function names
await _stateService.SetDisableFaviconAsync(!ShowWebsiteIcons);
}
private void ToggleShowWebsiteIconsCommand_CanExecuteChanged(object sender, EventArgs e)
{
TriggerPropertyChanged(nameof(IsShowWebsiteIconsEnabled));
}
internal void SubscribeEvents()
{
ToggleShowWebsiteIconsCommand.CanExecuteChanged += ToggleShowWebsiteIconsCommand_CanExecuteChanged;
}
internal void UnsubscribeEvents()
{
ToggleShowWebsiteIconsCommand.CanExecuteChanged -= ToggleShowWebsiteIconsCommand_CanExecuteChanged;
}
}
}

View File

@@ -1,131 +0,0 @@
<?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.AutofillServicesPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:AutofillServicesPageViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:AutofillServicesPageViewModel />
</ContentPage.BindingContext>
<ScrollView>
<StackLayout Padding="0" Spacing="20">
<StackLayout
StyleClass="box"
IsVisible="{Binding AutofillServiceVisible}">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n AutofillService}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<RelativeLayout HorizontalOptions="End">
<Switch
x:Name="AutofillServiceSwitch"
IsToggled="{Binding AutofillServiceToggled}"
StyleClass="box-value"
HorizontalOptions="End" />
<Button
Clicked="ToggleAutofillService"
StyleClass="box-overlay"
RelativeLayout.XConstraint="0"
RelativeLayout.YConstraint="0"
RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView, ElementName=AutofillServiceSwitch, Property=Width}"
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView, ElementName=AutofillServiceSwitch, Property=Height}" />
</RelativeLayout>
</StackLayout>
<Label
Text="{u:I18n AutofillServiceDescription}"
StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout>
<StackLayout
StyleClass="box"
IsVisible="{Binding InlineAutofillVisible}">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n InlineAutofill}"
StyleClass="box-label-regular"
IsEnabled="{Binding InlineAutofillEnabled}"
HorizontalOptions="StartAndExpand" />
<RelativeLayout HorizontalOptions="End">
<Switch
x:Name="InlineAutofillSwitch"
IsEnabled="{Binding InlineAutofillEnabled}"
IsToggled="{Binding InlineAutofillToggled}"
StyleClass="box-value"
HorizontalOptions="End" />
<Button
Clicked="ToggleInlineAutofill"
StyleClass="box-overlay"
RelativeLayout.XConstraint="0"
RelativeLayout.YConstraint="0"
RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView, ElementName=InlineAutofillSwitch, Property=Width}"
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView, ElementName=InlineAutofillSwitch, Property=Height}" />
</RelativeLayout>
</StackLayout>
<Label
Text="{u:I18n InlineAutofillDescription}"
StyleClass="box-footer-label, box-footer-label-switch"
IsEnabled="{Binding InlineAutofillEnabled}"/>
</StackLayout>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n Accessibility}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<RelativeLayout HorizontalOptions="End">
<Switch
x:Name="AccessibilitySwitch"
IsToggled="{Binding AccessibilityToggled}"
StyleClass="box-value"
HorizontalOptions="End" />
<Button
Command="{Binding ToggleAccessibilityCommand}"
StyleClass="box-overlay"
RelativeLayout.XConstraint="0"
RelativeLayout.YConstraint="0"
RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView, ElementName=AccessibilitySwitch, Property=Width}"
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView, ElementName=AccessibilitySwitch, Property=Height}" />
</RelativeLayout>
</StackLayout>
<Label
Text="{Binding AccessibilityDescriptionLabel}"
StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout>
<StackLayout
StyleClass="box"
IsVisible="{Binding DrawOverVisible}">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n DrawOver}"
StyleClass="box-label-regular"
IsEnabled="{Binding DrawOverEnabled}"
HorizontalOptions="StartAndExpand" />
<RelativeLayout HorizontalOptions="End">
<Switch
x:Name="DrawOverSwitch"
IsEnabled="{Binding DrawOverEnabled}"
IsToggled="{Binding DrawOverToggled}"
StyleClass="box-value"
HorizontalOptions="End" />
<Button
Clicked="ToggleDrawOver"
StyleClass="box-overlay"
RelativeLayout.XConstraint="0"
RelativeLayout.YConstraint="0"
RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView, ElementName=DrawOverSwitch, Property=Width}"
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView, ElementName=DrawOverSwitch, Property=Height}" />
</RelativeLayout>
</StackLayout>
<Label
Text="{Binding DrawOverDescriptionLabel}"
StyleClass="box-footer-label, box-footer-label-switch"
IsEnabled="{Binding InlineAutofillEnabled}"/>
</StackLayout>
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View File

@@ -1,66 +0,0 @@
using System;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class AutofillServicesPage : BaseContentPage
{
private readonly AutofillServicesPageViewModel _vm;
private readonly SettingsPage _settingsPage;
private DateTime? _timerStarted = null;
private TimeSpan _timerMaxLength = TimeSpan.FromMinutes(5);
public AutofillServicesPage(SettingsPage settingsPage)
{
InitializeComponent();
_vm = BindingContext as AutofillServicesPageViewModel;
_vm.Page = this;
_settingsPage = settingsPage;
}
protected async override void OnAppearing()
{
await _vm.InitAsync();
_vm.UpdateEnabled();
_timerStarted = DateTime.UtcNow;
Device.StartTimer(new TimeSpan(0, 0, 0, 0, 500), () =>
{
if (_timerStarted == null || (DateTime.UtcNow - _timerStarted) > _timerMaxLength)
{
return false;
}
_vm.UpdateEnabled();
return true;
});
base.OnAppearing();
}
protected override void OnDisappearing()
{
_timerStarted = null;
_settingsPage.BuildList();
base.OnDisappearing();
}
private void ToggleAutofillService(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.ToggleAutofillService();
}
}
private void ToggleInlineAutofill(object sender, EventArgs e)
{
_vm.ToggleInlineAutofill();
}
private void ToggleDrawOver(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.ToggleDrawOver();
}
}
}
}

View File

@@ -1,231 +0,0 @@
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Services;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
namespace Bit.App.Pages
{
public class AutofillServicesPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly IAutofillHandler _autofillHandler;
private readonly IStateService _stateService;
private readonly MobileI18nService _i18nService;
private readonly IPlatformUtilsService _platformUtilsService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private bool _autofillServiceToggled;
private bool _inlineAutofillToggled;
private bool _accessibilityToggled;
private bool _drawOverToggled;
private bool _inited;
public AutofillServicesPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService;
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
PageTitle = AppResources.AutofillServices;
ToggleAccessibilityCommand = new AsyncCommand(ToggleAccessibilityAsync,
onException: ex => _logger.Value.Exception(ex),
allowsMultipleExecutions: false);
}
#region Autofill Service
public bool AutofillServiceVisible
{
get => _deviceActionService.SystemMajorVersion() >= 26;
}
public bool AutofillServiceToggled
{
get => _autofillServiceToggled;
set => SetProperty(ref _autofillServiceToggled, value,
additionalPropertyNames: new string[]
{
nameof(InlineAutofillEnabled)
});
}
#endregion
#region Inline Autofill
public bool InlineAutofillVisible
{
get => _deviceActionService.SystemMajorVersion() >= 30;
}
public bool InlineAutofillEnabled
{
get => AutofillServiceToggled;
}
public bool InlineAutofillToggled
{
get => _inlineAutofillToggled;
set
{
if (SetProperty(ref _inlineAutofillToggled, value))
{
var task = UpdateInlineAutofillToggledAsync();
}
}
}
#endregion
#region Accessibility
public ICommand ToggleAccessibilityCommand { get; }
public string AccessibilityDescriptionLabel
{
get
{
if (_deviceActionService.SystemMajorVersion() <= 22)
{
// Android 5
return _i18nService.T("AccessibilityDescription");
}
if (_deviceActionService.SystemMajorVersion() == 23)
{
// Android 6
return _i18nService.T("AccessibilityDescription2");
}
if (_deviceActionService.SystemMajorVersion() == 24 || _deviceActionService.SystemMajorVersion() == 25)
{
// Android 7
return _i18nService.T("AccessibilityDescription3");
}
// Android 8+
return _i18nService.T("AccessibilityDescription4");
}
}
public bool AccessibilityToggled
{
get => _accessibilityToggled;
set => SetProperty(ref _accessibilityToggled, value,
additionalPropertyNames: new string[]
{
nameof(DrawOverEnabled)
});
}
#endregion
#region Draw-Over
public bool DrawOverVisible
{
get => _deviceActionService.SystemMajorVersion() >= 23;
}
public string DrawOverDescriptionLabel
{
get
{
if (_deviceActionService.SystemMajorVersion() <= 23)
{
// Android 6
return _i18nService.T("DrawOverDescription");
}
if (_deviceActionService.SystemMajorVersion() == 24 || _deviceActionService.SystemMajorVersion() == 25)
{
// Android 7
return _i18nService.T("DrawOverDescription2");
}
// Android 8+
return _i18nService.T("DrawOverDescription3");
}
}
public bool DrawOverEnabled
{
get => AccessibilityToggled;
}
public bool DrawOverToggled
{
get => _drawOverToggled;
set => SetProperty(ref _drawOverToggled, value);
}
#endregion
public async Task InitAsync()
{
InlineAutofillToggled = await _stateService.GetInlineAutofillEnabledAsync() ?? true;
_inited = true;
}
public void ToggleAutofillService()
{
if (!AutofillServiceToggled)
{
_deviceActionService.OpenAutofillSettings();
}
else
{
_autofillHandler.DisableAutofillService();
}
}
public void ToggleInlineAutofill()
{
if (!InlineAutofillEnabled)
{
return;
}
InlineAutofillToggled = !InlineAutofillToggled;
}
public async Task ToggleAccessibilityAsync()
{
if (!_autofillHandler.AutofillAccessibilityServiceRunning())
{
var accept = await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText,
AppResources.AccessibilityServiceDisclosure, AppResources.Accept,
AppResources.Decline);
if (!accept)
{
return;
}
}
_deviceActionService.OpenAccessibilitySettings();
}
public void ToggleDrawOver()
{
if (!DrawOverEnabled)
{
return;
}
_deviceActionService.OpenAccessibilityOverlayPermissionSettings();
}
public void UpdateEnabled()
{
AutofillServiceToggled =
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
AccessibilityToggled = _autofillHandler.AutofillAccessibilityServiceRunning();
DrawOverToggled = _autofillHandler.AutofillAccessibilityOverlayPermitted();
}
private async Task UpdateInlineAutofillToggledAsync()
{
if (_inited)
{
await _stateService.SetInlineAutofillEnabledAsync(InlineAutofillToggled);
}
}
}
}

View File

@@ -0,0 +1,133 @@
<?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"
xmlns:pages="clr-namespace:Bit.App.Pages"
x:DataType="pages:AutofillSettingsPageViewModel"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:Class="Bit.App.Pages.AutofillSettingsPage"
Title="{u:I18n Autofill}">
<ContentPage.BindingContext>
<pages:AutofillSettingsPageViewModel />
</ContentPage.BindingContext>
<ScrollView Padding="0, 0, 0, 20">
<StackLayout Padding="0" Spacing="5">
<controls:CustomLabel
Text="{u:I18n Autofill}"
StyleClass="settings-header" />
<controls:SwitchItemView
Title="{u:I18n AutofillServices}"
Subtitle="{u:I18n AutofillServicesExplanationLong}"
IsVisible="{Binding SupportsAndroidAutofillServices}"
IsToggled="{Binding UseAutofillServices}"
AutomationId="AutofillServicesSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:SwitchItemView
Title="{u:I18n InlineAutofill}"
Subtitle="{u:I18n UseInlineAutofillExplanationLong}"
IsToggled="{Binding UseInlineAutofill}"
IsVisible="{Binding ShowUseInlineAutofillToggle}"
IsEnabled="{Binding UseAutofillServices}"
AutomationId="InlineAutofillSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:SwitchItemView
Title="{u:I18n Accessibility}"
Subtitle="{Binding UseAccessibilityDescription}"
IsToggled="{Binding UseAccessibility}"
IsVisible="{Binding ShowUseAccessibilityToggle}"
AutomationId="AccessibilitySwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:SwitchItemView
Title="{u:I18n DrawOver}"
Subtitle="{Binding UseDrawOverDescription}"
IsToggled="{Binding UseDrawOver}"
IsVisible="{Binding ShowUseDrawOverToggle}"
IsEnabled="{Binding UseAccessibility}"
AutomationId="DrawOverSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:CustomLabel
Text="{u:I18n PasswordAutofill}"
StyleClass="settings-navigatable-label"
IsVisible="{Binding SupportsiOSAutofill}"
AutomationId="PasswordAutofillLabel">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding GoToPasswordAutofillCommand}" />
</Label.GestureRecognizers>
</controls:CustomLabel>
<controls:CustomLabel
Text="{u:I18n AppExtension}"
StyleClass="settings-navigatable-label"
IsVisible="{OnPlatform iOS=True, Android=False}"
AutomationId="AppExtensionLabel">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding GoToAppExtensionCommand}" />
</Label.GestureRecognizers>
</controls:CustomLabel>
<BoxView StyleClass="settings-box-row-separator" />
<controls:CustomLabel
Text="{u:I18n AdditionalOptions}"
StyleClass="settings-header" />
<controls:SwitchItemView
Title="{u:I18n CopyTotpAutomatically}"
Subtitle="{u:I18n CopyTotpAutomaticallyDescription}"
IsToggled="{Binding CopyTotpAutomatically}"
AutomationId="CopyTotpAutomaticallySwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:SwitchItemView
Title="{u:I18n AskToAddLogin}"
Subtitle="{u:I18n AskToAddLoginDescription}"
IsToggled="{Binding AskToAddLogin}"
IsVisible="{Binding SupportsAndroidAutofillServices}"
AutomationId="AskToAddLoginSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:SettingChooserItemView
Title="{u:I18n DefaultUriMatchDetection}"
Subtitle="{u:I18n DefaultUriMatchDetectionDescription}"
DisplayValue="{Binding DefaultUriMatchDetectionPickerViewModel.SelectedValue}"
ChooseCommand="{Binding DefaultUriMatchDetectionPickerViewModel.SelectOptionCommand}"
AutomationId="DefaultUriMatchDetectionChooser"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand"/>
<StackLayout
Spacing="0"
Padding="16,12"
IsVisible="{Binding SupportsAndroidAutofillServices}"
AutomationId="BlockAutoFillView">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding GoToBlockAutofillUrisCommand}" />
</StackLayout.GestureRecognizers>
<controls:CustomLabel
MaxLines="2"
Text="{u:I18n BlockAutoFill}"
HorizontalOptions="StartAndExpand"
LineBreakMode="TailTruncation" />
<Label
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
StyleClass="box-footer-label, box-footer-label-switch"
Margin="0,0,0,0"/>
</StackLayout>
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View File

@@ -0,0 +1,37 @@
using System;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
namespace Bit.App.Pages
{
public partial class AutofillSettingsPage : BaseContentPage
{
AutofillSettingsPageViewModel _vm;
public AutofillSettingsPage()
{
InitializeComponent();
_vm = BindingContext as AutofillSettingsPageViewModel;
_vm.Page = this;
}
protected async override void OnAppearing()
{
base.OnAppearing();
try
{
await _vm.InitAsync();
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
Navigation.PopAsync().FireAndForget();
}
}
}
}

View File

@@ -0,0 +1,181 @@
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class AutofillSettingsPageViewModel
{
private bool _useAutofillServices;
private bool _useInlineAutofill;
private bool _useAccessibility;
private bool _useDrawOver;
private bool _askToAddLogin;
public bool SupportsAndroidAutofillServices => Device.RuntimePlatform == Device.Android && _deviceActionService.SupportsAutofillServices();
public bool UseAutofillServices
{
get => _useAutofillServices;
set
{
if (SetProperty(ref _useAutofillServices, value))
{
((ICommand)ToggleUseAutofillServicesCommand).Execute(null);
}
}
}
public bool ShowUseInlineAutofillToggle => _deviceActionService.SupportsInlineAutofill();
public bool UseInlineAutofill
{
get => _useInlineAutofill;
set
{
if (SetProperty(ref _useInlineAutofill, value))
{
((ICommand)ToggleUseInlineAutofillCommand).Execute(null);
}
}
}
public bool ShowUseAccessibilityToggle => Device.RuntimePlatform == Device.Android;
public string UseAccessibilityDescription => _deviceActionService.GetAutofillAccessibilityDescription();
public bool UseAccessibility
{
get => _useAccessibility;
set
{
if (SetProperty(ref _useAccessibility, value))
{
((ICommand)ToggleUseAccessibilityCommand).Execute(null);
}
}
}
public bool ShowUseDrawOverToggle => _deviceActionService.SupportsDrawOver();
public bool UseDrawOver
{
get => _useDrawOver;
set
{
if (SetProperty(ref _useDrawOver, value))
{
((ICommand)ToggleUseDrawOverCommand).Execute(null);
}
}
}
public string UseDrawOverDescription => _deviceActionService.GetAutofillDrawOverDescription();
public bool AskToAddLogin
{
get => _askToAddLogin;
set
{
if (SetProperty(ref _askToAddLogin, value))
{
((ICommand)ToggleAskToAddLoginCommand).Execute(null);
}
}
}
public AsyncCommand ToggleUseAutofillServicesCommand { get; private set; }
public AsyncCommand ToggleUseInlineAutofillCommand { get; private set; }
public AsyncCommand ToggleUseAccessibilityCommand { get; private set; }
public AsyncCommand ToggleUseDrawOverCommand { get; private set; }
public AsyncCommand ToggleAskToAddLoginCommand { get; private set; }
public ICommand GoToBlockAutofillUrisCommand { get; private set; }
private void InitAndroidCommands()
{
ToggleUseAutofillServicesCommand = CreateDefaultAsyncCommnad(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseAutofillServices()), _ => _inited);
ToggleUseInlineAutofillCommand = CreateDefaultAsyncCommnad(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseInlineAutofillEnabledAsync()), _ => _inited);
ToggleUseAccessibilityCommand = CreateDefaultAsyncCommnad(ToggleUseAccessibilityAsync, _ => _inited);
ToggleUseDrawOverCommand = CreateDefaultAsyncCommnad(() => MainThread.InvokeOnMainThreadAsync(() => ToggleDrawOver()), _ => _inited);
ToggleAskToAddLoginCommand = CreateDefaultAsyncCommnad(ToggleAskToAddLoginAsync, _ => _inited);
GoToBlockAutofillUrisCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushAsync(new BlockAutofillUrisPage()));
}
private async Task InitAndroidAutofillSettingsAsync()
{
_useInlineAutofill = await _stateService.GetInlineAutofillEnabledAsync() ?? true;
await UpdateAndroidAutofillSettingsAsync();
await MainThread.InvokeOnMainThreadAsync(() =>
{
TriggerPropertyChanged(nameof(UseInlineAutofill));
});
}
private async Task UpdateAndroidAutofillSettingsAsync()
{
_useAutofillServices =
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
_useAccessibility = _autofillHandler.AutofillAccessibilityServiceRunning();
_useDrawOver = _autofillHandler.AutofillAccessibilityOverlayPermitted();
_askToAddLogin = await _stateService.GetAutofillDisableSavePromptAsync() != true;
await MainThread.InvokeOnMainThreadAsync(() =>
{
TriggerPropertyChanged(nameof(UseAutofillServices));
TriggerPropertyChanged(nameof(UseAccessibility));
TriggerPropertyChanged(nameof(UseDrawOver));
TriggerPropertyChanged(nameof(AskToAddLogin));
});
}
private void ToggleUseAutofillServices()
{
if (UseAutofillServices)
{
_deviceActionService.OpenAutofillSettings();
}
else
{
_autofillHandler.DisableAutofillService();
}
}
private async Task ToggleUseInlineAutofillEnabledAsync()
{
await _stateService.SetInlineAutofillEnabledAsync(UseInlineAutofill);
}
private async Task ToggleUseAccessibilityAsync()
{
if (!_autofillHandler.AutofillAccessibilityServiceRunning()
&&
!await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText, AppResources.AccessibilityServiceDisclosure,
AppResources.Accept, AppResources.Decline))
{
_useAccessibility = false;
await MainThread.InvokeOnMainThreadAsync(() => TriggerPropertyChanged(nameof(UseAccessibility)));
return;
}
_deviceActionService.OpenAccessibilitySettings();
}
private void ToggleDrawOver()
{
if (!UseAccessibility)
{
return;
}
_deviceActionService.OpenAccessibilityOverlayPermissionSettings();
}
private async Task ToggleAskToAddLoginAsync()
{
await _stateService.SetAutofillDisableSavePromptAsync(!AskToAddLogin);
}
}
}

View File

@@ -0,0 +1,111 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
namespace Bit.App.Pages
{
public partial class AutofillSettingsPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly IAutofillHandler _autofillHandler;
private readonly IStateService _stateService;
private readonly IPlatformUtilsService _platformUtilsService;
private bool _inited;
private bool _copyTotpAutomatically;
public AutofillSettingsPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
_stateService = ServiceContainer.Resolve<IStateService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
DefaultUriMatchDetectionPickerViewModel = new PickerViewModel<UriMatchType>(
_deviceActionService,
ServiceContainer.Resolve<ILogger>(),
DefaultUriMatchDetectionChangingAsync,
AppResources.DefaultUriMatchDetection,
_ => _inited,
ex => HandleException(ex));
ToggleCopyTotpAutomaticallyCommand = CreateDefaultAsyncCommnad(ToggleCopyTotpAutomaticallyAsync, _ => _inited);
InitAndroidCommands();
InitIOSCommands();
}
public bool CopyTotpAutomatically
{
get => _copyTotpAutomatically;
set
{
if (SetProperty(ref _copyTotpAutomatically, value))
{
((ICommand)ToggleCopyTotpAutomaticallyCommand).Execute(null);
}
}
}
public PickerViewModel<UriMatchType> DefaultUriMatchDetectionPickerViewModel { get; }
public AsyncCommand ToggleCopyTotpAutomaticallyCommand { get; private set; }
public async Task InitAsync()
{
await InitAndroidAutofillSettingsAsync();
_copyTotpAutomatically = await _stateService.GetDisableAutoTotpCopyAsync() != true;
await InitDefaultUriMatchDetectionPickerViewModelAsync();
_inited = true;
await MainThread.InvokeOnMainThreadAsync(() =>
{
TriggerPropertyChanged(nameof(CopyTotpAutomatically));
ToggleUseAutofillServicesCommand.RaiseCanExecuteChanged();
ToggleUseInlineAutofillCommand.RaiseCanExecuteChanged();
ToggleUseAccessibilityCommand.RaiseCanExecuteChanged();
ToggleUseDrawOverCommand.RaiseCanExecuteChanged();
DefaultUriMatchDetectionPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
});
}
private async Task InitDefaultUriMatchDetectionPickerViewModelAsync()
{
var options = new Dictionary<UriMatchType, string>
{
[UriMatchType.Domain] = AppResources.BaseDomain,
[UriMatchType.Host] = AppResources.Host,
[UriMatchType.StartsWith] = AppResources.StartsWith,
[UriMatchType.RegularExpression] = AppResources.RegEx,
[UriMatchType.Exact] = AppResources.Exact,
[UriMatchType.Never] = AppResources.Never
};
var defaultUriMatchDetection = ((UriMatchType?)await _stateService.GetDefaultUriMatchAsync()) ?? UriMatchType.Domain;
DefaultUriMatchDetectionPickerViewModel.Init(options, defaultUriMatchDetection, UriMatchType.Domain);
}
private async Task ToggleCopyTotpAutomaticallyAsync()
{
await _stateService.SetDisableAutoTotpCopyAsync(!CopyTotpAutomatically);
}
private async Task<bool> DefaultUriMatchDetectionChangingAsync(UriMatchType type)
{
await _stateService.SetDefaultUriMatchAsync((int?)type);
return true;
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Windows.Input;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class AutofillSettingsPageViewModel
{
public bool SupportsiOSAutofill => Device.RuntimePlatform == Device.iOS && _deviceActionService.SupportsAutofillServices();
public ICommand GoToPasswordAutofillCommand { get; private set; }
public ICommand GoToAppExtensionCommand { get; private set; }
private void InitIOSCommands()
{
GoToPasswordAutofillCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillPage())));
GoToAppExtensionCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new ExtensionPage())));
}
}
}

View File

@@ -7,6 +7,7 @@
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:BlockAutofillUrisPageViewModel"
NavigationPage.HasBackButton="False"
Title="{u:I18n BlockAutoFill}">
<ContentPage.BindingContext>
<pages:BlockAutofillUrisPageViewModel />

View File

@@ -38,7 +38,8 @@
<Label
Text="{u:I18n DisablePersonalVaultExportPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
HorizontalTextAlignment="Center"
AutomationId="DisablePrivateVaultPolicyLabel" />
</Frame>
<Grid
RowSpacing="10"
@@ -55,7 +56,8 @@
SelectedIndex="{Binding FileFormatSelectedIndex}"
SelectedIndexChanged="FileFormat_Changed"
StyleClass="box-value"
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}" />
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
AutomationId="FileFormatPicker" />
</StackLayout>
<StackLayout
StyleClass="box-row"
@@ -72,7 +74,8 @@
HorizontalOptions="Fill"
VerticalOptions="End"
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
Margin="0,0,0,10"/>
Margin="0,0,0,10"
AutomationId="SendTOTPCodeButton" />
</StackLayout>
<Grid
StyleClass="box-row"
@@ -96,7 +99,8 @@
Grid.Row="1"
Grid.Column="0"
ReturnType="Go"
ReturnCommand="{Binding ExportVaultCommand}" />
ReturnCommand="{Binding ExportVaultCommand}"
AutomationId="MasterPasswordEntry" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
@@ -106,7 +110,8 @@
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
IsVisible="{Binding UseOTPVerification, Converter={StaticResource inverseBool}}"/>
IsVisible="{Binding UseOTPVerification, Converter={StaticResource inverseBool}}"
AutomationId="TogglePasswordVisibilityButton" />
<Label
Text="{u:I18n ConfirmYourIdentity}"
StyleClass="box-footer-label"
@@ -128,7 +133,8 @@
Clicked="ExportVault_Clicked"
HorizontalOptions="Fill"
VerticalOptions="End"
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"/>
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
AutomationId="ExportVaultButton" />
</StackLayout>
</StackLayout>
</ScrollView>

View File

@@ -21,7 +21,6 @@ namespace Bit.App.Pages
private readonly II18nService _i18nService;
private readonly IExportService _exportService;
private readonly IPolicyService _policyService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly IUserVerificationService _userVerificationService;
private readonly IApiService _apiService;
private readonly ILogger _logger;
@@ -45,8 +44,7 @@ namespace Bit.App.Pages
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_exportService = ServiceContainer.Resolve<IExportService>("exportService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>("userVerificationService");
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
@@ -67,7 +65,7 @@ namespace Bit.App.Pages
_initialized = true;
FileFormatSelectedIndex = FileFormatOptions.FindIndex(k => k.Key == "json");
DisablePrivateVaultPolicyEnabled = await _policyService.PolicyAppliesToUser(PolicyType.DisablePersonalVaultExport);
UseOTPVerification = await _keyConnectorService.GetUsesKeyConnector();
UseOTPVerification = !await _userVerificationService.HasMasterPasswordAsync(true);
if (UseOTPVerification)
{
@@ -165,9 +163,9 @@ namespace Bit.App.Pages
return;
}
var verificationType = await _keyConnectorService.GetUsesKeyConnector()
? VerificationType.OTP
: VerificationType.MasterPassword;
var verificationType = await _userVerificationService.HasMasterPasswordAsync(true)
? VerificationType.MasterPassword
: VerificationType.OTP;
if (!await _userVerificationService.VerifyUser(Secret, verificationType))
{
return;

View File

@@ -23,6 +23,7 @@
<ContentPage.Resources>
<ResourceDictionary>
<u:DateTimeConverter x:Key="dateTime" />
<u:InverseBoolConverter x:Key="inverseBool" />
<xct:ItemSelectedEventArgsConverter x:Key="ItemSelectedEventArgsConverter" />
<controls:SelectionChangedEventArgsConverter x:Key="SelectionChangedEventArgsConverter" />
<DataTemplate
@@ -32,7 +33,8 @@
Padding="10, 0"
RowSpacing="0"
RowDefinitions="*, Auto, *, 10"
ColumnDefinitions="*, *">
ColumnDefinitions="*, *"
AutomationId="LoginRequestCell">
<Label
Text="{u:I18n FingerprintPhrase}"
FontSize="Small"
@@ -45,20 +47,23 @@
FontSize="Small"
Padding="0, 5, 0, 10"
VerticalTextAlignment="Center"
TextColor="{DynamicResource FingerprintPhrase}"/>
TextColor="{DynamicResource FingerprintPhrase}"
AutomationId="FingerprintPhraseLabel" />
<Label
Grid.Row="2"
HorizontalOptions="Start"
HorizontalTextAlignment="Start"
Text="{Binding RequestDeviceType}"
StyleClass="list-header-sub" />
StyleClass="list-header-sub"
AutomationId="RequestDeviceLabel" />
<Label
Grid.Row="2"
Grid.Column="1"
HorizontalOptions="End"
HorizontalTextAlignment="End"
Text="{Binding CreationDate, Converter={StaticResource dateTime}}"
StyleClass="list-header-sub" />
StyleClass="list-header-sub"
AutomationId="RequestDateLabel" />
<BoxView
StyleClass="list-section-separator-top, list-section-separator-top-platform"
VerticalOptions="End"
@@ -76,10 +81,29 @@
Command="{Binding RefreshCommand}"
VerticalOptions="FillAndExpand"
BackgroundColor="{DynamicResource BackgroundColor}">
<StackLayout>
<Image
x:Name="_emptyPlaceholder"
Source="empty_login_requests"
HorizontalOptions="Center"
WidthRequest="160"
HeightRequest="160"
Margin="0,70,0,0"
IsVisible="{Binding HasLoginRequests, Converter={StaticResource inverseBool}}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n NoPendingRequests}" />
<controls:CustomLabel
StyleClass="box-label-regular"
Text="{u:I18n NoPendingRequests}"
FontAttributes="{OnPlatform iOS=Bold}"
FontWeight="500"
HorizontalTextAlignment="Center"
Margin="14,10,14,0"/>
<controls:ExtendedCollectionView
ItemsSource="{Binding LoginRequests}"
ItemTemplate="{StaticResource loginRequestTemplate}"
SelectionMode="Single"
IsVisible="{Binding HasLoginRequests}"
ExtraDataForLogging="Login requests page" >
<controls:ExtendedCollectionView.Behaviors>
<xct:EventToCommandBehavior
@@ -88,13 +112,16 @@
EventArgsConverter="{StaticResource SelectionChangedEventArgsConverter}" />
</controls:ExtendedCollectionView.Behaviors>
</controls:ExtendedCollectionView>
</StackLayout>
</RefreshView>
<controls:IconLabelButton
VerticalOptions="End"
Margin="10,0"
Icon="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
Label="{u:I18n DeclineAllRequests}"
ButtonCommand="{Binding DeclineAllRequestsCommand}"/>
ButtonCommand="{Binding DeclineAllRequestsCommand}"
IsVisible="{Binding HasLoginRequests}"
AutomationId="DeleteAllRequestsButton" />
</StackLayout>
</ResourceDictionary>
</ContentPage.Resources>

View File

@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Models.Response;
using Bit.Core.Utilities;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -24,6 +27,8 @@ namespace Bit.App.Pages
{
base.OnAppearing();
await LoadOnAppearedAsync(_mainLayout, false, _vm.RefreshAsync, _mainContent);
UpdatePlaceholder();
}
private async void Close_Clicked(object sender, System.EventArgs e)
@@ -33,6 +38,22 @@ namespace Bit.App.Pages
await Navigation.PopModalAsync();
}
}
public override async Task UpdateOnThemeChanged()
{
await base.UpdateOnThemeChanged();
UpdatePlaceholder();
}
private void UpdatePlaceholder()
{
if (Device.RuntimePlatform == Device.Android)
{
MainThread.BeginInvokeOnMainThread(() =>
_emptyPlaceholder.Source = ImageSource.FromFile(ThemeManager.UsingLightTheme ? "empty_login_requests" : "empty_login_requests_dark"));
}
}
}
}

View File

@@ -9,6 +9,7 @@ using Bit.Core.Abstractions;
using Bit.Core.Models.Response;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -56,16 +57,14 @@ namespace Bit.App.Pages
set => SetProperty(ref _isRefreshing, value);
}
public bool HasLoginRequests => LoginRequests.Any();
public async Task RefreshAsync()
{
try
{
IsRefreshing = true;
LoginRequests.ReplaceRange(await _authService.GetActivePasswordlessLoginRequestsAsync());
if (!LoginRequests.Any())
{
Page.Navigation.PopModalAsync().FireAndForget();
}
}
catch (Exception ex)
{
@@ -74,6 +73,7 @@ namespace Bit.App.Pages
finally
{
IsRefreshing = false;
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(HasLoginRequests)));
}
}
@@ -136,4 +136,3 @@ namespace Bit.App.Pages
}
}
}

View File

@@ -1,169 +0,0 @@
<?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.OptionsPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:OptionsPageViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:OptionsPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
</ContentPage.ToolbarItems>
<ScrollView Padding="0, 0, 0, 20">
<StackLayout Padding="0" Spacing="20">
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
<Label
Text="{u:I18n Theme}"
StyleClass="box-label" />
<Picker
x:Name="_themePicker"
ItemsSource="{Binding ThemeOptions, Mode=OneTime}"
SelectedIndex="{Binding ThemeSelectedIndex}"
StyleClass="box-value"
AutomationId="ThemeSelectorPicker" />
</StackLayout>
<Label
StyleClass="box-footer-label"
Text="{u:I18n ThemeDescription}" />
</StackLayout>
<StackLayout
StyleClass="box"
IsVisible="{Binding ShowAutoDarkThemeOptions}">
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
<Label
Text="{u:I18n DefaultDarkTheme}"
StyleClass="box-label" />
<Picker
x:Name="_autoDarkThemePicker"
ItemsSource="{Binding AutoDarkThemeOptions, Mode=OneTime}"
SelectedIndex="{Binding AutoDarkThemeSelectedIndex}"
StyleClass="box-value"
AutomationId="DefaultDarkThemePicker" />
</StackLayout>
<Label
StyleClass="box-footer-label"
Text="{u:I18n DefaultDarkThemeDescription}" />
</StackLayout>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
<Label
Text="{u:I18n DefaultUriMatchDetection}"
StyleClass="box-label" />
<Picker
x:Name="_uriMatchPicker"
ItemsSource="{Binding UriMatchOptions, Mode=OneTime}"
SelectedIndex="{Binding UriMatchSelectedIndex}"
StyleClass="box-value"
AutomationId="DefaultUriMatchDetectionPicker" />
</StackLayout>
<Label
Text="{u:I18n DefaultUriMatchDetectionDescription}"
StyleClass="box-footer-label" />
</StackLayout>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
<Label
Text="{u:I18n ClearClipboard}"
StyleClass="box-label" />
<Picker
x:Name="_clearClipboardPicker"
ItemsSource="{Binding ClearClipboardOptions, Mode=OneTime}"
SelectedIndex="{Binding ClearClipboardSelectedIndex}"
StyleClass="box-value"
AutomationId="ClearClipboardPicker" />
</StackLayout>
<Label
Text="{u:I18n ClearClipboardDescription}"
StyleClass="box-footer-label" />
</StackLayout>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
<Label
Text="{u:I18n Language}"
StyleClass="box-label" />
<Picker
x:Name="_languagePicker"
ItemsSource="{Binding LocalesOptions, Mode=OneTime}"
SelectedItem="{Binding SelectedLocale}"
ItemDisplayBinding="{Binding Value}"
StyleClass="box-value"
AutomationId="LanguagePicker" />
</StackLayout>
<Label
Text="{u:I18n LanguageChangeRequiresAppRestart}"
StyleClass="box-footer-label" />
</StackLayout>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n CopyTotpAutomatically}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding AutoTotpCopy}"
StyleClass="box-value"
HorizontalOptions="End"
AutomationId="CopyTotpAutomaticallyToggle" />
</StackLayout>
<Label
Text="{u:I18n CopyTotpAutomaticallyDescription}"
StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n ShowWebsiteIcons}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Favicon}"
StyleClass="box-value"
HorizontalOptions="End"
AutomationId="ShowWebsiteIconsToggle" />
</StackLayout>
<Label
Text="{u:I18n ShowWebsiteIconsDescription}"
StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n AutofillService, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n AskToAddLogin}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding AutofillSavePrompt}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
<Label
Text="{u:I18n AskToAddLoginDescription}"
StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding GoToBlockAutofillUrisCommand}" />
</StackLayout.GestureRecognizers>
<Label
Text="{u:I18n BlockAutoFill}"
StyleClass="box-label-regular" />
<Label
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
StyleClass="box-footer-label" />
</StackLayout>
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View File

@@ -1,53 +0,0 @@
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
namespace Bit.App.Pages
{
public partial class OptionsPage : BaseContentPage
{
private readonly IAutofillHandler _autofillHandler;
private readonly OptionsPageViewModel _vm;
public OptionsPage()
{
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
InitializeComponent();
_vm = BindingContext as OptionsPageViewModel;
_vm.Page = this;
_themePicker.ItemDisplayBinding = new Binding("Value");
_autoDarkThemePicker.ItemDisplayBinding = new Binding("Value");
_uriMatchPicker.ItemDisplayBinding = new Binding("Value");
_clearClipboardPicker.ItemDisplayBinding = new Binding("Value");
if (Device.RuntimePlatform == Device.Android)
{
ToolbarItems.RemoveAt(0);
_vm.ShowAndroidAutofillSettings = _autofillHandler.SupportsAutofillService();
}
else
{
_themePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_autoDarkThemePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_uriMatchPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_clearClipboardPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_languagePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
}
}
protected async override void OnAppearing()
{
base.OnAppearing();
await _vm.InitAsync();
}
private async void Close_Clicked(object sender, System.EventArgs e)
{
if (DoOnce())
{
await Navigation.PopModalAsync();
}
}
}
}

View File

@@ -1,300 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class OptionsPageViewModel : BaseViewModel
{
private readonly IStateService _stateService;
private readonly IMessagingService _messagingService;
private readonly II18nService _i18nService;
private readonly IPlatformUtilsService _platformUtilsService;
private bool _autofillSavePrompt;
private bool _favicon;
private bool _autoTotpCopy;
private int _clearClipboardSelectedIndex;
private int _themeSelectedIndex;
private int _autoDarkThemeSelectedIndex;
private int _uriMatchSelectedIndex;
private KeyValuePair<string, string> _selectedLocale;
private bool _inited;
private bool _updatingAutofill;
private bool _showAndroidAutofillSettings;
public OptionsPageViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_i18nService = ServiceContainer.Resolve<II18nService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
PageTitle = AppResources.Options;
var iosIos = Device.RuntimePlatform == Device.iOS;
ClearClipboardOptions = new List<KeyValuePair<int?, string>>
{
new KeyValuePair<int?, string>(null, AppResources.Never),
new KeyValuePair<int?, string>(10, AppResources.TenSeconds),
new KeyValuePair<int?, string>(20, AppResources.TwentySeconds),
new KeyValuePair<int?, string>(30, AppResources.ThirtySeconds),
new KeyValuePair<int?, string>(60, AppResources.OneMinute)
};
if (!iosIos)
{
ClearClipboardOptions.Add(new KeyValuePair<int?, string>(120, AppResources.TwoMinutes));
ClearClipboardOptions.Add(new KeyValuePair<int?, string>(300, AppResources.FiveMinutes));
}
ThemeOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(null, AppResources.ThemeDefault),
new KeyValuePair<string, string>(ThemeManager.Light, AppResources.Light),
new KeyValuePair<string, string>(ThemeManager.Dark, AppResources.Dark),
new KeyValuePair<string, string>(ThemeManager.Black, AppResources.Black),
new KeyValuePair<string, string>(ThemeManager.Nord, AppResources.Nord),
new KeyValuePair<string, string>(ThemeManager.SolarizedDark, AppResources.SolarizedDark),
};
AutoDarkThemeOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(ThemeManager.Dark, AppResources.Dark),
new KeyValuePair<string, string>(ThemeManager.Black, AppResources.Black),
new KeyValuePair<string, string>(ThemeManager.Nord, AppResources.Nord),
new KeyValuePair<string, string>(ThemeManager.SolarizedDark, AppResources.SolarizedDark),
};
UriMatchOptions = new List<KeyValuePair<UriMatchType?, string>>
{
new KeyValuePair<UriMatchType?, string>(UriMatchType.Domain, AppResources.BaseDomain),
new KeyValuePair<UriMatchType?, string>(UriMatchType.Host, AppResources.Host),
new KeyValuePair<UriMatchType?, string>(UriMatchType.StartsWith, AppResources.StartsWith),
new KeyValuePair<UriMatchType?, string>(UriMatchType.RegularExpression, AppResources.RegEx),
new KeyValuePair<UriMatchType?, string>(UriMatchType.Exact, AppResources.Exact),
new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never),
};
LocalesOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(null, AppResources.DefaultSystem)
};
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<string, string>> ThemeOptions { get; set; }
public List<KeyValuePair<string, string>> AutoDarkThemeOptions { get; set; }
public List<KeyValuePair<UriMatchType?, string>> UriMatchOptions { get; set; }
public List<KeyValuePair<string, string>> LocalesOptions { get; }
public int ClearClipboardSelectedIndex
{
get => _clearClipboardSelectedIndex;
set
{
if (SetProperty(ref _clearClipboardSelectedIndex, value))
{
SaveClipboardChangedAsync().FireAndForget();
}
}
}
public int ThemeSelectedIndex
{
get => _themeSelectedIndex;
set
{
if (SetProperty(ref _themeSelectedIndex, value,
additionalPropertyNames: new[] { nameof(ShowAutoDarkThemeOptions) })
)
{
SaveThemeAsync().FireAndForget();
}
}
}
public bool ShowAutoDarkThemeOptions => ThemeOptions[ThemeSelectedIndex].Key == null;
public int AutoDarkThemeSelectedIndex
{
get => _autoDarkThemeSelectedIndex;
set
{
if (SetProperty(ref _autoDarkThemeSelectedIndex, value))
{
SaveThemeAsync().FireAndForget();
}
}
}
public int UriMatchSelectedIndex
{
get => _uriMatchSelectedIndex;
set
{
if (SetProperty(ref _uriMatchSelectedIndex, value))
{
SaveDefaultUriAsync().FireAndForget();
}
}
}
public KeyValuePair<string, string> SelectedLocale
{
get => _selectedLocale;
set
{
if (SetProperty(ref _selectedLocale, value))
{
UpdateCurrentLocaleAsync().FireAndForget();
}
}
}
public bool Favicon
{
get => _favicon;
set
{
if (SetProperty(ref _favicon, value))
{
UpdateFaviconAsync().FireAndForget();
}
}
}
public bool AutoTotpCopy
{
get => _autoTotpCopy;
set
{
if (SetProperty(ref _autoTotpCopy, value))
{
UpdateAutoTotpCopyAsync().FireAndForget();
}
}
}
public bool AutofillSavePrompt
{
get => _autofillSavePrompt;
set
{
if (SetProperty(ref _autofillSavePrompt, value))
{
UpdateAutofillSavePromptAsync().FireAndForget();
}
}
}
public bool ShowAndroidAutofillSettings
{
get => _showAndroidAutofillSettings;
set => SetProperty(ref _showAndroidAutofillSettings, value);
}
public ICommand GoToBlockAutofillUrisCommand { get; }
public async Task InitAsync()
{
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
var theme = await _stateService.GetThemeAsync();
ThemeSelectedIndex = ThemeOptions.FindIndex(k => k.Key == theme);
var autoDarkTheme = await _stateService.GetAutoDarkThemeAsync() ?? "dark";
AutoDarkThemeSelectedIndex = AutoDarkThemeOptions.FindIndex(k => k.Key == autoDarkTheme);
var defaultUriMatch = await _stateService.GetDefaultUriMatchAsync();
UriMatchSelectedIndex = defaultUriMatch == null ? 0 :
UriMatchOptions.FindIndex(k => (int?)k.Key == defaultUriMatch);
var clearClipboard = await _stateService.GetClearClipboardAsync();
ClearClipboardSelectedIndex = ClearClipboardOptions.FindIndex(k => k.Key == clearClipboard);
var appLocale = _stateService.GetLocale();
SelectedLocale = appLocale == null ? LocalesOptions.First() : LocalesOptions.FirstOrDefault(kv => kv.Key == appLocale);
_inited = true;
}
private async Task UpdateAutoTotpCopyAsync()
{
if (_inited)
{
// TODO: [PS-961] Fix negative function names
await _stateService.SetDisableAutoTotpCopyAsync(!AutoTotpCopy);
}
}
private async Task UpdateFaviconAsync()
{
if (_inited)
{
// TODO: [PS-961] Fix negative function names
await _stateService.SetDisableFaviconAsync(!Favicon);
}
}
private async Task SaveClipboardChangedAsync()
{
if (_inited && ClearClipboardSelectedIndex > -1)
{
await _stateService.SetClearClipboardAsync(ClearClipboardOptions[ClearClipboardSelectedIndex].Key);
}
}
private async Task SaveThemeAsync()
{
if (_inited && ThemeSelectedIndex > -1)
{
await _stateService.SetThemeAsync(ThemeOptions[ThemeSelectedIndex].Key);
await _stateService.SetAutoDarkThemeAsync(AutoDarkThemeOptions[AutoDarkThemeSelectedIndex].Key);
ThemeManager.SetTheme(Application.Current.Resources);
_messagingService.Send("updatedTheme");
}
}
private async Task SaveDefaultUriAsync()
{
if (_inited && UriMatchSelectedIndex > -1)
{
await _stateService.SetDefaultUriMatchAsync((int?)UriMatchOptions[UriMatchSelectedIndex].Key);
}
}
private async Task UpdateAutofillSavePromptAsync()
{
if (_inited)
{
// TODO: [PS-961] Fix negative function names
await _stateService.SetAutofillDisableSavePromptAsync(!AutofillSavePrompt);
}
}
private async Task UpdateCurrentLocaleAsync()
{
if (!_inited)
{
return;
}
_stateService.SetLocale(SelectedLocale.Key);
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.LanguageChangeXDescription, SelectedLocale.Value), AppResources.Language, AppResources.Ok);
}
}
}

View File

@@ -0,0 +1,72 @@
<?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"
xmlns:pages="clr-namespace:Bit.App.Pages"
x:DataType="pages:OtherSettingsPageViewModel"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:Class="Bit.App.Pages.OtherSettingsPage"
Title="{u:I18n Other}">
<ContentPage.BindingContext>
<pages:OtherSettingsPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<u:InverseBoolConverter x:Key="inverseBoolConverter" />
</ContentPage.Resources>
<ScrollView Padding="0, 0, 0, 20">
<StackLayout Padding="0" Spacing="5">
<controls:SwitchItemView
Title="{u:I18n EnableSyncOnRefresh}"
IsToggled="{Binding EnableSyncOnRefresh}"
Subtitle="{u:I18n EnableSyncOnRefreshDescription}"
AutomationId="SyncOnRefreshSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<StackLayout StyleClass="box" Margin="0,12,0,0">
<Button
Text="{u:I18n SyncNow}"
Command="{Binding SyncCommand}"
AutomationId="SyncNowButton"></Button>
<Label
Text="{Binding LastSyncDisplay}"
StyleClass="text-muted, text-sm"
HorizontalTextAlignment="Start"
Margin="0,10"
AutomationId="LastSyncLabel" />
</StackLayout>
<controls:SettingChooserItemView
Title="{u:I18n ClearClipboard}"
Subtitle="{u:I18n ClearClipboardDescription}"
DisplayValue="{Binding ClearClipboardPickerViewModel.SelectedValue}"
ChooseCommand="{Binding ClearClipboardPickerViewModel.SelectOptionCommand}"
AutomationId="ClearClipboardChooser"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand"/>
<controls:SwitchItemView
Title="{u:I18n AllowScreenCapture}"
IsToggled="{Binding IsScreenCaptureAllowed}"
IsEnabled="{Binding CanToggleeScreenCaptureAllowed}"
AutomationId="AllowScreenCaptureSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:SwitchItemView
Title="{u:I18n ConnectToWatch}"
IsToggled="{Binding ShouldConnectToWatch}"
IsEnabled="{Binding CanToggleShouldConnectToWatch}"
IsVisible="{OnPlatform iOS=True, Android=False}"
AutomationId="ConnectToWatchSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View File

@@ -0,0 +1,44 @@
using System;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
namespace Bit.App.Pages
{
public partial class OtherSettingsPage : BaseContentPage
{
private OtherSettingsPageViewModel _vm;
public OtherSettingsPage()
{
InitializeComponent();
_vm = BindingContext as OtherSettingsPageViewModel;
_vm.Page = this;
}
protected async override void OnAppearing()
{
base.OnAppearing();
try
{
_vm.SubscribeEvents();
await _vm.InitAsync();
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
Navigation.PopAsync().FireAndForget();
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_vm.UnsubscribeEvents();
}
}
}

View File

@@ -0,0 +1,245 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class OtherSettingsPageViewModel : BaseViewModel
{
private const int CLEAR_CLIPBOARD_NEVER_OPTION = -1;
private readonly IDeviceActionService _deviceActionService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IStateService _stateService;
private readonly ISyncService _syncService;
private readonly ILocalizeService _localizeService;
private readonly IWatchDeviceService _watchDeviceService;
private readonly ILogger _logger;
private string _lastSyncDisplay = "--";
private bool _inited;
private bool _syncOnRefresh;
private bool _isScreenCaptureAllowed;
private bool _shouldConnectToWatch;
public OtherSettingsPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_stateService = ServiceContainer.Resolve<IStateService>();
_syncService = ServiceContainer.Resolve<ISyncService>();
_localizeService = ServiceContainer.Resolve<ILocalizeService>();
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
_logger = ServiceContainer.Resolve<ILogger>();
SyncCommand = CreateDefaultAsyncCommnad(SyncAsync, _ => _inited);
ToggleIsScreenCaptureAllowedCommand = CreateDefaultAsyncCommnad(ToggleIsScreenCaptureAllowedAsync, _ => _inited);
ToggleShouldConnectToWatchCommand = CreateDefaultAsyncCommnad(ToggleShouldConnectToWatchAsync, _ => _inited);
ClearClipboardPickerViewModel = new PickerViewModel<int>(
_deviceActionService,
_logger,
OnClearClipboardChangingAsync,
AppResources.ClearClipboard,
_ => _inited,
ex => HandleException(ex));
}
public bool EnableSyncOnRefresh
{
get => _syncOnRefresh;
set
{
if (SetProperty(ref _syncOnRefresh, value))
{
UpdateSyncOnRefreshAsync().FireAndForget();
}
}
}
public string LastSyncDisplay
{
get => $"{AppResources.LastSync} {_lastSyncDisplay}";
set => SetProperty(ref _lastSyncDisplay, value);
}
public PickerViewModel<int> ClearClipboardPickerViewModel { get; }
public bool IsScreenCaptureAllowed
{
get => _isScreenCaptureAllowed;
set
{
if (SetProperty(ref _isScreenCaptureAllowed, value))
{
((ICommand)ToggleIsScreenCaptureAllowedCommand).Execute(null);
}
}
}
public bool CanToggleeScreenCaptureAllowed => ToggleIsScreenCaptureAllowedCommand.CanExecute(null);
public bool ShouldConnectToWatch
{
get => _shouldConnectToWatch;
set
{
if (SetProperty(ref _shouldConnectToWatch, value))
{
((ICommand)ToggleShouldConnectToWatchCommand).Execute(null);
}
}
}
public bool CanToggleShouldConnectToWatch => ToggleShouldConnectToWatchCommand.CanExecute(null);
public AsyncCommand SyncCommand { get; }
public AsyncCommand ToggleIsScreenCaptureAllowedCommand { get; }
public AsyncCommand ToggleShouldConnectToWatchCommand { get; }
public async Task InitAsync()
{
await SetLastSyncAsync();
EnableSyncOnRefresh = await _stateService.GetSyncOnRefreshAsync();
await InitClearClipboardAsync();
_isScreenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
_shouldConnectToWatch = await _stateService.GetShouldConnectToWatchAsync();
_inited = true;
MainThread.BeginInvokeOnMainThread(() =>
{
TriggerPropertyChanged(nameof(IsScreenCaptureAllowed));
TriggerPropertyChanged(nameof(ShouldConnectToWatch));
SyncCommand.RaiseCanExecuteChanged();
ClearClipboardPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
ToggleIsScreenCaptureAllowedCommand.RaiseCanExecuteChanged();
ToggleShouldConnectToWatchCommand.RaiseCanExecuteChanged();
});
}
private async Task InitClearClipboardAsync()
{
var clearClipboardOptions = new Dictionary<int, string>
{
[CLEAR_CLIPBOARD_NEVER_OPTION] = AppResources.Never,
[10] = AppResources.TenSeconds,
[20] = AppResources.TwentySeconds,
[30] = AppResources.ThirtySeconds,
[60] = AppResources.OneMinute
};
if (Device.RuntimePlatform != Device.iOS)
{
clearClipboardOptions.Add(120, AppResources.TwoMinutes);
clearClipboardOptions.Add(300, AppResources.FiveMinutes);
}
var clearClipboard = await _stateService.GetClearClipboardAsync() ?? CLEAR_CLIPBOARD_NEVER_OPTION;
ClearClipboardPickerViewModel.Init(clearClipboardOptions, clearClipboard, CLEAR_CLIPBOARD_NEVER_OPTION);
}
public async Task UpdateSyncOnRefreshAsync()
{
if (_inited)
{
await _stateService.SetSyncOnRefreshAsync(_syncOnRefresh);
}
}
public async Task SetLastSyncAsync()
{
var last = await _syncService.GetLastSyncAsync();
if (last is null)
{
LastSyncDisplay = AppResources.Never;
return;
}
var localDate = last.Value.ToLocalTime();
LastSyncDisplay = string.Format("{0} {1}",
_localizeService.GetLocaleShortDate(localDate),
_localizeService.GetLocaleShortTime(localDate));
}
public async Task SyncAsync()
{
if (!await HasConnectivityAsync())
{
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
await _syncService.SyncPasswordlessLoginRequestsAsync();
var success = await _syncService.FullSyncAsync(true);
await _deviceActionService.HideLoadingAsync();
if (!success)
{
await Page.DisplayAlert(null, AppResources.SyncingFailed, AppResources.Ok);
return;
}
await SetLastSyncAsync();
_platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete);
}
private async Task<bool> OnClearClipboardChangingAsync(int optionKey)
{
await _stateService.SetClearClipboardAsync(optionKey == CLEAR_CLIPBOARD_NEVER_OPTION ? (int?)null : optionKey);
return true;
}
private async Task ToggleIsScreenCaptureAllowedAsync()
{
if (IsScreenCaptureAllowed
&&
!await Page.DisplayAlert(AppResources.AllowScreenCapture, AppResources.AreYouSureYouWantToEnableScreenCapture, AppResources.Yes, AppResources.No))
{
_isScreenCaptureAllowed = !IsScreenCaptureAllowed;
TriggerPropertyChanged(nameof(IsScreenCaptureAllowed));
return;
}
await _stateService.SetScreenCaptureAllowedAsync(IsScreenCaptureAllowed);
await _deviceActionService.SetScreenCaptureAllowedAsync();
}
private async Task ToggleShouldConnectToWatchAsync()
{
await _watchDeviceService.SetShouldConnectToWatchAsync(ShouldConnectToWatch);
}
private void ToggleIsScreenCaptureAllowedCommand_CanExecuteChanged(object sender, EventArgs e)
{
TriggerPropertyChanged(nameof(CanToggleeScreenCaptureAllowed));
}
private void ToggleShouldConnectToWatchCommand_CanExecuteChanged(object sender, EventArgs e)
{
TriggerPropertyChanged(nameof(CanToggleShouldConnectToWatch));
}
internal void SubscribeEvents()
{
ToggleIsScreenCaptureAllowedCommand.CanExecuteChanged += ToggleIsScreenCaptureAllowedCommand_CanExecuteChanged;
ToggleShouldConnectToWatchCommand.CanExecuteChanged += ToggleShouldConnectToWatchCommand_CanExecuteChanged;
}
internal void UnsubscribeEvents()
{
ToggleIsScreenCaptureAllowedCommand.CanExecuteChanged -= ToggleIsScreenCaptureAllowedCommand_CanExecuteChanged;
ToggleShouldConnectToWatchCommand.CanExecuteChanged -= ToggleShouldConnectToWatchCommand_CanExecuteChanged;
}
}
}

View File

@@ -0,0 +1,178 @@
<?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"
xmlns:pages="clr-namespace:Bit.App.Pages"
x:DataType="pages:SecuritySettingsPageViewModel"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:Class="Bit.App.Pages.SecuritySettingsPage"
Title="{u:I18n AccountSecurity}">
<ContentPage.BindingContext>
<pages:SecuritySettingsPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<u:IsNotNullConverter x:Key="isNotNullConverter" />
</ContentPage.Resources>
<ScrollView Padding="0, 0, 0, 20">
<StackLayout Padding="0" Spacing="5">
<controls:CustomLabel
Text="{u:I18n ApproveLoginRequests}"
StyleClass="settings-header" />
<controls:SwitchItemView
Title="{u:I18n UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices}"
IsToggled="{Binding UseThisDeviceToApproveLoginRequests}"
AutomationId="ApproveLoginRequestsMadeFromOtherDevicesSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:CustomLabel
Text="{u:I18n PendingLogInRequests}"
StyleClass="settings-navigatable-label"
IsVisible="{Binding UseThisDeviceToApproveLoginRequests}"
AutomationId="PendingLogInRequestsLabel">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding GoToPendingLogInRequestsCommand}" />
</Label.GestureRecognizers>
</controls:CustomLabel>
<BoxView StyleClass="settings-box-row-separator" />
<controls:CustomLabel
Text="{u:I18n UnlockOptions}"
StyleClass="settings-header" />
<controls:SwitchItemView
Title="{Binding UnlockWithBiometricsTitle}"
IsToggled="{Binding CanUnlockWithBiometrics}"
IsVisible="{Binding UnlockWithBiometricsTitle, Converter={StaticResource isNotNullConverter}}"
AutomationId="CanUnlockWithBiometricsSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:SwitchItemView
Title="{u:I18n UnlockWithPIN}"
IsToggled="{Binding CanUnlockWithPin}"
AutomationId="CanUnlockWithPinSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="settings-box-row-separator" />
<controls:CustomLabel
Text="{u:I18n SessionTimeout}"
StyleClass="settings-header" />
<Frame
IsVisible="{Binding ShowVaultTimeoutPolicyInfo}"
Padding="10"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="{DynamicResource PrimaryColor}"
AutomationId="VaultTimeoutPolicyLabel"
Margin="16,5">
<Label
Text="{Binding VaultTimeoutPolicyDescription}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
<controls:SettingChooserItemView
Title="{u:I18n SessionTimeout}"
DisplayValue="{Binding VaultTimeoutPickerViewModel.SelectedValue}"
ChooseCommand="{Binding VaultTimeoutPickerViewModel.SelectOptionCommand}"
AutomationId="VaultTimeoutChooser"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand"/>
<controls:BaseSettingItemView
Title="{u:I18n Custom}"
IsVisible="{Binding ShowCustomVaultTimeoutPicker}"
AutomationId="CustomVaultTimeoutChooser"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand"
ControlTemplate="{StaticResource SettingControlTemplate}">
<TimePicker Time="{Binding CustomVaultTimeoutTime}" Format="HH:mm"
FontSize="Small"
TextColor="{DynamicResource MutedColor}"
StyleClass="list-sub" Margin="-5"
HorizontalOptions="End"
ios:TimePicker.UpdateMode="WhenFinished"
AutomationId="SettingCustomVaultTimeoutPicker"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding CustomVaultTimeoutTimeVerbalized}"/>
</controls:BaseSettingItemView>
<controls:SettingChooserItemView
Title="{u:I18n SessionTimeoutAction}"
DisplayValue="{Binding VaultTimeoutActionPickerViewModel.SelectedValue}"
ChooseCommand="{Binding VaultTimeoutActionPickerViewModel.SelectOptionCommand}"
AutomationId="VaultTimeoutActionChooser"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand"/>
<BoxView StyleClass="settings-box-row-separator" />
<controls:CustomLabel
Text="{u:I18n Other}"
StyleClass="settings-header" />
<controls:CustomLabel
Text="{u:I18n AccountFingerprintPhrase}"
StyleClass="settings-navigatable-label"
AutomationId="AccountFingerprintPhraseLabel">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ShowAccountFingerprintPhraseCommand}" />
</Label.GestureRecognizers>
</controls:CustomLabel>
<controls:ExternalLinkItemView
Title="{u:I18n TwoStepLogin}"
GoToLinkCommand="{Binding GoToTwoStepLoginCommand}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand"
AutomationId="TwoStepLoginLinkItemView" />
<controls:ExternalLinkItemView
Title="{u:I18n ChangeMasterPassword}"
GoToLinkCommand="{Binding GoToChangeMasterPasswordCommand}"
IsVisible="{Binding ShowChangeMasterPassword}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand"
AutomationId="ChangeMasterPasswordLinkItemView" />
<controls:CustomLabel
Text="{u:I18n LockNow}"
StyleClass="settings-navigatable-label"
AutomationId="LockNowLabel">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding LockCommand}" />
</Label.GestureRecognizers>
</controls:CustomLabel>
<controls:CustomLabel
Text="{u:I18n LogOut}"
StyleClass="settings-navigatable-label"
AutomationId="LogOutLabel">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding LogOutCommand}" />
</Label.GestureRecognizers>
</controls:CustomLabel>
<controls:CustomLabel
Text="{u:I18n DeleteAccount}"
StyleClass="settings-navigatable-label"
AutomationId="DeleteAccountLabel">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding DeleteAccountCommand}" />
</Label.GestureRecognizers>
</controls:CustomLabel>
</StackLayout>
</ScrollView>
</pages:BaseContentPage>

View File

@@ -0,0 +1,37 @@
using System;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
namespace Bit.App.Pages
{
public partial class SecuritySettingsPage : BaseContentPage
{
private SecuritySettingsPageViewModel _vm;
public SecuritySettingsPage()
{
InitializeComponent();
_vm = BindingContext as SecuritySettingsPageViewModel;
_vm.Page = this;
}
protected async override void OnAppearing()
{
base.OnAppearing();
try
{
await _vm.InitAsync();
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
Navigation.PopAsync().FireAndForget();
}
}
}
}

View File

@@ -0,0 +1,547 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Pages.Accounts;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class SecuritySettingsPageViewModel : BaseViewModel
{
private const int NEVER_SESSION_TIMEOUT_VALUE = -2;
private const int CUSTOM_VAULT_TIMEOUT_VALUE = -100;
private readonly IStateService _stateService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IDeviceActionService _deviceActionService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IBiometricService _biometricsService;
private readonly IUserPinService _userPinService;
private readonly ICryptoService _cryptoService;
private readonly IUserVerificationService _userVerificationService;
private readonly IPolicyService _policyService;
private readonly IMessagingService _messagingService;
private readonly IEnvironmentService _environmentService;
private readonly ILogger _logger;
private bool _inited;
private bool _useThisDeviceToApproveLoginRequests;
private bool _supportsBiometric, _canUnlockWithBiometrics;
private bool _canUnlockWithPin;
private bool _hasMasterPassword;
private int? _maximumVaultTimeoutPolicy;
private string _vaultTimeoutActionPolicy;
private TimeSpan? _customVaultTimeoutTime;
public SecuritySettingsPageViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>();
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
_biometricsService = ServiceContainer.Resolve<IBiometricService>();
_userPinService = ServiceContainer.Resolve<IUserPinService>();
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
_policyService = ServiceContainer.Resolve<IPolicyService>();
_messagingService = ServiceContainer.Resolve<IMessagingService>();
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
_logger = ServiceContainer.Resolve<ILogger>();
VaultTimeoutPickerViewModel = new PickerViewModel<int>(
_deviceActionService,
_logger,
OnVaultTimeoutChangingAsync,
AppResources.SessionTimeout,
_ => _inited,
ex => HandleException(ex));
VaultTimeoutPickerViewModel.SetAfterSelectionChanged(_ => MainThread.InvokeOnMainThreadAsync(TriggerUpdateCustomVaultTimeoutPicker));
VaultTimeoutActionPickerViewModel = new PickerViewModel<VaultTimeoutAction>(
_deviceActionService,
_logger,
OnVaultTimeoutActionChangingAsync,
AppResources.SessionTimeoutAction,
_ => _inited && !HasVaultTimeoutActionPolicy,
ex => HandleException(ex));
ToggleUseThisDeviceToApproveLoginRequestsCommand = CreateDefaultAsyncCommnad(ToggleUseThisDeviceToApproveLoginRequestsAsync, _ => _inited);
GoToPendingLogInRequestsCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new LoginPasswordlessRequestsListPage())));
ToggleCanUnlockWithBiometricsCommand = CreateDefaultAsyncCommnad(ToggleCanUnlockWithBiometricsAsync, _ => _inited);
ToggleCanUnlockWithPinCommand = CreateDefaultAsyncCommnad(ToggleCanUnlockWithPinAsync, _ => _inited);
ShowAccountFingerprintPhraseCommand = CreateDefaultAsyncCommnad(ShowAccountFingerprintPhraseAsync);
GoToTwoStepLoginCommand = CreateDefaultAsyncCommnad(() => GoToWebVaultSettingsAsync(AppResources.TwoStepLoginDescriptionLong, AppResources.ContinueToWebApp));
GoToChangeMasterPasswordCommand = CreateDefaultAsyncCommnad(() => GoToWebVaultSettingsAsync(AppResources.ChangeMasterPasswordDescriptionLong, AppResources.ContinueToWebApp));
LockCommand = CreateDefaultAsyncCommnad(() => _vaultTimeoutService.LockAsync(true, true));
LogOutCommand = CreateDefaultAsyncCommnad(LogOutAsync);
DeleteAccountCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage())));
}
public bool UseThisDeviceToApproveLoginRequests
{
get => _useThisDeviceToApproveLoginRequests;
set
{
if (SetProperty(ref _useThisDeviceToApproveLoginRequests, value))
{
((ICommand)ToggleUseThisDeviceToApproveLoginRequestsCommand).Execute(null);
}
}
}
public string UnlockWithBiometricsTitle
{
get
{
if (!_supportsBiometric)
{
return null;
}
var biometricName = AppResources.Biometrics;
if (Device.RuntimePlatform == Device.iOS)
{
biometricName = _deviceActionService.SupportsFaceBiometric()
? AppResources.FaceID
: AppResources.TouchID;
}
return string.Format(AppResources.UnlockWith, biometricName);
}
}
public bool CanUnlockWithBiometrics
{
get => _canUnlockWithBiometrics;
set
{
if (SetProperty(ref _canUnlockWithBiometrics, value))
{
((ICommand)ToggleCanUnlockWithBiometricsCommand).Execute(null);
}
}
}
public bool CanUnlockWithPin
{
get => _canUnlockWithPin;
set
{
if (SetProperty(ref _canUnlockWithPin, value))
{
((ICommand)ToggleCanUnlockWithPinCommand).Execute(null);
}
}
}
public TimeSpan? CustomVaultTimeoutTime
{
get => _customVaultTimeoutTime;
set
{
var oldValue = _customVaultTimeoutTime;
if (SetProperty(ref _customVaultTimeoutTime, value, additionalPropertyNames: new string[] { nameof(CustomVaultTimeoutTimeVerbalized) }) && value.HasValue)
{
UpdateVaultTimeoutAsync((int)value.Value.TotalMinutes)
.FireAndForget(ex =>
{
HandleException(ex);
MainThread.BeginInvokeOnMainThread(() => SetProperty(ref _customVaultTimeoutTime, oldValue));
});
}
}
}
public string CustomVaultTimeoutTimeVerbalized => CustomVaultTimeoutTime?.Verbalize(A11yExtensions.TimeSpanVerbalizationMode.HoursAndMinutes);
public bool ShowCustomVaultTimeoutPicker => VaultTimeoutPickerViewModel.SelectedKey == CUSTOM_VAULT_TIMEOUT_VALUE;
public bool ShowVaultTimeoutPolicyInfo => _maximumVaultTimeoutPolicy.HasValue || HasVaultTimeoutActionPolicy;
public string VaultTimeoutPolicyDescription
{
get
{
if (!ShowVaultTimeoutPolicyInfo)
{
return null;
}
static string LocalizeTimeoutAction(string actionPolicy)
{
return actionPolicy == Policy.ACTION_LOCK ? AppResources.Lock : AppResources.LogOut;
};
if (!_maximumVaultTimeoutPolicy.HasValue)
{
return string.Format(AppResources.VaultTimeoutActionPolicyInEffect, LocalizeTimeoutAction(_vaultTimeoutActionPolicy));
}
var hours = Math.Floor((float)_maximumVaultTimeoutPolicy / 60);
var minutes = _maximumVaultTimeoutPolicy % 60;
return string.IsNullOrWhiteSpace(_vaultTimeoutActionPolicy)
? string.Format(AppResources.VaultTimeoutPolicyInEffect, hours, minutes)
: string.Format(AppResources.VaultTimeoutPolicyWithActionInEffect, hours, minutes, LocalizeTimeoutAction(_vaultTimeoutActionPolicy));
}
}
public bool ShowChangeMasterPassword { get; private set; }
private bool IsVaultTimeoutActionLockAllowed => _hasMasterPassword || _canUnlockWithBiometrics || _canUnlockWithPin;
private int? CurrentVaultTimeout => GetRawVaultTimeoutFrom(VaultTimeoutPickerViewModel.SelectedKey);
private bool IncludeLinksWithSubscriptionInfo => Device.RuntimePlatform != Device.iOS;
private bool HasVaultTimeoutActionPolicy => !string.IsNullOrEmpty(_vaultTimeoutActionPolicy);
public PickerViewModel<int> VaultTimeoutPickerViewModel { get; }
public PickerViewModel<VaultTimeoutAction> VaultTimeoutActionPickerViewModel { get; }
public AsyncCommand ToggleUseThisDeviceToApproveLoginRequestsCommand { get; }
public ICommand GoToPendingLogInRequestsCommand { get; }
public AsyncCommand ToggleCanUnlockWithBiometricsCommand { get; }
public AsyncCommand ToggleCanUnlockWithPinCommand { get; }
public ICommand ShowAccountFingerprintPhraseCommand { get; }
public ICommand GoToTwoStepLoginCommand { get; }
public ICommand GoToChangeMasterPasswordCommand { get; }
public ICommand LockCommand { get; }
public ICommand LogOutCommand { get; }
public ICommand DeleteAccountCommand { get; }
public async Task InitAsync()
{
var decryptionOptions = await _stateService.GetAccountDecryptionOptions();
// set default true for backwards compatibility
_hasMasterPassword = decryptionOptions?.HasMasterPassword ?? true;
_useThisDeviceToApproveLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
_canUnlockWithBiometrics = await _vaultTimeoutService.IsBiometricLockSetAsync();
_canUnlockWithPin = await _vaultTimeoutService.GetPinLockTypeAsync() != Core.Services.PinLockType.Disabled;
await LoadPoliciesAsync();
await InitVaultTimeoutPickerAsync();
await InitVaultTimeoutActionPickerAsync();
ShowChangeMasterPassword = IncludeLinksWithSubscriptionInfo && await _userVerificationService.HasMasterPasswordAsync();
_inited = true;
MainThread.BeginInvokeOnMainThread(() =>
{
TriggerPropertyChanged(nameof(UseThisDeviceToApproveLoginRequests));
TriggerPropertyChanged(nameof(UnlockWithBiometricsTitle));
TriggerPropertyChanged(nameof(CanUnlockWithBiometrics));
TriggerPropertyChanged(nameof(CanUnlockWithPin));
TriggerPropertyChanged(nameof(ShowVaultTimeoutPolicyInfo));
TriggerPropertyChanged(nameof(VaultTimeoutPolicyDescription));
TriggerPropertyChanged(nameof(ShowChangeMasterPassword));
TriggerUpdateCustomVaultTimeoutPicker();
ToggleUseThisDeviceToApproveLoginRequestsCommand.RaiseCanExecuteChanged();
ToggleCanUnlockWithBiometricsCommand.RaiseCanExecuteChanged();
ToggleCanUnlockWithPinCommand.RaiseCanExecuteChanged();
VaultTimeoutPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
VaultTimeoutActionPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
});
}
private async Task LoadPoliciesAsync()
{
if (!await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout))
{
return;
}
var maximumVaultTimeoutPolicy = await _policyService.FirstOrDefault(PolicyType.MaximumVaultTimeout);
_maximumVaultTimeoutPolicy = maximumVaultTimeoutPolicy?.GetInt(Policy.MINUTES_KEY);
_vaultTimeoutActionPolicy = maximumVaultTimeoutPolicy?.GetString(Policy.ACTION_KEY);
MainThread.BeginInvokeOnMainThread(VaultTimeoutActionPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged);
}
private async Task InitVaultTimeoutPickerAsync()
{
var options = new Dictionary<int, string>
{
[0] = AppResources.Immediately,
[1] = AppResources.OneMinute,
[5] = AppResources.FiveMinutes,
[15] = AppResources.FifteenMinutes,
[30] = AppResources.ThirtyMinutes,
[60] = AppResources.OneHour,
[240] = AppResources.FourHours,
[-1] = AppResources.OnRestart,
[NEVER_SESSION_TIMEOUT_VALUE] = AppResources.Never
};
if (_maximumVaultTimeoutPolicy.HasValue)
{
options = options.Where(t => t.Key >= 0 && t.Key <= _maximumVaultTimeoutPolicy.Value)
.ToDictionary(v => v.Key, v => v.Value);
}
options.Add(CUSTOM_VAULT_TIMEOUT_VALUE, AppResources.Custom);
var vaultTimeout = await _vaultTimeoutService.GetVaultTimeout() ?? NEVER_SESSION_TIMEOUT_VALUE;
VaultTimeoutPickerViewModel.Init(options, vaultTimeout, CUSTOM_VAULT_TIMEOUT_VALUE, false);
if (VaultTimeoutPickerViewModel.SelectedKey == CUSTOM_VAULT_TIMEOUT_VALUE)
{
_customVaultTimeoutTime = TimeSpan.FromMinutes(vaultTimeout);
}
}
private async Task InitVaultTimeoutActionPickerAsync()
{
var options = new Dictionary<VaultTimeoutAction, string>();
if (IsVaultTimeoutActionLockAllowed)
{
options.Add(VaultTimeoutAction.Lock, AppResources.Lock);
}
options.Add(VaultTimeoutAction.Logout, AppResources.LogOut);
var timeoutAction = await _vaultTimeoutService.GetVaultTimeoutAction() ?? VaultTimeoutAction.Lock;
if (!IsVaultTimeoutActionLockAllowed && timeoutAction == VaultTimeoutAction.Lock)
{
timeoutAction = VaultTimeoutAction.Logout;
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, VaultTimeoutAction.Logout);
}
VaultTimeoutActionPickerViewModel.Init(options, timeoutAction, IsVaultTimeoutActionLockAllowed ? VaultTimeoutAction.Lock : VaultTimeoutAction.Logout);
}
private async Task ToggleUseThisDeviceToApproveLoginRequestsAsync()
{
if (UseThisDeviceToApproveLoginRequests
&&
!await Page.DisplayAlert(AppResources.ApproveLoginRequests, AppResources.UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices, AppResources.Yes, AppResources.No))
{
_useThisDeviceToApproveLoginRequests = !UseThisDeviceToApproveLoginRequests;
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(UseThisDeviceToApproveLoginRequests)));
return;
}
await _stateService.SetApprovePasswordlessLoginsAsync(UseThisDeviceToApproveLoginRequests);
if (!UseThisDeviceToApproveLoginRequests || await _pushNotificationService.AreNotificationsSettingsEnabledAsync())
{
return;
}
var openAppSettingsResult = await _platformUtilsService.ShowDialogAsync(
AppResources.ReceivePushNotificationsForNewLoginRequests,
string.Empty,
AppResources.Settings,
AppResources.NoThanks
);
if (openAppSettingsResult)
{
_deviceActionService.OpenAppSettings();
}
}
private async Task ToggleCanUnlockWithBiometricsAsync()
{
if (!_canUnlockWithBiometrics)
{
await UpdateVaultTimeoutActionIfNeededAsync();
await _biometricsService.SetCanUnlockWithBiometricsAsync(CanUnlockWithBiometrics);
return;
}
if (!_supportsBiometric
||
!await _platformUtilsService.AuthenticateBiometricAsync(null, Device.RuntimePlatform == Device.Android ? "." : null))
{
_canUnlockWithBiometrics = false;
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));
return;
}
await _biometricsService.SetCanUnlockWithBiometricsAsync(CanUnlockWithBiometrics);
}
public async Task ToggleCanUnlockWithPinAsync()
{
if (!CanUnlockWithPin)
{
await _vaultTimeoutService.ClearAsync();
await UpdateVaultTimeoutActionIfNeededAsync();
return;
}
var newPin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN,
AppResources.SetPINDescription, null, AppResources.Submit, AppResources.Cancel, true);
if (string.IsNullOrWhiteSpace(newPin))
{
_canUnlockWithPin = false;
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithPin)));
return;
}
var requireMasterPasswordOnRestart = await _userVerificationService.HasMasterPasswordAsync()
&&
await _platformUtilsService.ShowDialogAsync(AppResources.PINRequireMasterPasswordRestart,
AppResources.UnlockWithPIN,
AppResources.Yes,
AppResources.No);
await _userPinService.SetupPinAsync(newPin, requireMasterPasswordOnRestart);
}
private async Task UpdateVaultTimeoutActionIfNeededAsync()
{
if (IsVaultTimeoutActionLockAllowed)
{
return;
}
VaultTimeoutActionPickerViewModel.Select(VaultTimeoutAction.Logout);
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, VaultTimeoutAction.Logout);
_deviceActionService.Toast(AppResources.VaultTimeoutActionChangedToLogOut);
}
private async Task<bool> OnVaultTimeoutChangingAsync(int newTimeout)
{
if (newTimeout == NEVER_SESSION_TIMEOUT_VALUE
&&
!await _platformUtilsService.ShowDialogAsync(AppResources.NeverLockWarning, AppResources.Warning, AppResources.Yes, AppResources.Cancel))
{
return false;
}
if (newTimeout == CUSTOM_VAULT_TIMEOUT_VALUE)
{
_customVaultTimeoutTime = TimeSpan.FromMinutes(0);
}
return await UpdateVaultTimeoutAsync(newTimeout);
}
private async Task<bool> UpdateVaultTimeoutAsync(int newTimeout)
{
var rawTimeout = GetRawVaultTimeoutFrom(newTimeout);
if (rawTimeout > _maximumVaultTimeoutPolicy)
{
await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutToLarge, AppResources.Warning);
VaultTimeoutPickerViewModel.Select(_maximumVaultTimeoutPolicy.Value, false);
if (VaultTimeoutPickerViewModel.SelectedKey == CUSTOM_VAULT_TIMEOUT_VALUE)
{
_customVaultTimeoutTime = TimeSpan.FromMinutes(_maximumVaultTimeoutPolicy.Value);
}
MainThread.BeginInvokeOnMainThread(TriggerUpdateCustomVaultTimeoutPicker);
return false;
}
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(rawTimeout, VaultTimeoutActionPickerViewModel.SelectedKey);
await _cryptoService.RefreshKeysAsync();
return true;
}
private void TriggerUpdateCustomVaultTimeoutPicker()
{
TriggerPropertyChanged(nameof(ShowCustomVaultTimeoutPicker));
TriggerPropertyChanged(nameof(CustomVaultTimeoutTime));
}
private int? GetRawVaultTimeoutFrom(int vaultTimeoutPickerKey)
{
if (vaultTimeoutPickerKey == NEVER_SESSION_TIMEOUT_VALUE)
{
return null;
}
if (vaultTimeoutPickerKey == CUSTOM_VAULT_TIMEOUT_VALUE
&&
CustomVaultTimeoutTime.HasValue)
{
return (int)CustomVaultTimeoutTime.Value.TotalMinutes;
}
return vaultTimeoutPickerKey;
}
private async Task<bool> OnVaultTimeoutActionChangingAsync(VaultTimeoutAction timeoutActionKey)
{
if (!string.IsNullOrEmpty(_vaultTimeoutActionPolicy))
{
// do nothing if we have a policy set
return false;
}
if (timeoutActionKey == VaultTimeoutAction.Logout
&&
!await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutLogOutConfirmation, AppResources.Warning, AppResources.Yes, AppResources.Cancel))
{
return false;
}
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, timeoutActionKey);
_messagingService.Send(AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND);
return true;
}
private async Task ShowAccountFingerprintPhraseAsync()
{
List<string> fingerprint;
try
{
fingerprint = await _cryptoService.GetFingerprintAsync(await _stateService.GetActiveUserIdAsync());
}
catch (Exception e) when (e.Message == "No public key available.")
{
return;
}
var phrase = string.Join("-", fingerprint);
var text = $"{AppResources.YourAccountsFingerprint}:\n\n{phrase}";
var learnMore = await _platformUtilsService.ShowDialogAsync(text, AppResources.FingerprintPhrase,
AppResources.LearnMore, AppResources.Close);
if (learnMore)
{
_platformUtilsService.LaunchUri(ExternalLinksConstants.HELP_FINGERPRINT_PHRASE);
}
}
private async Task GoToWebVaultSettingsAsync(string dialogText, string dialogTitle)
{
if (await _platformUtilsService.ShowDialogAsync(dialogText, dialogTitle, AppResources.Continue, AppResources.Cancel))
{
_platformUtilsService.LaunchUri(string.Format(ExternalLinksConstants.WEB_VAULT_SETTINGS_FORMAT, _environmentService.GetWebVaultUrl()));
}
}
public async Task LogOutAsync()
{
if (await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation, AppResources.LogOut, AppResources.Yes, AppResources.Cancel))
{
_messagingService.Send(AccountsManagerMessageCommands.LOGOUT);
}
}
}
}

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