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

Compare commits

..

68 Commits

Author SHA1 Message Date
github-actions[bot]
ee27900c3a Bump version to 2022.9.1 (#2069)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(cherry picked from commit 3f8e00985c)
2022-09-14 09:55:00 -06:00
André Bispo
0723227eca [SG-659] Fixed TOTP not showing for free user with classic plans. (#2071) 2022-09-14 15:57:24 +01:00
github-actions[bot]
099d7e22e2 Bump version to 2022.9.0 (#2068)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(cherry picked from commit 533928a4f1)
2022-09-12 11:00:03 -06:00
André Filipe da Silva Bispo
c86d4e7984 [SG-598] Removed space from copied totp code (#2046)
Removed space from copied totp code

(cherry picked from commit ecd4da08ee)
2022-08-23 12:41:08 -04:00
André Filipe da Silva Bispo
6164106c84 [SG-599] Cannot read authenticator key if you don't include URI before TOTP Secret. (#2047)
Removed unnecessary code when adding a TOTP auth key secret manually

(cherry picked from commit 9163b9e4de)
2022-08-23 12:40:56 -04:00
github-actions[bot]
525288d804 Autosync the updated translations (#2042)
Co-authored-by: github-actions <>
2022-08-19 12:51:41 +02:00
André Filipe da Silva Bispo
e829279e29 [SG-416] Updates to Bitwarden Authenticator (Feature Branch) (#2041)
* Initial commit of new TOTP page

* Revert config files from previous commit

This reverts commit b02c58e362.

* clear extra code and fix build

* add tab page

* add authentication view cell

* add toolbar icons

* got the countdown working

* enable context loading and vm init

* PS-70 Added toggle to quickly filter TOTP cypher items and show their details, Added new text resource

* PS-70 removed old authentication tab

* removed unnecessary code on vm

* fixed formatting

* PS-70 Added circular progress to the OTP count down

* PS-70 Fixed grid cell width. Added red progress at 20 percent. Refactored circular progress view.

* PS-70 Added new props to custom control.

* PS-70 show toggle only if it's premium

* PS-70 removed unnecessary code

* PS-70 add copy to clipboard.

* PS-70 show upgrade to premium text on details to free user.

* PS-70 added text labels to resource files

* PS-70 Renamed TOTP to Totp to have consistency in naming. Removed a11y text of switch because android was overlapping text.

* PS-70 added new UI to enter code manually in the QR Code scanner screen. Changed existing labels on scanner screen.

* PS-70 Splited totp code to adjust spacing.

* PS-70 Added scanner square corner overlay. Added scanning animation. Added scan success animation.

* PS-70 let zxing scanner camera feed on until screen is closed.

* PS-70 fixed scanner animation for android devices

* PS-70 added vibrate permission to manifest. refactored scanpage code. added manual authentication key feature in scanner.

* PS-70 fixed totp cell title label font

* PS-70 added copy button to totp edit cipher. Added row button when totp is null.

* PS-70 changed labels on manual scanner screen

* PS-70  Added label on top of button to solve UI bug.

* PS-70 Fixed android button overlapping bug by  adding button styling to a Frame view and placing a label inside. Fixed Color on scanner page.

* PS-70 Added frame styling for iOS, since frame view has different base configuration for android and iOS.

* PS-70 fixed font clipping bug on iOS

* PS-70 removed shadow for newer versions of android

* PS-70 code format

* PS-70 removed update to premium uri launch

* PS-70 PR fix for AppResource vs code behind generation.

* PS-70 changed premium required label. fixed bug when to show premium required label.

* [SSG-416] Removed the dashes from free user and just left the Premium subscription required.

* [SSG-416] removed unnecessary changes to the TabsPage file

* [SSG-416] removed unnecessary using.

* [SSG-416] Updated ViewPageViewModel and code refactoring.

* [SG-416] Updated scanner mode toggle text color to be inline with figma designs

* [SSG-416] Mobile PR Fixes

* [SSG-416] Add to remove a11y text from totp toggle because on android it places an helper text next to the switch making it invisible. Also removed from the label because it already reads the text from the label

* [SSG-416] run dotnet tool run dotnet-format

* [SSG-416] PR fixes

* Revert "[SSG-416] PR fixes"

This reverts commit 2f2b90acee.

* [PS-416] Fixed a bug where the item details page was not updating after saving.

* [SG-416] Authenticator toggle remake  (#2027)

* [SG-416] Removed toggle to TOTP. Added on MainPage new entry to go to screen with TOTP codes. Added filter for TOTP codes to be used when searching.

* [SG-416] Removed unnecessary code. Added nav back if there is only 1 cipher with totp code and the user removes it.

* [SG-416] Run dotnet format tool

* [SG-416] PR fixes

* [SG-416] PR Fixes. Manifest formatting. Add try catch. Extracted method and added null protection.

* [SG-416] Make TOTP codes appear above favourites.

* [SG-416] PR fixes. Show error dialog.

Co-authored-by: Carlos J. Muentes <42616259+cmuentes@users.noreply.github.com>
Co-authored-by: Jacob Fink <jfink@bitwarden.com>
2022-08-17 22:10:16 +01:00
aj-rosado
3d9555d420 [PS-1009] Changed keyboard on Passphrase generator to not allow emojis (#2038)
* PS-1009 Added effect to Entry that doesn't allow keyboard with emojis on passphrase separator

* PS-1009 Removed unnecessary ImeOptions setting from NoEmojiKeyboardEffect
Improved code

* PS-1009 Removed unnecessary null validation on Android's NoEmojiKeyboardEffect

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2022-08-16 18:54:28 +01:00
Deividt Gemeli
5f7a1e769a Changed the right margin of the switch at Create Account(#2021) 2022-08-16 18:52:28 +01:00
Joseph Flinn
8b118408fa Add workaround for broken windows 2022 runner (#2040)
* Add workaround for broken windows 2022 runner

* Adding workaround to F-Droid job
2022-08-16 09:46:54 -07:00
github-actions[bot]
de41845e3e Bumped version to 2022.8.1 (#2039)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-08-16 08:01:17 -07:00
aj-rosado
61597585b5 [PS-1219] Crash when login with SSO (#2023)
* PS-1219 Added null checks and improved error handling on SSO Login

* PS-1219 Improved code

* PS-1219 Improved const naming

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2022-08-16 12:05:23 +01:00
Michał Chęciński
e04b250a73 Add renovate config (#2034) 2022-08-12 13:54:57 +02:00
github-actions[bot]
4fbe1b40e3 Autosync the updated translations (#2035)
Co-authored-by: github-actions <>
2022-08-12 11:32:58 +02:00
Federico Maccaroni
3ef5b576ac [EC-371] Fix iOS extensions login more menu (#2016)
* EC-371 fix iOS extensions login more menu and HintPage to display correctly on extensions

* EC-371 fix merge
2022-08-08 11:28:48 -03:00
github-actions[bot]
570b56364a Autosync the updated translations (#2030)
Co-authored-by: github-actions <>
2022-08-05 14:30:57 +02:00
Federico Maccaroni
ae4e8e2d8e [EC-341] Fix show alternative 2FA on iOS extensions (#2011)
* EC-341 Fix show alternative 2FA on iOS extensions

* EC-341 Fix iOS.Core.csproj reference
2022-08-04 16:42:41 -03:00
Todd Martin
2c8406d0ad [ENG-71] Add Github deployment to release pipeline. (#2022)
* Added Github deployment to release pipeline.

* Added explicit expression syntax

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

* Added explicit expression syntax

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

* Added initial-status

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

* Removed in_progress update since it's set on initial status

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

Co-authored-by: Todd Martin <>
Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
2022-08-04 13:47:49 -04:00
Michał Chęciński
94bd5ceed3 Fix Android build (#2029)
Change worker to win-2022 for VS2022
2022-08-04 12:35:17 -04:00
github-actions[bot]
aa6be3d691 Bumped version to 2022.8.0 (#2028)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-08-04 17:28:54 +02:00
Vince Grassia
97fe65647a Update Crowdin action hash (#2025) 2022-08-03 16:51:41 -04:00
Michał Chęciński
ee8b8866e0 Auto bump mobile version after release (#2013)
* Add autobump version number workflow

* Comment pr since not tested

* Update .github/workflows/version-auto-bump.yml

Co-authored-by: Micaiah Martin <77340197+mimartin12@users.noreply.github.com>

* Trigger version bump workflow

* Comment for testing

* add input for testing

* FIx

* Remove testing values

Co-authored-by: Micaiah Martin <77340197+mimartin12@users.noreply.github.com>
2022-08-02 09:34:07 +02:00
Michał Chęciński
3128a4c5c8 Add autobump workflow stub (#2015) 2022-07-29 12:18:57 +02:00
aj-rosado
8ec6545bbc [PS-1116] Improved network error handling (#2007)
* PS-1116 Improved network error handling on ViewPageViewModel and AddEditPageViewModel

* PS-1116 Renamed ViewPage and AddEditPage pages to a more explicit name.
Refactored CheckPassword from the CipherPages to a single Base class.

* PS-1116 Updated variables relative to the AddEditPage and ViewPage refactor to CipherAddEditPage and CipherDetailPage

* Renamed CipherDetailPage to CipherDetailsPage

* Code improvement

* PS-1116 Improved code formatting

* PS-1116 Improved formatting

* PS-1116 Improved code formatting
2022-07-27 17:46:56 +01:00
Federico Maccaroni
90a6850d76 EC-366 Updated AndroidManifest format as what VS4M autoformats (#2014) 2022-07-26 13:08:05 -03:00
Andreas Coroiu
16f70dc0ce [EC-348] change blacklisted to blocked URIs (#2012)
* [EC-348] update blacklisted to blocked URIs

* [EC-348] update variable names
2022-07-26 09:55:16 +02:00
github-actions[bot]
f0ebc5e644 Bumped version to 2022.6.3 (#2005)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-22 09:51:35 -07:00
Andreas Coroiu
03c5dd78c1 [EC-353] sentence cases (#2004) 2022-07-22 10:16:50 +02:00
Andreas Coroiu
e2b6e99a0c feat: change to on and off (#2001) 2022-07-21 15:57:08 +02:00
aj-rosado
263aeef030 [PS-1080] Added text alternative to Boolean custom field icon (#2000)
* PS-1080 Added new accessibility text property to the custom field bool icon

* PS-1080 Added BoolValue property to the FieldView and added new AccessibilityProperty to bool icon
2022-07-21 09:53:39 +01:00
Carlos Gonçalves
f809170c51 [SG-467] Fix environment url validations (#1999)
* [SG-467] Fixed url validation so it works with or without http or https

* [SG-467] Validation has been refactored for simplicity.
2022-07-20 16:27:49 +01:00
Vince Grassia
c2fcc0ac52 Update 'Dry Run' path in Release workflow (#1997) 2022-07-19 15:01:03 -04:00
Federico Maccaroni
5e61fb0a14 EC-325 fix format (#1995) 2022-07-15 17:35:21 +01:00
Pedro da Rocha Pires
cf222bd0c3 [EC-325] Settings option to allow screen capture on Android (#1914)
* settings option to allow screen capture on Android

* Improved code on Screen Capture and added prompt to the user to allow screen capture

* EC-325 Removed async on OnCreate of MainActivity given that's not necessary anymore

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2022-07-15 16:13:10 +01:00
Micaiah Martin
cb0c52fb26 Add publish options to release workflow (#1994) 2022-07-15 07:30:14 -06:00
Michał Chęciński
c07c305384 Add version change check in the version bump workflow (#1992) 2022-07-15 11:08:38 +02:00
Federico Maccaroni
d2fbf5bdea EC-312 Fix crash on entering invalid credentials five times on Autofill (#1988) 2022-07-14 23:17:04 +01:00
Federico Maccaroni
2d2a883b96 EC-306 Fix crash happening on vietnamise when trying to go to Password Autofill on iOS given that the string was the same as Autofill Services and the comparison was misleading. Also refactored so that the action is on each item instead of having to compare to act (#1989) 2022-07-14 23:04:13 +01:00
Federico Maccaroni
1f2fb3f796 [EC-324] Added more logging for information on list crash (#1993)
* EC-324 Added more logging for trying to get more information on list out of range crash on AppCenter

* EC-324 Fix include on iOS.Core.csproj on iOS CollectionView files
2022-07-14 22:54:45 +01:00
Federico Maccaroni
8f3a4b98a5 EC-323 sanitize data on get first letters for avatar image creation (#1990) 2022-07-14 21:33:30 +01:00
Donkeykong307
70cf7431f7 Opera GX Autofill Support (#1855)
Added Opera GX Support for autofill
2022-07-14 21:27:53 +01:00
vincentvidal
f2ba86a62b Add support for iodé Browser (#1886) 2022-07-14 21:24:02 +01:00
Federico Maccaroni
292908f53f [EC-259] Added Account Switching to Share extension on iOS (#1971)
* EC-259 Added Account switching on share extension on iOS, also improved performance for this and exception handling

* EC-259 code formatting

* EC-259 Added account switching to Share extension Send view

* EC-259 Fixed navigation on share extension when a forms page is already presented

* EC-259 Fix send text UI update when going from the iOS extension

* EC-259 Improved DateTimeViewModel with helper property to easily setup date and time at the same time and applied on usage
2022-07-12 14:12:23 -03:00
Carlos Gonçalves
d621a5d2f3 [PS 920] Fix selfhosted url validations (#1967)
* PS-920 - Added feedback to user when saving bad formed URLs
* Added feedback to user when trying to perform login with bad formed URL

* PS-920 - Refactor to use AsyncCommand
*(missing file from previous commit)

* PS-920 - Fixed whitespace formatting

* PS-920 - Removed unused method

* PS-920 - Fixed validation
* Added comment for hard coded string

* PS-920 - Removed unused properties
* Fixed url validations
* Refactored method to local function

* PS-920 - Added exception handling and logging
* Added generic error message string to AppResources
2022-07-11 18:02:11 +01:00
Patrick H. Lauke
75e8276784 Use correct icon for checked/unchecked boolean (#1986)
Closes https://github.com/bitwarden/mobile/issues/1985
2022-07-11 10:48:19 -03:00
Andreas Coroiu
67f49a0591 [PS-686] Mobile update negative copy in settings (#1961)
* feat: update auto totp copy setting

* feat: update show icons settings

* feat: update auto add settings

* feat: update settings and options to sentence case

* feat: update translation keys

With the latest changes the translation keys had diverged from their contents.
This commit fixes that.

* fix: revert AndroidManifest changes

* chore: add todo comments to fix negative functions
2022-07-11 08:45:42 +02:00
Rui Tomé
cceded2a0f Updated the wording on the modal warning when deleting the account (#1982) 2022-07-08 09:28:23 +01:00
Federico Maccaroni
846d3a85a2 EC-308 Fix crash produced by creating avatar image on AccountSwitchingOverlayHelper and also added more logging to see when it happens. (#1983) 2022-07-07 20:24:29 +01:00
github-actions[bot]
7802da2b9c Bumped version to 2022.6.2 (#1981)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-07 09:07:08 -07:00
Rui Tomé
cd56a124d5 [EC-303] Add warning when vault timeout is set to "Never" (#1976)
* Added to AppResources.resx the message warning the user about setting the lockout option to "Never"

* Added the condition to check the newly selected option on the vault timeout settings

* Changed the wording on the warning as to reflect the mobile version

* Changed the vault timeout modal to have the ability to cancel

* Simplified the reversion of value if the user cancels the change
2022-07-07 16:35:58 +01:00
Matt Gibson
58a3662d0f Add user verification to reset password request (#1980)
We only need master password hash because this is currently
only used for sso password setting after auto-provisioning. Key
Connector is not involved in these accounts
2022-07-06 17:23:20 -05:00
Ben Pearo
6c7413e38c replace link to mobile section of contributing documentation with working link (#1978) 2022-07-06 11:43:07 -03:00
Federico Maccaroni
547e61a66b Fix formatting (#1975) 2022-07-05 17:44:45 -04:00
Federico Maccaroni
d246d1dece EC-297 Fix possible crash when copying password on cipher item view. Also improved a bit the code of copying commands (#1974) 2022-07-05 18:14:46 -03:00
Federico Maccaroni
e2502e2e0c Improved the ServiceContainer to be easier to use and not to have the service name hardcoded to register/resolve a service (#1865) 2022-07-05 18:14:31 -03:00
Federico Maccaroni
448cce38e1 Improved BroadcastService and added try...catch on async void callbacks (#1917) 2022-07-05 18:14:10 -03:00
Federico Maccaroni
dbc1e5ea3e Fix crash when trying to Focus an Entry from a background thread and improved the code so there are fewer direct access from the VM to the View (#1879) 2022-07-05 16:37:06 -04:00
github-actions[bot]
a6ddc2496f Bumped version to 2022.6.1 (#1969)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-06-29 13:25:08 -07:00
github-actions[bot]
d9a818279f Bumped version to 2022.6.0 (#1968)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-06-29 09:11:34 -07:00
Matt Gibson
6e2e613fee Add ssoToken to limit lifetime of SSO redirect (#1965) 2022-06-27 14:53:15 -05:00
mp-bw
109aeb49e4 [BEEEP] [PS-940] Support for dark theme selection while using Default (System) theme (#1959)
* support for dark theme selection while using Default (System) theme

* refinements
2022-06-21 14:59:30 -04:00
Joseph Flinn
c892e9fa57 Pin NuGet version (#1957)
* Pinning the version of NuGet to 5.x

* pinning NuGet verison to 5.9.x

* pinning NuGet to 5.9.0.7134

* pinning NuGet to 5.9.0
2022-06-16 14:48:09 -07:00
Federico Maccaroni
b2500557e7 SG-396 Fix tappable area after hiding account switching (#1956) 2022-06-16 17:09:50 -04:00
mp-bw
7c311fbb55 fix for missing personal items added prior to joining org with personal ownership policy (#1955) 2022-06-16 09:55:09 -04:00
mp-bw
f24388c1b5 separate init and showVaultFilter property set (#1954) 2022-06-15 16:18:30 -03:00
Federico Maccaroni
3aef86bd34 [SG-386] iOS Update user state when coming from background (#1952)
* SG-386 Updated active user when coming from background to the iOS app and the extension had switched users

* Added iOSExtensionActiveUserIdKey to preference keys

* Reorder iOS preference keys
2022-06-15 13:44:25 -03:00
mp-bw
c53a85cd50 [SG-390] Fix for missing org items with single org & personal ownership enabled (#1953)
* fix for missing org items with single org & personal ownership enabled

* fix for ui issue with vault filter state change on pull to refresh
2022-06-15 11:43:54 -03:00
mp-bw
448758a697 Additional logic around filter display (#1951) 2022-06-14 14:02:03 -03:00
227 changed files with 20714 additions and 4588 deletions

View File

@@ -57,12 +57,37 @@ jobs:
android:
name: Android
runs-on: windows-2019
runs-on: windows-2022
needs: setup
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
nuget-version: 5.9.0
- name: Set up MSBuild
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
- name: Work Around for broken Windows 2022 Runner Image
run: |
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
$InstallPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
$componentsToAdd = @(
"Component.Xamarin"
)
[string]$workloadArgs = $componentsToAdd | ForEach-Object {" --add " + $_}
$Arguments = ('/c', "vs_installer.exe", 'modify', '--installPath', "`"$InstallPath`"",$workloadArgs, '--quiet', '--norestart', '--nocache')
$process = Start-Process -FilePath cmd.exe -ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
if ($process.ExitCode -eq 0)
{
Write-Host "components have been successfully added"
}
else
{
Write-Host "components were not installed"
exit 1
}
- name: Print environment
run: |
nuget help | grep Version
@@ -207,11 +232,36 @@ jobs:
f-droid:
name: F-Droid Build
runs-on: windows-2019
runs-on: windows-2022
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
nuget-version: 5.9.0
- name: Set up MSBuild
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
- name: Work Around for broken Windows 2022 Runner Image
run: |
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
$InstallPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
$componentsToAdd = @(
"Component.Xamarin"
)
[string]$workloadArgs = $componentsToAdd | ForEach-Object {" --add " + $_}
$Arguments = ('/c', "vs_installer.exe", 'modify', '--installPath', "`"$InstallPath`"",$workloadArgs, '--quiet', '--norestart', '--nocache')
$process = Start-Process -FilePath cmd.exe -ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
if ($process.ExitCode -eq 0)
{
Write-Host "components have been successfully added"
}
else
{
Write-Host "components were not installed"
exit 1
}
- name: Print environment
run: |
nuget help | grep Version
@@ -368,6 +418,11 @@ jobs:
runs-on: macos-11
needs: setup
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
nuget-version: 5.9.0
- name: Print environment
run: |
nuget help | grep Version

View File

@@ -30,7 +30,7 @@ jobs:
secrets: "crowdin-api-token"
- name: Download translations
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -13,6 +13,11 @@ on:
- Initial Release
- Redeploy
- Dry Run
fdroid_publish:
description: 'Publish to f-droid store'
required: true
default: true
type: boolean
jobs:
release:
@@ -48,18 +53,38 @@ jobs:
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch-name::$BRANCH_NAME"
- name: Create GitHub deployment
uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48
id: deployment
with:
token: '${{ secrets.GITHUB_TOKEN }}'
initial-status: 'in_progress'
environment: 'production'
description: 'Deployment ${{ steps.version.outputs.version }} from branch ${{ steps.branch.outputs.branch-name }}'
task: release
- name: Download all artifacts
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ steps.branch.outputs.branch-name }}
- name: Download all artifacts
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
with:
workflow: build.yml
workflow_conclusion: success
branch: master
- name: Prep Bitwarden iOS release asset
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
- name: Create release
if: github.event.inputs.release_type != 'Dry Run'
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
with:
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
@@ -73,16 +98,34 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
- name: Update deployment status to Success
if: ${{ success() }}
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'success'
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
- name: Update deployment status to Failure
if: ${{ failure() }}
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'failure'
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
f-droid:
name: F-Droid Release
runs-on: ubuntu-20.04
needs: release
if: inputs.fdroid_publish
steps:
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
with:
workflow: build.yml
@@ -90,6 +133,15 @@ jobs:
branch: ${{ needs.release.outputs.branch-name }}
name: com.x8bit.bitwarden-fdroid.apk
- name: Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
with:
workflow: build.yml
workflow_conclusion: success
branch: master
name: com.x8bit.bitwarden-fdroid.apk
- name: Set up Node
uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2.5.1
with:
@@ -155,5 +207,5 @@ jobs:
cd $GITHUB_WORKSPACE
- name: Deploy to gh-pages
if: github.event.inputs.release_type != 'Dry Run'
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: npm run deploy

67
.github/workflows/version-auto-bump.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
---
name: Version Auto Bump
on:
release:
types: [published]
jobs:
setup:
name: "Setup"
runs-on: ubuntu-20.04
outputs:
version_number: ${{ steps.version.outputs.new-version }}
steps:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Get version to bump
id: version
env:
RELEASE_TAG: ${{ github.event.release.tag }}
run: |
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\1/')
CURR_VER=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\2/')
echo $CURR_VER
((CURR_VER++))
NEW_VER=$CURR_MAJOR$CURR_VER
echo $NEW_VER
echo "::set-output name=new-version::$NEW_VER"
trigger_version_bump:
name: "Trigger version bump workflow"
runs-on: ubuntu-20.04
needs:
- setup
steps:
- name: Login to Azure
uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
env:
KEYVAULT: bitwarden-prod-kv
SECRET: "github-pat-bitwarden-devops-bot-repo-scope"
run: |
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $SECRET --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$SECRET::$VALUE"
- name: Call GitHub API to trigger workflow bump
env:
TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
VERSION: ${{ needs.setup.outputs.version_number}}
run: |
JSON_STRING=$(printf '{"ref":"master", "inputs": { "version_number":"%s"}}' "$VERSION")
curl \
-X POST \
-i -u bitwarden-devops-bot:$TOKEN \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/bitwarden/mobile/actions/workflows/version-bump.yml/dispatches \
-d $JSON_STRING

View File

@@ -19,12 +19,6 @@ jobs:
- name: Create Version Branch
run: |
git switch -c version_bump_${{ github.event.inputs.version_number }}
git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Checkout Version Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
with:
ref: version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - Android XML
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
@@ -56,16 +50,32 @@ jobs:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/iOS/Info.plist"
- name: Commit files
- name: Setup git
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
- name: Check if version changed
id: version-changed
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "::set-output name=changes_to_commit::TRUE"
else
echo "::set-output name=changes_to_commit::FALSE"
echo "No changes to commit!";
fi
- name: Commit files
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: |
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
- name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Create Version PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -12,7 +12,7 @@ The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin
# Build/Run
Please refer to the [Mobile section](https://contributing.bitwarden.com/clients/mobile) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
Please refer to the [Mobile section](https://contributing.bitwarden.com/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
# We're Hiring!

22
renovate.json Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
"schedule:monthly",
":maintainLockFilesMonthly",
":preserveSemverRanges",
":rebaseStalePrs",
":disableDependencyDashboard"
],
"enabledManagers": [
"nuget"
],
"packageRules": [
{
"matchManagers": ["nuget"],
"groupName": "Nuget updates",
"groupSlug": "nuget",
"separateMajorMinor": false
}
]
}

View File

@@ -54,6 +54,7 @@ namespace Bit.Droid.Accessibility
new Browser("com.google.android.apps.chrome", "url_bar"),
new Browser("com.google.android.apps.chrome_dev", "url_bar"),
// Rem. for "com.google.android.captiveportallogin": URL displayed in ActionBar subtitle without viewId.
new Browser("com.iode.firefox", "mozac_browser_toolbar_url_view"),
new Browser("com.jamal2367.styx", "search"),
new Browser("com.kiwibrowser.browser", "url_bar"),
new Browser("com.kiwibrowser.browser.dev", "url_bar"),
@@ -67,6 +68,7 @@ namespace Bit.Droid.Accessibility
new Browser("com.naver.whale", "url_bar"),
new Browser("com.opera.browser", "url_field"),
new Browser("com.opera.browser.beta", "url_field"),
new Browser("com.opera.gx", "addressbarEdit"),
new Browser("com.opera.mini.native", "url_field"),
new Browser("com.opera.mini.native.beta", "url_field"),
new Browser("com.opera.touch", "addressbarEdit"),

View File

@@ -151,6 +151,7 @@
<Compile Include="Services\ClipboardService.cs" />
<Compile Include="Utilities\IntentExtensions.cs" />
<Compile Include="Renderers\CustomPageRenderer.cs" />
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\bwi-font.ttf" />

View File

@@ -73,6 +73,7 @@ namespace Bit.Droid.Autofill
"com.google.android.apps.chrome",
"com.google.android.apps.chrome_dev",
"com.google.android.captiveportallogin",
"com.iode.firefox",
"com.jamal2367.styx",
"com.kiwibrowser.browser",
"com.kiwibrowser.browser.dev",
@@ -86,6 +87,7 @@ namespace Bit.Droid.Autofill
"com.naver.whale",
"com.opera.browser",
"com.opera.browser.beta",
"com.opera.gx",
"com.opera.mini.native",
"com.opera.mini.native.beta",
"com.opera.touch",

View File

@@ -0,0 +1,24 @@
using Android.Widget;
using Bit.Droid.Effects;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportEffect(typeof(NoEmojiKeyboardEffect), nameof(NoEmojiKeyboardEffect))]
namespace Bit.Droid.Effects
{
public class NoEmojiKeyboardEffect : PlatformEffect
{
protected override void OnAttached()
{
if (Control is EditText editText)
{
editText.InputType = Android.Text.InputTypes.ClassText | Android.Text.InputTypes.TextVariationVisiblePassword | Android.Text.InputTypes.TextFlagMultiLine;
}
}
protected override void OnDetached()
{
}
}
}

View File

@@ -64,10 +64,11 @@ namespace Bit.Droid
Intent?.Validate();
base.OnCreate(savedInstanceState);
if (!CoreHelpers.InDebugMode())
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget(_ =>
{
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
}
});
ServiceContainer.Resolve<ILogger>("logger").InitAsync();

View File

@@ -20,6 +20,7 @@ using System.Net;
using Bit.App.Utilities;
using Bit.App.Pages;
using Bit.App.Utilities.AccountManagement;
using Bit.App.Controls;
#if !FDROID
using Android.Gms.Security;
#endif
@@ -69,7 +70,8 @@ namespace Bit.Droid
ServiceContainer.Resolve<IStorageService>("secureStorageService"),
ServiceContainer.Resolve<IStateService>("stateService"),
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService"));
ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
}
#if !FDROID
@@ -99,12 +101,13 @@ namespace Bit.Droid
{
ServiceContainer.Register<INativeLogService>("nativeLogService", new AndroidLogService());
#if FDROID
ServiceContainer.Register<ILogger>("logger", new StubLogger());
var logger = new StubLogger();
#elif DEBUG
ServiceContainer.Register<ILogger>("logger", DebugLogger.Instance);
var logger = DebugLogger.Instance;
#else
ServiceContainer.Register<ILogger>("logger", Logger.Instance);
var logger = Logger.Instance;
#endif
ServiceContainer.Register("logger", logger);
// Note: This might cause a race condition. Investigate more.
Task.Run(() =>
@@ -124,13 +127,13 @@ namespace Bit.Droid
var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db"));
var localizeService = new LocalizeService();
var broadcasterService = new BroadcasterService();
var broadcasterService = new BroadcasterService(logger);
var messagingService = new MobileBroadcasterMessagingService(broadcasterService);
var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo());
var secureStorageService = new SecureStorageService();
var cryptoPrimitiveService = new CryptoPrimitiveService();
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
var stateService = new StateService(mobileStorageService, secureStorageService);
var stateService = new StateService(mobileStorageService, secureStorageService, messagingService);
var stateMigrationService =
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
var clipboardService = new ClipboardService(stateService);
@@ -159,6 +162,7 @@ namespace Bit.Droid
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
// Push
#if FDROID

View File

@@ -1,57 +1,49 @@
<?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="2022.05.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.NFC"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
<application android:label="Bitwarden" android:theme="@style/LaunchTheme" android:allowBackup="false" tools:replace="android:allowBackup" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.x8bit.bitwarden.fileprovider" android:exported="false" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths"/>
</provider>
<meta-data android:name="android.max_aspect" android:value="2.1"/>
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions"/>
<!-- Support for Samsung "Multi Window" mode (for Android < 7.0 users) -->
<meta-data android:name="com.samsung.android.sdk.multiwindow.enable" android:value="true"/>
<meta-data android:name="com.samsung.android.sdk.multiwindow.penwindow.enable" android:value="true"/>
<!-- Support for LG "Dual Window" mode (for Android < 7.0 users) -->
<meta-data android:name="com.lge.support.SPLIT_WINDOW" android:value="true"/>
<!-- Declare MainActivity manually so we can set LaunchMode using API dependant resource -->
<activity android:name="com.x8bit.bitwarden.MainActivity" android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|uiMode" android:exported="true" android:icon="@mipmap/ic_launcher" android:label="Bitwarden" android:launchMode="@integer/launchModeAPIlevel" android:theme="@style/LaunchTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="application/*"/>
<data android:mimeType="image/*"/>
<data android:mimeType="video/*"/>
<data android:mimeType="text/*"/>
</intent-filter>
</activity>
</application>
<!-- Package visibility (for Android 11+) -->
<queries>
<intent>
<action android:name="*"/>
</intent>
</queries>
</manifest>
<?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="2022.9.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<application android:label="Bitwarden" android:theme="@style/LaunchTheme" android:allowBackup="false" tools:replace="android:allowBackup" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.x8bit.bitwarden.fileprovider" android:exported="false" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" />
</provider>
<meta-data android:name="android.max_aspect" android:value="2.1" />
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions" />
<!-- Support for Samsung "Multi Window" mode (for Android < 7.0 users) -->
<meta-data android:name="com.samsung.android.sdk.multiwindow.enable" android:value="true" />
<meta-data android:name="com.samsung.android.sdk.multiwindow.penwindow.enable" android:value="true" />
<!-- Support for LG "Dual Window" mode (for Android < 7.0 users) -->
<meta-data android:name="com.lge.support.SPLIT_WINDOW" android:value="true" />
<!-- Declare MainActivity manually so we can set LaunchMode using API dependant resource -->
<activity android:name="com.x8bit.bitwarden.MainActivity" android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|uiMode" android:exported="true" android:icon="@mipmap/ic_launcher" android:label="Bitwarden" android:launchMode="@integer/launchModeAPIlevel" android:theme="@style/LaunchTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="text/*" />
</intent-filter>
</activity>
</application>
<!-- Package visibility (for Android 11+) -->
<queries>
<intent>
<action android:name="*" />
</intent>
</queries>
</manifest>

View File

@@ -77,6 +77,9 @@
<compatibility-package
android:name="com.google.android.captiveportallogin"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.iode.firefox"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.jamal2367.styx"
android:maxLongVersionCode="10000000000"/>
@@ -116,6 +119,9 @@
<compatibility-package
android:name="com.opera.browser.beta"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.opera.gx"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.opera.mini.native"
android:maxLongVersionCode="10000000000"/>

View File

@@ -28,17 +28,25 @@ namespace Bit.Droid.Services
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
{
// Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+
if ((int)Build.VERSION.SdkInt < 33)
try
{
await Clipboard.SetTextAsync(text);
}
else
{
CopyToClipboard(text, isSensitive);
}
// Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+
if ((int)Build.VERSION.SdkInt < 33)
{
await Clipboard.SetTextAsync(text);
}
else
{
CopyToClipboard(text, isSensitive);
}
await ClearClipboardAlarmAsync(expiresInMs);
await ClearClipboardAlarmAsync(expiresInMs);
}
catch (Java.Lang.SecurityException ex) when (ex.Message.Contains("does not belong to"))
{
// #1962 Just ignore, the content is copied either way but there is some app interfiering in the process
// that the OS catches and just throws this exception.
}
}
public bool IsCopyNotificationHandledByPlatform()

View File

@@ -948,5 +948,21 @@ namespace Bit.Droid.Services
{
// for any Android-specific cleanup required after switching accounts
}
public async Task SetScreenCaptureAllowedAsync()
{
if (CoreHelpers.ForceScreenCaptureEnabled())
{
return;
}
var activity = CrossCurrentActivity.Current?.Activity;
if (await _stateService.GetScreenCaptureAllowedAsync())
{
activity.RunOnUiThread(() => activity.Window.ClearFlags(WindowManagerFlags.Secure));
return;
}
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
}
}
}

View File

@@ -59,10 +59,10 @@ namespace Bit.Droid.Utilities
{
if (string.IsNullOrWhiteSpace(theme) && osDarkModeEnabled)
{
theme = "dark";
theme = ThemeManager.Dark;
}
if (theme == "dark" || theme == "black" || theme == "nord")
if (theme == ThemeManager.Dark || theme == ThemeManager.Black || theme == ThemeManager.Nord)
{
LightTheme = false;
}

View File

@@ -8,5 +8,6 @@ namespace Bit.App.Abstractions
{
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
Task LogOutAsync(string userId, bool userInitiated, bool expired);
}
}

View File

@@ -48,5 +48,6 @@ namespace Bit.App.Abstractions
bool SupportsFido2();
float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync();
Task SetScreenCaptureAllowedAsync();
}
}

View File

@@ -97,11 +97,11 @@
<Compile Update="Pages\Vault\PasswordHistoryPage.xaml.cs">
<DependentUpon>PasswordHistoryPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Vault\AddEditPage.xaml.cs">
<DependentUpon>AddEditPage.xaml</DependentUpon>
<Compile Update="Pages\Vault\CipherDetailsPage.xaml.cs">
<DependentUpon>CipherDetailsPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Vault\ViewPage.xaml.cs">
<DependentUpon>ViewPage.xaml</DependentUpon>
<Compile Update="Pages\Vault\CipherAddEditPage.xaml.cs">
<DependentUpon>CipherAddEditPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Settings\SettingsPage\SettingsPage.xaml.cs">
<DependentUpon>SettingsPage.xaml</DependentUpon>
@@ -129,12 +129,10 @@
<Folder Include="Behaviors\" />
<Folder Include="Controls\AccountSwitchingOverlay\" />
<Folder Include="Utilities\AccountManagement\" />
<Folder Include="Controls\DateTime\" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Controls\CipherViewCell\CipherViewCell.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Remove="Pages\Accounts\AccountsPopupPage.xaml" />
</ItemGroup>
@@ -162,12 +160,6 @@
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Styles\Base.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\AppResources.cs.Designer.cs">
<DependentUpon>AppResources.cs.resx</DependentUpon>
@@ -422,5 +414,6 @@
<None Remove="Xamarin.CommunityToolkit" />
<None Remove="Controls\AccountSwitchingOverlay\" />
<None Remove="Utilities\AccountManagement\" />
<None Remove="Controls\DateTime\" />
</ItemGroup>
</Project>

View File

@@ -10,6 +10,7 @@ using Bit.App.Utilities.AccountManagement;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
@@ -56,86 +57,93 @@ namespace Bit.App
Bootstrap();
_broadcasterService.Subscribe(nameof(App), async (message) =>
{
if (message.Command == "showDialog")
try
{
var details = message.Data as DialogDetails;
var confirmed = true;
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
AppResources.Ok : details.ConfirmText;
Device.BeginInvokeOnMainThread(async () =>
if (message.Command == "showDialog")
{
if (!string.IsNullOrWhiteSpace(details.CancelText))
var details = message.Data as DialogDetails;
var confirmed = true;
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
AppResources.Ok : details.ConfirmText;
Device.BeginInvokeOnMainThread(async () =>
{
confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText,
details.CancelText);
}
else
{
await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText);
}
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
});
}
else if (message.Command == "resumed")
{
if (Device.RuntimePlatform == Device.iOS)
if (!string.IsNullOrWhiteSpace(details.CancelText))
{
confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText,
details.CancelText);
}
else
{
await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText);
}
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
});
}
else if (message.Command == "resumed")
{
ResumedAsync().FireAndForget();
if (Device.RuntimePlatform == Device.iOS)
{
ResumedAsync().FireAndForget();
}
}
else if (message.Command == "slept")
{
if (Device.RuntimePlatform == Device.iOS)
{
await SleptAsync();
}
}
else if (message.Command == "migrated")
{
await Task.Delay(1000);
await _accountsManager.NavigateOnAccountChangeAsync();
}
else if (message.Command == "popAllAndGoToTabGenerator" ||
message.Command == "popAllAndGoToTabMyVault" ||
message.Command == "popAllAndGoToTabSend" ||
message.Command == "popAllAndGoToAutofillCiphers")
{
Device.BeginInvokeOnMainThread(async () =>
{
if (Current.MainPage is TabsPage tabsPage)
{
while (tabsPage.Navigation.ModalStack.Count > 0)
{
await tabsPage.Navigation.PopModalAsync(false);
}
if (message.Command == "popAllAndGoToAutofillCiphers")
{
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
}
else if (message.Command == "popAllAndGoToTabMyVault")
{
Options.MyVaultTile = false;
tabsPage.ResetToVaultPage();
}
else if (message.Command == "popAllAndGoToTabGenerator")
{
Options.GeneratorTile = false;
tabsPage.ResetToGeneratorPage();
}
else if (message.Command == "popAllAndGoToTabSend")
{
tabsPage.ResetToSendPage();
}
}
});
}
else if (message.Command == "convertAccountToKeyConnector")
{
Device.BeginInvokeOnMainThread(async () =>
{
await Application.Current.MainPage.Navigation.PushModalAsync(
new NavigationPage(new RemoveMasterPasswordPage()));
});
}
}
else if (message.Command == "slept")
catch (Exception ex)
{
if (Device.RuntimePlatform == Device.iOS)
{
await SleptAsync();
}
}
else if (message.Command == "migrated")
{
await Task.Delay(1000);
await _accountsManager.NavigateOnAccountChangeAsync();
}
else if (message.Command == "popAllAndGoToTabGenerator" ||
message.Command == "popAllAndGoToTabMyVault" ||
message.Command == "popAllAndGoToTabSend" ||
message.Command == "popAllAndGoToAutofillCiphers")
{
Device.BeginInvokeOnMainThread(async () =>
{
if (Current.MainPage is TabsPage tabsPage)
{
while (tabsPage.Navigation.ModalStack.Count > 0)
{
await tabsPage.Navigation.PopModalAsync(false);
}
if (message.Command == "popAllAndGoToAutofillCiphers")
{
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
}
else if (message.Command == "popAllAndGoToTabMyVault")
{
Options.MyVaultTile = false;
tabsPage.ResetToVaultPage();
}
else if (message.Command == "popAllAndGoToTabGenerator")
{
Options.GeneratorTile = false;
tabsPage.ResetToGeneratorPage();
}
else if (message.Command == "popAllAndGoToTabSend")
{
tabsPage.ResetToSendPage();
}
}
});
}
else if (message.Command == "convertAccountToKeyConnector")
{
Device.BeginInvokeOnMainThread(async () =>
{
await Application.Current.MainPage.Navigation.PushModalAsync(
new NavigationPage(new RemoveMasterPasswordPage()));
});
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
}
@@ -202,6 +210,7 @@ namespace Bit.App
private async Task ResumedAsync()
{
await _stateService.CheckExtensionActiveUserAndSwitchIfNeededAsync();
await _vaultTimeoutService.CheckVaultTimeoutAsync();
_messagingService.Send("startEventTimer");
await UpdateThemeAsync();
@@ -292,7 +301,7 @@ namespace Bit.App
UpdateThemeAsync();
};
Current.MainPage = new NavigationPage(new HomePage(Options));
var mainPageTask = _accountsManager.NavigateOnAccountChangeAsync();
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
}
@@ -321,20 +330,20 @@ namespace Bit.App
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
if (topPage is NavigationPage navPage)
{
if (navPage.CurrentPage is ViewPage viewPage)
if (navPage.CurrentPage is CipherDetailsPage cipherDetailsPage)
{
lastPageBeforeLock = new PreviousPageInfo
{
Page = "view",
CipherId = viewPage.ViewModel.CipherId
CipherId = cipherDetailsPage.ViewModel.CipherId
};
}
else if (navPage.CurrentPage is AddEditPage addEditPage && addEditPage.ViewModel.EditMode)
else if (navPage.CurrentPage is CipherAddEditPage cipherAddEditPage && cipherAddEditPage.ViewModel.EditMode)
{
lastPageBeforeLock = new PreviousPageInfo
{
Page = "edit",
CipherId = addEditPage.ViewModel.CipherId
CipherId = cipherAddEditPage.ViewModel.CipherId
};
}
}
@@ -369,7 +378,7 @@ namespace Bit.App
Current.MainPage = new TabsPage(Options);
break;
case NavigationTarget.AddEditCipher:
Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options));
Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options));
break;
case NavigationTarget.AutofillCiphers:
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));

View File

@@ -65,6 +65,8 @@ namespace Bit.App.Controls
public bool LongPressAccountEnabled { get; set; } = true;
public Action AfterHide { get; set; }
public async Task ToggleVisibilityAsync()
{
if (IsVisible)
@@ -137,6 +139,8 @@ namespace Bit.App.Controls
// remove overlay
IsVisible = false;
AfterHide?.Invoke();
});
}

View File

@@ -45,23 +45,28 @@ namespace Bit.App.Controls
public ICommand LongPressAccountCommand { get; }
public bool FromIOSExtension { get; set; }
private async Task SelectAccountAsync(AccountViewCellViewModel item)
{
if (item.AccountView.IsAccount)
if (!item.AccountView.IsAccount)
{
if (!item.AccountView.IsActive)
_messagingService.Send(AccountsManagerMessageCommands.ADD_ACCOUNT);
return;
}
if (!item.AccountView.IsActive)
{
await _stateService.SetActiveUserAsync(item.AccountView.UserId);
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
if (FromIOSExtension)
{
await _stateService.SetActiveUserAsync(item.AccountView.UserId);
_messagingService.Send("switchedAccount");
}
else if (AllowActiveAccountSelection)
{
_messagingService.Send("switchedAccount");
await _stateService.SaveExtensionActiveUserIdToStorageAsync(item.AccountView.UserId);
}
}
else
else if (AllowActiveAccountSelection)
{
_messagingService.Send("addAccount");
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
}
}

View File

@@ -13,7 +13,8 @@ namespace Bit.App.Controls
public AccountViewCellViewModel(AccountView accountView)
{
AccountView = accountView;
AvatarImageSource = new AvatarImageSource(AccountView.Name, AccountView.Email);
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
?.GetOrCreateAvatar(AccountView.Name, AccountView.Email);
}
public AccountView AccountView

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.AuthenticatorViewCell"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
StyleClass="list-row, list-row-platform"
HorizontalOptions="FillAndExpand"
x:DataType="pages:GroupingsPageTOTPListItem"
ColumnDefinitions="40,*,40,Auto,40"
RowSpacing="0"
Padding="0,10,0,0"
RowDefinitions="*,*">
<Grid.Resources>
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<u:InverseBoolConverter x:Key="inverseBool" />
</Grid.Resources>
<controls:IconLabel
Grid.Column="0"
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
Grid.RowSpan="2"
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
AutomationProperties.IsInAccessibleTree="False" />
<ff:CachedImage
Grid.Column="0"
BitmapOptimizations="True"
ErrorPlaceholder="login.png"
LoadingPlaceholder="login.png"
HorizontalOptions="Center"
VerticalOptions="Center"
WidthRequest="22"
HeightRequest="22"
Grid.RowSpan="2"
IsVisible="{Binding ShowIconImage}"
Source="{Binding IconImageSource, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="False" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="1"
Grid.Row="0"
VerticalTextAlignment="Center"
VerticalOptions="Fill"
StyleClass="list-title, list-title-platform"
Text="{Binding Cipher.Name}" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="1"
Grid.Row="1"
VerticalTextAlignment="Center"
VerticalOptions="Fill"
StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Cipher.SubTitle}" />
<controls:CircularProgressbarView
Progress="{Binding Progress}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand" />
<Label
Text="{Binding TotpSec, Mode=OneWay}"
Style="{DynamicResource textTotp}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
StyleClass="text-sm"
HorizontalTextAlignment="Center"
HorizontalOptions="Fill"
VerticalTextAlignment="Center"
VerticalOptions="Fill" />
<StackLayout
Grid.Row="0"
Grid.Column="3"
Margin="3,0,2,0"
Spacing="5"
Grid.RowSpan="2"
Orientation="Horizontal"
HorizontalOptions="Fill"
VerticalOptions="Fill">
<controls:MonoLabel
Text="{Binding TotpCodeFormattedStart, Mode=OneWay}"
Style="{DynamicResource textTotp}"
StyleClass="text-lg"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
HorizontalOptions="Center"
VerticalOptions="FillAndExpand" />
<controls:MonoLabel
Text="{Binding TotpCodeFormattedEnd, Mode=OneWay}"
Style="{DynamicResource textTotp}"
StyleClass="text-lg"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
HorizontalOptions="Center"
VerticalOptions="FillAndExpand" />
</StackLayout>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
CommandParameter="LoginTotp"
Grid.Row="0"
Grid.Column="4"
Grid.RowSpan="2"
Padding="0,0,1,0"
HorizontalOptions="Center"
VerticalOptions="Center"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyTotp}" />
</controls:ExtendedGrid>

View File

@@ -0,0 +1,67 @@
using System;
using Bit.App.Pages;
using Bit.App.Utilities;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class AuthenticatorViewCell : ExtendedGrid
{
public static readonly BindableProperty CipherProperty = BindableProperty.Create(
nameof(Cipher), typeof(CipherView), typeof(AuthenticatorViewCell), default(CipherView), BindingMode.TwoWay);
public static readonly BindableProperty WebsiteIconsEnabledProperty = BindableProperty.Create(
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(AuthenticatorViewCell));
public static readonly BindableProperty TotpSecProperty = BindableProperty.Create(
nameof(TotpSec), typeof(long), typeof(AuthenticatorViewCell));
public AuthenticatorViewCell()
{
InitializeComponent();
}
public Command CopyCommand { get; set; }
public CipherView Cipher
{
get => GetValue(CipherProperty) as CipherView;
set => SetValue(CipherProperty, value);
}
public bool? WebsiteIconsEnabled
{
get => (bool)GetValue(WebsiteIconsEnabledProperty);
set => SetValue(WebsiteIconsEnabledProperty, value);
}
public long TotpSec
{
get => (long)GetValue(TotpSecProperty);
set => SetValue(TotpSecProperty, value);
}
public bool ShowIconImage
{
get => WebsiteIconsEnabled ?? false
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
&& IconImageSource != null;
}
private string _iconImageSource = string.Empty;
public string IconImageSource
{
get
{
if (_iconImageSource == string.Empty) // default value since icon source can return null
{
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
}
return _iconImageSource;
}
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SkiaSharp;
@@ -50,7 +51,7 @@ namespace Bit.App.Controls
private Stream Draw()
{
string chars = null;
string chars;
string upperData = null;
if (string.IsNullOrEmpty(_data))
@@ -71,62 +72,83 @@ namespace Bit.App.Controls
var textColor = Color.White;
var size = 50;
var bitmap = new SKBitmap(
size * 2,
using (var bitmap = new SKBitmap(size * 2,
size * 2,
SKImageInfo.PlatformColorType,
SKAlphaType.Premul);
var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
var radius = midX - midX / 5;
var circlePaint = new SKPaint
SKAlphaType.Premul))
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex())
};
canvas.DrawCircle(midX, midY, radius, circlePaint);
using (var canvas = new SKCanvas(bitmap))
{
canvas.Clear(SKColors.Transparent);
using (var paint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex())
})
{
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
var radius = midX - midX / 5;
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
var textSize = midX / 1.3f;
var textPaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
Color = SKColor.Parse(textColor.ToHex()),
TextSize = textSize,
TextAlign = SKTextAlign.Center,
Typeface = typeface
};
var rect = new SKRect();
textPaint.MeasureText(chars, ref rect);
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
using (var circlePaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex())
})
{
canvas.DrawCircle(midX, midY, radius, circlePaint);
return SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100).AsStream();
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
var textSize = midX / 1.3f;
using (var textPaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
Color = SKColor.Parse(textColor.ToHex()),
TextSize = textSize,
TextAlign = SKTextAlign.Center,
Typeface = typeface
})
{
var rect = new SKRect();
textPaint.MeasureText(chars, ref rect);
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
using (var img = SKImage.FromBitmap(bitmap))
{
var data = img.Encode(SKEncodedImageFormat.Png, 100);
return data?.AsStream(true);
}
}
}
}
}
}
}
private string GetFirstLetters(string data, int charCount)
{
var parts = data.Split();
var sanitizedData = data.Trim();
var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 1 && charCount <= 2)
{
var text = "";
for (int i = 0; i < charCount; i++)
var text = string.Empty;
for (var i = 0; i < charCount; i++)
{
text += parts[i].Substring(0, 1);
text += parts[i][0];
}
return text;
}
if (data.Length > 2)
if (sanitizedData.Length > 2)
{
return data.Substring(0, 2);
return sanitizedData.Substring(0, 2);
}
return data;
return sanitizedData;
}
private Color StringToColor(string str)

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Concurrent;
namespace Bit.App.Controls
{
public interface IAvatarImageSourcePool
{
AvatarImageSource GetOrCreateAvatar(string name, string email);
}
public class AvatarImageSourcePool : IAvatarImageSourcePool
{
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
public AvatarImageSource GetOrCreateAvatar(string name, string email)
{
var key = $"{name}{email}";
if (!_cache.TryGetValue(key, out var avatar))
{
avatar = new AvatarImageSource(name, email);
if (!_cache.TryAdd(key, avatar)
&&
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.
{
// if add and get after fails, then something wrong is going on with this method.
throw new InvalidOperationException("Something is wrong creating the avatar image");
}
}
return avatar;
}
}
}

View File

@@ -0,0 +1,139 @@
using System;
using System.Runtime.CompilerServices;
using SkiaSharp;
using SkiaSharp.Views.Forms;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class CircularProgressbarView : SKCanvasView
{
private Circle _circle;
public static readonly BindableProperty ProgressProperty = BindableProperty.Create(
nameof(Progress), typeof(double), typeof(CircularProgressbarView), propertyChanged: OnProgressChanged);
public static readonly BindableProperty RadiusProperty = BindableProperty.Create(
nameof(Radius), typeof(float), typeof(CircularProgressbarView), 15f);
public static readonly BindableProperty StrokeWidthProperty = BindableProperty.Create(
nameof(StrokeWidth), typeof(float), typeof(CircularProgressbarView), 3f);
public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create(
nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
public static readonly BindableProperty EndingProgressColorProperty = BindableProperty.Create(
nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
public static readonly BindableProperty BackgroundProgressColorProperty = BindableProperty.Create(
nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
public double Progress
{
get { return (double)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
}
public float Radius
{
get => (float)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
public float StrokeWidth
{
get => (float)GetValue(StrokeWidthProperty);
set => SetValue(StrokeWidthProperty, value);
}
public Color ProgressColor
{
get => (Color)GetValue(ProgressColorProperty);
set => SetValue(ProgressColorProperty, value);
}
public Color EndingProgressColor
{
get => (Color)GetValue(EndingProgressColorProperty);
set => SetValue(EndingProgressColorProperty, value);
}
public Color BackgroundProgressColor
{
get => (Color)GetValue(BackgroundProgressColorProperty);
set => SetValue(BackgroundProgressColorProperty, value);
}
private static void OnProgressChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var context = bindable as CircularProgressbarView;
context.InvalidateSurface();
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(Progress))
{
_circle = new Circle(Radius * (float)DeviceDisplay.MainDisplayInfo.Density, (info) => new SKPoint((float)info.Width / 2, (float)info.Height / 2));
}
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
if (_circle != null)
{
_circle.CalculateCenter(e.Info);
e.Surface.Canvas.Clear();
DrawCircle(e.Surface.Canvas, _circle, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, BackgroundProgressColor.ToSKColor());
DrawArc(e.Surface.Canvas, _circle, () => (float)Progress, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, ProgressColor.ToSKColor(), EndingProgressColor.ToSKColor());
}
}
private void DrawCircle(SKCanvas canvas, Circle circle, float strokewidth, SKColor color)
{
canvas.DrawCircle(circle.Center, circle.Redius,
new SKPaint()
{
StrokeWidth = strokewidth,
Color = color,
IsStroke = true,
IsAntialias = true
});
}
private void DrawArc(SKCanvas canvas, Circle circle, Func<float> progress, float strokewidth, SKColor color, SKColor progressEndColor)
{
var progressValue = progress();
var angle = progressValue * 3.6f;
canvas.DrawArc(circle.Rect, 270, angle, false,
new SKPaint()
{
StrokeWidth = strokewidth,
Color = progressValue < 20f ? progressEndColor : color,
IsStroke = true,
IsAntialias = true
});
}
}
public class Circle
{
private readonly Func<SKImageInfo, SKPoint> _centerFunc;
public Circle(float redius, Func<SKImageInfo, SKPoint> centerFunc)
{
_centerFunc = centerFunc;
Redius = redius;
}
public SKPoint Center { get; set; }
public float Redius { get; set; }
public SKRect Rect => new SKRect(Center.X - Redius, Center.Y - Redius, Center.X + Redius, Center.Y + Redius);
public void CalculateCenter(SKImageInfo argsInfo)
{
Center = _centerFunc(argsInfo);
}
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Grid
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.DateTimePicker"
ColumnDefinitions="*,*">
<controls:ExtendedDatePicker
x:Name="_datePicker"
Grid.Column="0"
NullableDate="{Binding Date, Mode=TwoWay}"
Format="d"
AutomationProperties.IsInAccessibleTree="True" />
<controls:ExtendedTimePicker
x:Name="_timePicker"
Grid.Column="1"
NullableTime="{Binding Time, Mode=TwoWay}"
Format="t"
AutomationProperties.IsInAccessibleTree="True" />
</Grid>

View File

@@ -0,0 +1,34 @@
using System.Runtime.CompilerServices;
using Xamarin.CommunityToolkit.UI.Views;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class DateTimePicker : Grid
{
public DateTimePicker()
{
InitializeComponent();
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(BindingContext)
&&
BindingContext is DateTimeViewModel dateTimeViewModel)
{
AutomationProperties.SetName(_datePicker, dateTimeViewModel.DateName);
AutomationProperties.SetName(_timePicker, dateTimeViewModel.TimeName);
_datePicker.PlaceHolder = dateTimeViewModel.DatePlaceholder;
_timePicker.PlaceHolder = dateTimeViewModel.TimePlaceholder;
}
}
}
public class LazyDateTimePicker : LazyView<DateTimePicker>
{
}
}

View File

@@ -0,0 +1,70 @@
using System;
using Bit.Core.Utilities;
namespace Bit.App.Controls
{
public class DateTimeViewModel : ExtendedViewModel
{
DateTime? _date;
TimeSpan? _time;
public DateTimeViewModel(string dateName, string timeName)
{
DateName = dateName;
TimeName = timeName;
}
public Action<DateTime?> OnDateChanged { get; set; }
public Action<TimeSpan?> OnTimeChanged { get; set; }
public DateTime? Date
{
get => _date;
set
{
if (SetProperty(ref _date, value))
{
OnDateChanged?.Invoke(value);
}
}
}
public TimeSpan? Time
{
get => _time;
set
{
if (SetProperty(ref _time, value))
{
OnTimeChanged?.Invoke(value);
}
}
}
public string DateName { get; }
public string TimeName { get; }
public string DatePlaceholder { get; set; }
public string TimePlaceholder { get; set; }
public DateTime? DateTime
{
get
{
if (Date.HasValue)
{
if (Time.HasValue)
{
return Date.Value.Add(Time.Value);
}
return Date;
}
return null;
}
set
{
Date = value?.Date;
Time = value?.Date.TimeOfDay;
}
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
using Xamarin.Forms;
namespace Bit.App.Effects
{
public class NoEmojiKeyboardEffect : RoutingEffect
{
public NoEmojiKeyboardEffect()
: base("Bitwarden.NoEmojiKeyboardEffect")
{ }
}
}

View File

@@ -72,8 +72,7 @@ namespace Bit.App.Pages
}
public string ShowPasswordIcon => ShowPassword ? "" : "";
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.VisibilityTogglePasswordIsVisibleActivateToHide :
AppResources.VisibilityTogglePasswordIsNotVisibleActivateToHide;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string MasterPassword { get; set; }
public string ConfirmMasterPassword { get; set; }
public string Hint { get; set; }

View File

@@ -14,7 +14,7 @@
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n Save}" Clicked="Submit_Clicked" />
<ToolbarItem Text="{u:I18n Save}" Command="{Binding SubmitCommand}" />
</ContentPage.ToolbarItems>
<ScrollView>

View File

@@ -36,14 +36,6 @@ namespace Bit.App.Pages
};
}
private async void Submit_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.SubmitAsync();
}
}
private async Task SubmitSuccessAsync()
{
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);

View File

@@ -1,15 +1,17 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
using Xamarin.CommunityToolkit.ObjectModel;
namespace Bit.App.Pages
{
public class EnvironmentPageViewModel : BaseViewModel
{
private readonly IEnvironmentService _environmentService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
public EnvironmentPageViewModel()
{
@@ -22,10 +24,10 @@ namespace Bit.App.Pages
IdentityUrl = _environmentService.IdentityUrl;
IconsUrl = _environmentService.IconsUrl;
NotificationsUrls = _environmentService.NotificationsUrl;
SubmitCommand = new Command(async () => await SubmitAsync());
SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
}
public Command SubmitCommand { get; }
public ICommand SubmitCommand { get; }
public string BaseUrl { get; set; }
public string ApiUrl { get; set; }
public string IdentityUrl { get; set; }
@@ -37,6 +39,12 @@ namespace Bit.App.Pages
public async Task SubmitAsync()
{
if (!ValidateUrls())
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok);
return;
}
var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
{
Base = BaseUrl,
@@ -57,5 +65,25 @@ namespace Bit.App.Pages
SubmitSuccessAction?.Invoke();
}
public bool ValidateUrls()
{
bool IsUrlValid(string url)
{
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute);
}
return IsUrlValid(BaseUrl)
&& IsUrlValid(ApiUrl)
&& IsUrlValid(IdentityUrl)
&& IsUrlValid(WebVaultUrl)
&& IsUrlValid(IconsUrl);
}
private void OnSubmitException(Exception ex)
{
_logger.Value.Exception(ex);
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
}
}

View File

@@ -14,7 +14,7 @@
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
<ToolbarItem Text="{u:I18n Submit}" Command="{Binding SubmitCommand}" />
</ContentPage.ToolbarItems>
<ScrollView>

View File

@@ -1,5 +1,4 @@
using System;
using Xamarin.Forms;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -24,14 +23,6 @@ namespace Bit.App.Pages
RequestFocus(_email);
}
private async void Submit_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.SubmitAsync();
}
}
private async void Close_Clicked(object sender, System.EventArgs e)
{
if (DoOnce())

View File

@@ -1,10 +1,11 @@
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Xamarin.Forms;
using Xamarin.CommunityToolkit.ObjectModel;
namespace Bit.App.Pages
{
@@ -13,18 +14,26 @@ namespace Bit.App.Pages
private readonly IDeviceActionService _deviceActionService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IApiService _apiService;
private readonly ILogger _logger;
public HintPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
_logger = ServiceContainer.Resolve<ILogger>();
PageTitle = AppResources.PasswordHint;
SubmitCommand = new Command(async () => await SubmitAsync());
SubmitCommand = new AsyncCommand(SubmitAsync,
onException: ex =>
{
_logger.Exception(ex);
_deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
},
allowsMultipleExecutions: false);
}
public Command SubmitCommand { get; }
public ICommand SubmitCommand { get; }
public string Email { get; set; }
public async Task SubmitAsync()
@@ -37,14 +46,14 @@ namespace Bit.App.Pages
}
if (string.IsNullOrWhiteSpace(Email))
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred,
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
AppResources.Ok);
return;
}
if (!Email.Contains("@"))
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
return;
}
@@ -54,7 +63,7 @@ namespace Bit.App.Pages
await _apiService.PostPasswordHintAsync(
new Core.Models.Request.PasswordHintRequest { Email = Email });
await _deviceActionService.HideLoadingAsync();
await Page.DisplayAlert(null, AppResources.PasswordHintAlert, AppResources.Ok);
await _deviceActionService.DisplayAlertAsync(null, AppResources.PasswordHintAlert, AppResources.Ok);
await Page.Navigation.PopModalAsync();
}
catch (ApiException e)

View File

@@ -54,7 +54,7 @@ namespace Bit.App.Pages
{
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
}
_broadcasterService.Subscribe(nameof(HomePage), async (message) =>
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
{
if (message.Command == "updatedTheme")
{

View File

@@ -80,7 +80,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
</Grid>
<Grid
x:Name="_passwordGrid"
@@ -119,7 +120,7 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
<StackLayout

View File

@@ -25,8 +25,6 @@ namespace Bit.App.Pages
_vm = BindingContext as LockPageViewModel;
_vm.Page = this;
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
MasterPasswordEntry = _masterPassword;
PinEntry = _pin;
if (Device.RuntimePlatform == Device.iOS)
{
@@ -38,8 +36,17 @@ namespace Bit.App.Pages
}
}
public Entry MasterPasswordEntry { get; set; }
public Entry PinEntry { get; set; }
public Entry SecretEntry
{
get
{
if (_vm?.PinLock ?? false)
{
return _pin;
}
return _masterPassword;
}
}
public async Task PromptBiometricAfterResumeAsync()
{
@@ -70,16 +77,12 @@ namespace Bit.App.Pages
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
await _vm.InitAsync();
_vm.FocusSecretEntry += PerformFocusSecretEntry;
if (!_vm.BiometricLock)
{
if (_vm.PinLock)
{
RequestFocus(PinEntry);
}
else
{
RequestFocus(MasterPasswordEntry);
}
RequestFocus(SecretEntry);
}
else
{
@@ -99,6 +102,18 @@ namespace Bit.App.Pages
}
}
private void PerformFocusSecretEntry(int? cursorPosition)
{
Device.BeginInvokeOnMainThread(() =>
{
SecretEntry.Focus();
if (cursorPosition.HasValue)
{
SecretEntry.CursorPosition = cursorPosition.Value;
}
});
}
protected override bool OnBackButtonPressed()
{
if (_accountListOverlay.IsVisible)

View File

@@ -10,6 +10,7 @@ using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.Helpers;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -27,6 +28,7 @@ namespace Bit.App.Pages
private readonly IBiometricService _biometricService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly ILogger _logger;
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
private string _email;
private bool _showPassword;
@@ -129,11 +131,15 @@ namespace Bit.App.Pages
public Command SubmitCommand { get; }
public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.VisibilityTogglePasswordIsVisibleActivateToHide :
AppResources.VisibilityTogglePasswordIsNotVisibleActivateToHide;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string MasterPassword { get; set; }
public string Pin { get; set; }
public Action UnlockedAction { get; set; }
public event Action<int?> FocusSecretEntry
{
add => _secretEntryFocusWeakEventManager.AddEventHandler(value);
remove => _secretEntryFocusWeakEventManager.RemoveEventHandler(value);
}
public async Task InitAsync()
{
@@ -347,11 +353,8 @@ namespace Bit.App.Pages
public void TogglePassword()
{
ShowPassword = !ShowPassword;
var page = (Page as LockPage);
var entry = PinLock ? page.PinEntry : page.MasterPasswordEntry;
var str = PinLock ? Pin : MasterPassword;
entry.Focus();
entry.CursorPosition = String.IsNullOrEmpty(str) ? 0 : str.Length;
var secret = PinLock ? Pin : MasterPassword;
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
}
public async Task PromptBiometricAsync()
@@ -362,18 +365,8 @@ namespace Bit.App.Pages
return;
}
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
PinLock ? AppResources.PIN : AppResources.MasterPassword, () =>
{
var page = Page as LockPage;
if (PinLock)
{
page.PinEntry.Focus();
}
else
{
page.MasterPasswordEntry.Focus();
}
});
PinLock ? AppResources.PIN : AppResources.MasterPassword,
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
await _stateService.SetBiometricLockedAsync(!success);
if (success)
{

View File

@@ -55,6 +55,7 @@
<Entry
x:Name="_email"
Text="{Binding Email}"
IsEnabled="{Binding IsEmailEnabled}"
Keyboard="Email"
StyleClass="box-value">
<VisualStateManager.VisualStateGroups>
@@ -101,7 +102,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}"/>
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
</Grid>
</StackLayout>
<StackLayout Padding="10, 0">

View File

@@ -1,8 +1,8 @@
using System;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -15,6 +15,8 @@ namespace Bit.App.Pages
private bool _inputFocused;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
public LoginPage(string email = null, AppOptions appOptions = null)
{
_appOptions = appOptions;
@@ -30,11 +32,10 @@ namespace Bit.App.Pages
await _accountListOverlay.HideAsync();
await Navigation.PopModalAsync();
};
if (!string.IsNullOrWhiteSpace(email))
{
_email.IsEnabled = false;
}
else
_vm.IsEmailEnabled = string.IsNullOrWhiteSpace(email);
_vm.IsIosExtension = _appOptions?.IosExtension ?? false;
if (_vm.IsEmailEnabled)
{
_vm.ShowCancelButton = true;
}
@@ -53,7 +54,7 @@ namespace Bit.App.Pages
ToolbarItems.Add(_getPasswordHint);
}
if (Device.RuntimePlatform == Device.Android && !_email.IsEnabled)
if (Device.RuntimePlatform == Device.Android && !_vm.IsEmailEnabled)
{
ToolbarItems.Add(_removeAccount);
}
@@ -110,7 +111,7 @@ namespace Bit.App.Pages
{
if (DoOnce())
{
await _vm.LogInAsync(true, _email.IsEnabled);
await _vm.LogInAsync(true, _vm.IsEmailEnabled);
}
}
@@ -139,26 +140,16 @@ namespace Bit.App.Pages
}
}
private async void More_Clicked(object sender, System.EventArgs e)
private async void More_Clicked(object sender, EventArgs e)
{
await _accountListOverlay.HideAsync();
if (!DoOnce())
try
{
return;
await _accountListOverlay.HideAsync();
_vm.MoreCommand.Execute(null);
}
var buttons = _email.IsEnabled ? new[] { AppResources.GetPasswordHint }
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
var selection = await DisplayActionSheet(AppResources.Options,
AppResources.Cancel, null, buttons);
if (selection == AppResources.GetPasswordHint)
catch (Exception ex)
{
await Navigation.PushModalAsync(new NavigationPage(new HintPage()));
}
else if (selection == AppResources.RemoveAccount)
{
await _vm.RemoveAccountAsync();
_logger.Value.Exception(ex);
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
@@ -8,6 +9,7 @@ using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -28,6 +30,7 @@ namespace Bit.App.Pages
private bool _showCancelButton;
private string _email;
private string _masterPassword;
private bool _isEmailEnabled;
public LoginPageViewModel()
{
@@ -44,6 +47,7 @@ namespace Bit.App.Pages
PageTitle = AppResources.Bitwarden;
TogglePasswordCommand = new Command(TogglePassword);
LogInCommand = new Command(async () => await LogInAsync());
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
{
@@ -81,13 +85,21 @@ namespace Bit.App.Pages
set => SetProperty(ref _masterPassword, value);
}
public bool IsEmailEnabled
{
get => _isEmailEnabled;
set => SetProperty(ref _isEmailEnabled, value);
}
public bool IsIosExtension { get; set; }
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public Command LogInCommand { get; }
public Command TogglePasswordCommand { get; }
public ICommand MoreCommand { get; internal set; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.VisibilityTogglePasswordIsVisibleActivateToHide :
AppResources.VisibilityTogglePasswordIsNotVisibleActivateToHide;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public Action StartTwoFactorAction { get; set; }
public Action LogInSuccessAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
@@ -202,6 +214,28 @@ namespace Bit.App.Pages
}
}
private async Task MoreAsync()
{
var buttons = IsEmailEnabled
? new[] { AppResources.GetPasswordHint }
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, buttons);
if (selection == AppResources.GetPasswordHint)
{
var hintNavigationPage = new NavigationPage(new HintPage());
if (IsIosExtension)
{
ThemeManager.ApplyResourcesTo(hintNavigationPage);
}
await Page.Navigation.PushModalAsync(hintNavigationPage);
}
else if (selection == AppResources.RemoveAccount)
{
await RemoveAccountAsync();
}
}
public void TogglePassword()
{
ShowPassword = !ShowPassword;

View File

@@ -69,12 +69,12 @@ namespace Bit.App.Pages
}
}
private async void LogIn_Clicked(object sender, EventArgs e)
private void LogIn_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
CopyAppOptions();
await _vm.LogInAsync();
_vm.LogInCommand.Execute(null);
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
@@ -8,13 +9,15 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
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 LoginSsoPageViewModel : BaseViewModel
{
private const string REDIRECT_URI = "bitwarden://sso-callback";
private readonly IDeviceActionService _deviceActionService;
private readonly IAuthService _authService;
private readonly ISyncService _syncService;
@@ -23,6 +26,7 @@ namespace Bit.App.Pages
private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IStateService _stateService;
private readonly ILogger _logger;
private string _orgIdentifier;
@@ -37,9 +41,11 @@ namespace Bit.App.Pages
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>("cryptoFunctionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
PageTitle = AppResources.Bitwarden;
LogInCommand = new Command(async () => await LogInAsync());
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
}
public string OrgIdentifier
@@ -48,7 +54,7 @@ namespace Bit.App.Pages
set => SetProperty(ref _orgIdentifier, value);
}
public Command LogInCommand { get; }
public ICommand LogInCommand { get; }
public Action StartTwoFactorAction { get; set; }
public Action StartSetPasswordAction { get; set; }
public Action SsoAuthSuccessAction { get; set; }
@@ -65,78 +71,91 @@ namespace Bit.App.Pages
public async Task LogInAsync()
{
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return;
}
if (string.IsNullOrWhiteSpace(OrgIdentifier))
{
await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier),
AppResources.AnErrorHasOccurred,
AppResources.Ok);
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
try
{
await _apiService.PreValidateSso(OrgIdentifier);
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return;
}
if (string.IsNullOrWhiteSpace(OrgIdentifier))
{
await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier),
AppResources.AnErrorHasOccurred,
AppResources.Ok);
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
var response = await _apiService.PreValidateSso(OrgIdentifier);
if (string.IsNullOrWhiteSpace(response?.Token))
{
_logger.Error(response is null ? "Login SSO Error: response is null" : "Login SSO Error: response.Token is null or whitespace");
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError);
return;
}
var ssoToken = response.Token;
var passwordOptions = new PasswordGenerationOptions(true);
passwordOptions.Length = 64;
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash);
var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
var url = _apiService.IdentityBaseUrl + "/connect/authorize?" +
"client_id=" + _platformUtilsService.GetClientType().GetString() + "&" +
"redirect_uri=" + Uri.EscapeDataString(REDIRECT_URI) + "&" +
"response_type=code&scope=api%20offline_access&" +
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
"code_challenge_method=S256&response_mode=query&" +
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier) + "&" +
"ssoToken=" + Uri.EscapeDataString(ssoToken);
WebAuthenticatorResult authResult = null;
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
new Uri(REDIRECT_URI));
var code = GetResultCode(authResult, state);
if (!string.IsNullOrEmpty(code))
{
await LogIn(code, codeVerifier, OrgIdentifier);
}
else
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
AppResources.AnErrorHasOccurred);
}
}
catch (ApiException e)
{
_logger.Exception(e);
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(
(e?.Error != null ? e.Error.GetSingleMessage() : AppResources.LoginSsoError),
await _platformUtilsService.ShowDialogAsync(e?.Error?.GetSingleMessage() ?? AppResources.LoginSsoError,
AppResources.AnErrorHasOccurred);
return;
}
var passwordOptions = new PasswordGenerationOptions(true);
passwordOptions.Length = 64;
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash);
var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
var redirectUri = "bitwarden://sso-callback";
var url = _apiService.IdentityBaseUrl + "/connect/authorize?" +
"client_id=" + _platformUtilsService.GetClientType().GetString() + "&" +
"redirect_uri=" + Uri.EscapeDataString(redirectUri) + "&" +
"response_type=code&scope=api%20offline_access&" +
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
"code_challenge_method=S256&response_mode=query&" +
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier);
WebAuthenticatorResult authResult = null;
try
{
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
new Uri(redirectUri));
}
catch (TaskCanceledException)
{
// user canceled
await _deviceActionService.HideLoadingAsync();
return;
}
var code = GetResultCode(authResult, state);
if (!string.IsNullOrEmpty(code))
{
await LogIn(code, codeVerifier, redirectUri, OrgIdentifier);
}
else
catch (Exception ex)
{
_logger.Exception(ex);
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
AppResources.AnErrorHasOccurred);
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
}
}
@@ -155,11 +174,11 @@ namespace Bit.App.Pages
return code;
}
private async Task LogIn(string code, string codeVerifier, string redirectUri, string orgId)
private async Task LogIn(string code, string codeVerifier, string orgId)
{
try
{
var response = await _authService.LogInSsoAsync(code, codeVerifier, redirectUri, orgId);
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
await _deviceActionService.HideLoadingAsync();

View File

@@ -68,7 +68,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
</Grid>
<Label
Text="{u:I18n MasterPasswordDescription}"
@@ -106,7 +107,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}"/>
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
<StackLayout StyleClass="box-row">
<Label
@@ -130,7 +132,7 @@
IsToggled="{Binding AcceptPolicies}"
StyleClass="box-value"
HorizontalOptions="Start"
Margin="{Binding SwitchMargin}"/>
Margin="0, 0, 10, 0"/>
<Label StyleClass="box-footer-label"
HorizontalOptions="Fill">
<Label.FormattedText>

View File

@@ -61,21 +61,12 @@ namespace Bit.App.Pages
get => _acceptPolicies;
set => SetProperty(ref _acceptPolicies, value);
}
public Thickness SwitchMargin
{
get => Device.RuntimePlatform == Device.Android
? new Thickness(0, 0, 0, 0)
: new Thickness(0, 0, 10, 0);
}
public bool ShowTerms { get; set; }
public Command SubmitCommand { get; }
public Command TogglePasswordCommand { get; }
public Command ToggleConfirmPasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.VisibilityTogglePasswordIsVisibleActivateToHide :
AppResources.VisibilityTogglePasswordIsNotVisibleActivateToHide;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string Name { get; set; }
public string Email { get; set; }
public string MasterPassword { get; set; }

View File

@@ -107,7 +107,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}"/>
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
<Label
Text="{u:I18n MasterPasswordDescription}"
@@ -145,7 +146,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}"/>
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
<StackLayout StyleClass="box-row">
<Label

View File

@@ -90,8 +90,7 @@ namespace Bit.App.Pages
public Command TogglePasswordCommand { get; }
public Command ToggleConfirmPasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.VisibilityTogglePasswordIsVisibleActivateToHide :
AppResources.VisibilityTogglePasswordIsNotVisibleActivateToHide;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string MasterPassword { get; set; }
public string ConfirmMasterPassword { get; set; }
public string Hint { get; set; }
@@ -220,7 +219,8 @@ namespace Bit.App.Pages
// Request
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
{
ResetPasswordKey = encryptedKey.EncryptedString
ResetPasswordKey = encryptedKey.EncryptedString,
MasterPasswordHash = masterPasswordHash,
};
var userId = await _stateService.GetActiveUserIdAsync();
// Enroll user

View File

@@ -17,15 +17,19 @@
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
x:Name="_cancelItem" />
</ContentPage.ToolbarItems>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:IsNullConverter x:Key="isNull" />
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
x:Name="_moreItem" x:Key="moreItem"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
<ToolbarItem
x:Name="_moreItem"
x:Key="moreItem"
Icon="more_vert.png"
Order="Primary"
Command="{Binding MoreCommand}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
<ToolbarItem Text="{u:I18n UseAnotherTwoStepMethod}"
Clicked="Methods_Clicked"
Order="Secondary"

View File

@@ -2,7 +2,6 @@
using System.Threading.Tasks;
using Bit.App.Controls;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
@@ -137,21 +136,6 @@ namespace Bit.App.Pages
}
}
private async void More_Clicked(object sender, EventArgs e)
{
if (!DoOnce())
{
return;
}
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel, null, AppResources.UseAnotherTwoStepMethod);
if (selection == AppResources.UseAnotherTwoStepMethod)
{
await _vm.AnotherMethodAsync();
}
}
private async void ResendEmail_Clicked(object sender, EventArgs e)
{
if (DoOnce())

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
@@ -12,6 +13,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Request;
using Bit.Core.Utilities;
using Newtonsoft.Json;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
@@ -30,6 +32,7 @@ namespace Bit.App.Pages
private readonly IStateService _stateService;
private readonly II18nService _i18nService;
private readonly IAppIdService _appIdService;
private readonly ILogger _logger;
private TwoFactorProviderType? _selectedProviderType;
private string _totpInstruction;
@@ -51,9 +54,11 @@ namespace Bit.App.Pages
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_logger = ServiceContainer.Resolve<ILogger>();
PageTitle = AppResources.TwoStepLogin;
SubmitCommand = new Command(async () => await SubmitAsync());
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
}
public string TotpInstruction
@@ -111,6 +116,7 @@ namespace Bit.App.Pages
});
}
public Command SubmitCommand { get; }
public ICommand MoreCommand { get; }
public Action TwoFactorAuthSuccessAction { get; set; }
public Action StartSetPasswordAction { get; set; }
public Action CloseAction { get; set; }
@@ -337,6 +343,15 @@ namespace Bit.App.Pages
}
}
private async Task MoreAsync()
{
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, AppResources.UseAnotherTwoStepMethod);
if (selection == AppResources.UseAnotherTwoStepMethod)
{
await AnotherMethodAsync();
}
}
public async Task AnotherMethodAsync()
{
var supportedProviders = _authService.GetSupportedTwoFactorProviders();

View File

@@ -105,7 +105,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}"/>
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
</StackLayout>
<StackLayout StyleClass="box">
@@ -140,7 +141,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
<StackLayout StyleClass="box-row">
<Label

View File

@@ -6,6 +6,7 @@
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:effects="clr-namespace:Bit.App.Effects"
x:DataType="pages:GeneratorPageViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
@@ -128,7 +129,11 @@
Text="{Binding WordSeparator}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
StyleClass="box-value" />
StyleClass="box-value">
<Entry.Effects>
<effects:NoEmojiKeyboardEffect />
</Entry.Effects>
</Entry>
</StackLayout>
<StackLayout StyleClass="box-row, box-row-switch">
<Label

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"
@@ -303,14 +303,14 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:ExtendedDatePicker
NullableDate="{Binding DeletionDate, Mode=TwoWay}"
NullableDate="{Binding DeletionDateTimeViewModel.Date, Mode=TwoWay}"
Format="d"
IsEnabled="{Binding SendEnabled}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionDate}"
Grid.Column="0" />
<controls:ExtendedTimePicker
NullableTime="{Binding DeletionTime, Mode=TwoWay}"
NullableTime="{Binding DeletionDateTimeViewModel.Time, Mode=TwoWay}"
Format="t"
IsEnabled="{Binding SendEnabled}"
AutomationProperties.IsInAccessibleTree="True"
@@ -343,7 +343,7 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:ExtendedDatePicker
NullableDate="{Binding ExpirationDate, Mode=TwoWay}"
NullableDate="{Binding ExpirationDateTimeViewModel.Date, Mode=TwoWay}"
PlaceHolder="mm/dd/yyyy"
Format="d"
IsEnabled="{Binding SendEnabled}"
@@ -351,7 +351,7 @@
AutomationProperties.Name="{u:I18n ExpirationDate}"
Grid.Column="0" />
<controls:ExtendedTimePicker
NullableTime="{Binding ExpirationTime, Mode=TwoWay}"
NullableTime="{Binding ExpirationDateTimeViewModel.Time, Mode=TwoWay}"
PlaceHolder="--:-- --"
Format="t"
IsEnabled="{Binding SendEnabled}"
@@ -444,7 +444,8 @@
Command="{Binding TogglePasswordCommand}"
Margin="10,0,0,0"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}"/>
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</StackLayout>
<Label
Text="{u:I18n PasswordInfo}"

View File

@@ -23,7 +23,6 @@ namespace Bit.App.Pages
private AppOptions _appOptions;
private SendAddEditPageViewModel _vm;
public Action OnClose { get; set; }
public Action AfterSubmit { get; set; }
public SendAddEditPage(
@@ -136,14 +135,7 @@ namespace Bit.App.Pages
private async Task CloseAsync()
{
if (OnClose is null)
{
await Navigation.PopModalAsync();
}
else
{
OnClose();
}
await Navigation.PopModalAsync();
}
protected override bool OnBackButtonPressed()

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
@@ -23,7 +24,7 @@ namespace Bit.App.Pages
private readonly IStateService _stateService;
private readonly ISendService _sendService;
private readonly ILogger _logger;
private bool _sendEnabled;
private bool _sendEnabled = true;
private bool _canAccessPremium;
private bool _emailVerified;
private SendView _send;
@@ -33,11 +34,7 @@ namespace Bit.App.Pages
private int _deletionDateTypeSelectedIndex;
private int _expirationDateTypeSelectedIndex;
private DateTime _simpleDeletionDateTime;
private DateTime _deletionDate;
private TimeSpan _deletionTime;
private DateTime? _simpleExpirationDateTime;
private DateTime? _expirationDate;
private TimeSpan? _expirationTime;
private bool _isOverridingPickers;
private int? _maxAccessCount;
private string[] _additionalSendProperties = new[]
@@ -89,8 +86,34 @@ namespace Bit.App.Pages
new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays),
new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom),
};
DeletionDateTimeViewModel = new DateTimeViewModel(AppResources.DeletionDate, AppResources.DeletionTime);
ExpirationDateTimeViewModel = new DateTimeViewModel(AppResources.ExpirationDate, AppResources.ExpirationTime)
{
OnDateChanged = date =>
{
if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Time.HasValue)
{
// auto-set time to current time upon setting date
ExpirationDateTimeViewModel.Time = DateTimeNow().TimeOfDay;
}
},
OnTimeChanged = time =>
{
if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Date.HasValue)
{
// auto-set date to current date upon setting time
ExpirationDateTimeViewModel.Date = DateTime.Today;
}
},
DatePlaceholder = "mm/dd/yyyy",
TimePlaceholder = "--:-- --"
};
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger);
}
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public Command TogglePasswordCommand { get; set; }
public Command ToggleOptionsCommand { get; set; }
public string SendId { get; set; }
@@ -126,23 +149,14 @@ namespace Bit.App.Pages
}
}
}
public DateTime DeletionDate
{
get => _deletionDate;
set => SetProperty(ref _deletionDate, value);
}
public TimeSpan DeletionTime
{
get => _deletionTime;
set => SetProperty(ref _deletionTime, value);
}
public bool ShowOptions
{
get => _showOptions;
set => SetProperty(ref _showOptions, value,
additionalPropertyNames: new[]
{
nameof(OptionsAccessilibityText)
nameof(OptionsAccessilibityText),
nameof(OptionsShowHideIcon)
});
}
public int ExpirationDateTypeSelectedIndex
@@ -156,28 +170,7 @@ namespace Bit.App.Pages
}
}
}
public DateTime? ExpirationDate
{
get => _expirationDate;
set
{
if (SetProperty(ref _expirationDate, value))
{
ExpirationDateChanged();
}
}
}
public TimeSpan? ExpirationTime
{
get => _expirationTime;
set
{
if (SetProperty(ref _expirationTime, value))
{
ExpirationTimeChanged();
}
}
}
public int? MaxAccessCount
{
get => _maxAccessCount;
@@ -205,7 +198,7 @@ namespace Bit.App.Pages
}
public string FileName
{
get => _fileName;
get => _fileName ?? AppResources.NoFileChosen;
set
{
if (SetProperty(ref _fileName, value))
@@ -240,11 +233,13 @@ namespace Bit.App.Pages
public bool IsFile => Send?.Type == SendType.File;
public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6;
public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7;
public DateTimeViewModel DeletionDateTimeViewModel { get; }
public DateTimeViewModel ExpirationDateTimeViewModel { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.VisibilityTogglePasswordIsVisibleActivateToHide :
AppResources.VisibilityTogglePasswordIsNotVisibleActivateToHide;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string FileTypeAccessibilityLabel => IsFile ? AppResources.FileTypeIsSelected : AppResources.FileTypeIsNotSelected;
public string TextTypeAccessibilityLabel => IsText ? AppResources.TextTypeIsSelected : AppResources.TextTypeIsNotSelected;
public string OptionsShowHideIcon => ShowOptions ? BitwardenIcons.ChevronUp : BitwardenIcons.AngleDown;
public async Task InitAsync()
{
@@ -269,10 +264,8 @@ namespace Bit.App.Pages
return false;
}
Send = await send.DecryptAsync();
DeletionDate = Send.DeletionDate.ToLocalTime();
DeletionTime = DeletionDate.TimeOfDay;
ExpirationDate = Send.ExpirationDate?.ToLocalTime();
ExpirationTime = ExpirationDate?.TimeOfDay;
DeletionDateTimeViewModel.DateTime = Send.DeletionDate.ToLocalTime();
ExpirationDateTimeViewModel.DateTime = Send.ExpirationDate?.ToLocalTime();
}
else
{
@@ -281,8 +274,7 @@ namespace Bit.App.Pages
{
Type = Type.GetValueOrDefault(defaultType),
};
_deletionDate = DateTimeNow().AddDays(7);
_deletionTime = DeletionDate.TimeOfDay;
DeletionDateTimeViewModel.DateTime = DateTimeNow().AddDays(7);
DeletionDateTypeSelectedIndex = 4;
ExpirationDateTypeSelectedIndex = 0;
}
@@ -306,23 +298,22 @@ namespace Bit.App.Pages
public void ClearExpirationDate()
{
_isOverridingPickers = true;
ExpirationDate = null;
ExpirationTime = null;
ExpirationDateTimeViewModel.DateTime = null;
_isOverridingPickers = false;
}
private void UpdateSendData()
{
// filename
if (Send.File != null && FileName != null)
if (Send.File != null && _fileName != null)
{
Send.File.FileName = FileName;
Send.File.FileName = _fileName;
}
// deletion date
if (ShowDeletionCustomPickers)
{
Send.DeletionDate = DeletionDate.Date.Add(DeletionTime).ToUniversalTime();
Send.DeletionDate = DeletionDateTimeViewModel.DateTime.Value.ToUniversalTime();
}
else
{
@@ -330,9 +321,9 @@ namespace Bit.App.Pages
}
// expiration date
if (ShowExpirationCustomPickers && ExpirationDate.HasValue && ExpirationTime.HasValue)
if (ShowExpirationCustomPickers && ExpirationDateTimeViewModel.DateTime.HasValue)
{
Send.ExpirationDate = ExpirationDate.Value.Date.Add(ExpirationTime.Value).ToUniversalTime();
Send.ExpirationDate = ExpirationDateTimeViewModel.DateTime.Value.ToUniversalTime();
}
else if (_simpleExpirationDateTime.HasValue)
{
@@ -485,7 +476,7 @@ namespace Bit.App.Pages
return;
}
if (Page is SendAddEditPage sendPage && sendPage.OnClose != null)
if (Page is SendAddOnlyPage sendPage && sendPage.OnClose != null)
{
sendPage.OnClose();
return;
@@ -626,24 +617,6 @@ namespace Bit.App.Pages
}
}
private void ExpirationDateChanged()
{
if (!_isOverridingPickers && !ExpirationTime.HasValue)
{
// auto-set time to current time upon setting date
ExpirationTime = DateTimeNow().TimeOfDay;
}
}
private void ExpirationTimeChanged()
{
if (!_isOverridingPickers && !ExpirationDate.HasValue)
{
// auto-set date to current date upon setting time
ExpirationDate = DateTime.Today;
}
}
private void MaxAccessCountChanged()
{
Send.MaxAccessCount = _maxAccessCount;
@@ -667,5 +640,10 @@ namespace Bit.App.Pages
DateTimeKind.Local
);
}
internal void TriggerSendTextPropertyChanged()
{
Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(Send)));
}
}
}

View File

@@ -0,0 +1,183 @@
<?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:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
x:DataType="pages:SendAddEditPageViewModel"
x:Class="Bit.App.Pages.SendAddOnlyOptionsView">
<ContentView.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
</ResourceDictionary>
</ContentView.Resources>
<ContentView.Content>
<StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,10,0,0">
<Label
Text="{u:I18n DeletionDate}"
StyleClass="box-label" />
<Picker
x:Name="_deletionDateTypePicker"
ItemsSource="{Binding DeletionTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding DeletionDateTypeSelectedIndex}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
ItemDisplayBinding="{Binding Key}"
ios:Picker.UpdateMode="WhenFinished"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionTime}" />
<controls:LazyDateTimePicker
x:Name="_lazyDeletionDateTimePicker"
BindingContext="{Binding DeletionDateTimeViewModel}"
IsVisible="{Binding ShowDeletionCustomPickers}"
Margin="0,5,0,0" />
<Label
Text="{u:I18n DeletionDateInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout StyleClass="box-row" Margin="0,5,0,0">
<Label
Text="{u:I18n ExpirationDate}"
StyleClass="box-label" />
<Picker
x:Name="_expirationDateTypePicker"
ItemsSource="{Binding ExpirationTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding ExpirationDateTypeSelectedIndex}"
ItemDisplayBinding="{Binding Key}"
ios:Picker.UpdateMode="WhenFinished"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ExpirationTime}" />
<controls:LazyDateTimePicker
x:Name="_lazyExpirationDateTimePicker"
BindingContext="{Binding ExpirationDateTimeViewModel}"
IsVisible="{Binding ShowExpirationCustomPickers}"
Margin="0,5,0,0" />
<Label
Text="{u:I18n ExpirationDateInfo}"
StyleClass="box-footer-label"
HorizontalOptions="StartAndExpand"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,5,0,0">
<Label
Text="{u:I18n MaximumAccessCount}"
StyleClass="box-label" />
<StackLayout
StyleClass="box-row"
Orientation="Horizontal">
<Entry
Text="{Binding MaxAccessCount}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
Keyboard="Numeric"
MaxLength="9"
TextChanged="OnMaxAccessCountTextChanged"
HorizontalOptions="FillAndExpand" />
<controls:ExtendedStepper
x:Name="_maxAccessCountStepper"
Value="{Binding MaxAccessCount}"
Maximum="999999999"
IsEnabled="{Binding SendEnabled}"
Margin="10,0,0,0" />
</StackLayout>
<Label
Text="{u:I18n MaximumAccessCountInfo}"
StyleClass="box-footer-label" />
</StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,5,0,0">
<Label
Text="{u:I18n NewPassword}"
StyleClass="box-label" />
<StackLayout Orientation="Horizontal">
<Entry
Text="{Binding NewPassword}"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
HorizontalOptions="FillAndExpand" />
<controls:IconButton
IsEnabled="{Binding SendEnabled}"
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Margin="10,0,0,0"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</StackLayout>
<Label
Text="{u:I18n PasswordInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,5,0,0">
<Label
Text="{u:I18n Notes}"
StyleClass="box-label" />
<Editor
x:Name="_notesEditor"
AutoSize="TextChanges"
Text="{Binding Send.Notes}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
Margin="0,10,0,5"
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
<Editor.Effects>
<effects:ScrollEnabledEffect />
</Editor.Effects>
</Editor>
<BoxView
StyleClass="box-row-separator" />
<Label
Text="{u:I18n NotesInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row, box-row-switch"
Margin="0,5,0,0">
<Label
Text="{u:I18n HideEmail}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Send.HideEmail}"
IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row, box-row-switch"
Margin="0,5,0,0">
<Label
Text="{u:I18n DisableSend}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Send.Disabled}"
IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
</StackLayout>
</ContentView.Content>
</ContentView>

View File

@@ -0,0 +1,91 @@
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Bit.App.Behaviors;
using Xamarin.CommunityToolkit.UI.Views;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class SendAddOnlyOptionsView : ContentView
{
public SendAddOnlyOptionsView()
{
InitializeComponent();
}
private SendAddEditPageViewModel ViewModel => BindingContext as SendAddEditPageViewModel;
public void SetMainScrollView(ScrollView scrollView)
{
_notesEditor.Behaviors.Add(new EditorPreventAutoBottomScrollingOnFocusedBehavior { ParentScrollView = scrollView });
}
private void OnMaxAccessCountTextChanged(object sender, TextChangedEventArgs e)
{
if (ViewModel is null)
{
return;
}
if (string.IsNullOrWhiteSpace(e.NewTextValue))
{
ViewModel.MaxAccessCount = null;
_maxAccessCountStepper.Value = 0;
return;
}
// accept only digits
if (!int.TryParse(e.NewTextValue, out int _))
{
((Entry)sender).Text = e.OldTextValue;
}
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(BindingContext)
&&
ViewModel != null)
{
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
}
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (!_lazyDeletionDateTimePicker.IsLoaded
&&
e.PropertyName == nameof(SendAddEditPageViewModel.ShowDeletionCustomPickers)
&&
ViewModel.ShowDeletionCustomPickers)
{
_lazyDeletionDateTimePicker.LoadViewAsync();
}
if (!_lazyExpirationDateTimePicker.IsLoaded
&&
e.PropertyName == nameof(SendAddEditPageViewModel.ShowExpirationCustomPickers)
&&
ViewModel.ShowExpirationCustomPickers)
{
_lazyExpirationDateTimePicker.LoadViewAsync();
}
}
}
public class SendAddOnlyOptionsLazyView : LazyView<SendAddOnlyOptionsView>
{
public ScrollView MainScrollView { get; set; }
public override async ValueTask LoadViewAsync()
{
await base.LoadViewAsync();
if (Content is SendAddOnlyOptionsView optionsView)
{
optionsView.SetMainScrollView(MainScrollView);
}
}
}
}

View File

@@ -0,0 +1,190 @@
<?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.SendAddOnlyPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
xmlns:effects="clr-namespace:Bit.App.Effects"
x:DataType="pages:SendAddEditPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:SendAddEditPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<!--Order matters here or the avatar's image won't be updated correctly, check iOS CustomNavigationRenderer for more info-->
<controls:ExtendedToolbarItem
x:Name="_accountAvatar"
IconImageSource="{Binding AvatarImageSource}"
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
Order="Primary"
Priority="-2"
UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" />
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" />
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/>
</ContentPage.ToolbarItems>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
</ResourceDictionary>
</ContentPage.Resources>
<AbsoluteLayout>
<ScrollView
x:Name="_scrollView"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All">
<StackLayout x:Name="_mainContainer" StyleClass="box">
<Frame
IsVisible="{Binding SendEnabled, Converter={StaticResource inverseBool}}"
Padding="10"
Margin="0, 12, 0, 0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{u:I18n SendDisabledWarning}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
<Frame
IsVisible="{Binding SendOptionsPolicyInEffect}"
Padding="10"
Margin="0, 12, 0, 0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{u:I18n SendOptionsPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n Name}"
StyleClass="box-label" />
<Entry
x:Name="_nameEntry"
Text="{Binding Send.Name}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value" />
<Label
Text="{u:I18n NameInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row"
IsVisible="{Binding IsFile}">
<Label
Text="{u:I18n TypeFile}"
StyleClass="box-label" />
<StackLayout
StyleClass="box-row">
<Label
Text="{Binding FileName}"
LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
<Label
Margin="0, 5, 0, 0"
Text="{u:I18n MaxFileSize}"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
</StackLayout>
</StackLayout>
<StackLayout
StyleClass="box-row"
IsVisible="{Binding IsText}">
<Label
Text="{u:I18n TypeText}"
StyleClass="box-label" />
<Editor
x:Name="_textEditor"
AutoSize="TextChanges"
Text="{Binding Send.Text.Text}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
Margin="{Binding EditorMargins}"
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
<Editor.Behaviors>
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
</Editor.Behaviors>
<Editor.Effects>
<effects:ScrollEnabledEffect />
</Editor.Effects>
</Editor>
<BoxView
StyleClass="box-row-separator" />
<Label
Text="{u:I18n TypeTextInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,10" />
<StackLayout
StyleClass="box-row, box-row-switch"
Margin="0,10,0,0">
<Label
Text="{u:I18n HideTextByDefault}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Send.Text.Hidden}"
IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End"
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"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding OptionsAccessilibityText}">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="OptionsHeader_Tapped" />
</StackLayout.GestureRecognizers>
<Label
Text="{u:I18n Options}"
TextColor="{DynamicResource PrimaryColor}"
Margin="0,0,5,0"
AutomationProperties.IsInAccessibleTree="False"/>
<controls:IconLabel
Text="{Binding OptionsShowHideIcon}"
TextColor="{DynamicResource PrimaryColor}"
AutomationProperties.IsInAccessibleTree="False"/>
</StackLayout>
<pages:SendAddOnlyOptionsLazyView x:Name="_lazyOptionsView" IsVisible="{Binding ShowOptions}" />
</StackLayout>
</ScrollView>
<controls:AccountSwitchingOverlayView
x:Name="_accountListOverlay"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All"
LongPressAccountEnabled="False"
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
</AbsoluteLayout>
</pages:BaseContentPage>

View File

@@ -0,0 +1,178 @@
using System;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
/// <summary>
/// This is a version of <see cref="SendAddEditPage"/> that is reduced for adding only and adapted
/// for performance for iOS Share extension.
/// </summary>
/// <remarks>
/// This should NOT be used in Android.
/// </remarks>
public partial class SendAddOnlyPage : BaseContentPage
{
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private AppOptions _appOptions;
private SendAddEditPageViewModel _vm;
public Action OnClose { get; set; }
public Action AfterSubmit { get; set; }
public SendAddOnlyPage(
AppOptions appOptions = null,
string sendId = null,
SendType? type = null)
{
if (appOptions?.IosExtension != true)
{
throw new InvalidOperationException(nameof(SendAddOnlyPage) + " is only prepared to be used in iOS share extension");
}
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_appOptions = appOptions;
InitializeComponent();
_vm = BindingContext as SendAddEditPageViewModel;
_vm.Page = this;
_vm.SendId = sendId;
_vm.Type = appOptions?.CreateSend?.Item1 ?? type;
if (_vm.IsText)
{
_nameEntry.ReturnType = ReturnType.Next;
_nameEntry.ReturnCommand = new Command(() => _textEditor.Focus());
}
}
protected override async void OnAppearing()
{
base.OnAppearing();
try
{
if (!await AppHelpers.IsVaultTimeoutImmediateAsync())
{
await _vaultTimeoutService.CheckVaultTimeoutAsync();
}
if (await _vaultTimeoutService.IsLockedAsync())
{
return;
}
await _vm.InitAsync();
if (!await _vm.LoadAsync())
{
await CloseAsync();
return;
}
_accountAvatar?.OnAppearing();
await Device.InvokeOnMainThreadAsync(async () => _vm.AvatarImageSource = await GetAvatarImageSourceAsync());
await HandleCreateRequest();
if (string.IsNullOrWhiteSpace(_vm.Send?.Name))
{
RequestFocus(_nameEntry);
}
AdjustToolbar();
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
await CloseAsync();
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_accountAvatar?.OnDisappearing();
}
private async Task CloseAsync()
{
if (OnClose is null)
{
await Navigation.PopModalAsync();
}
else
{
OnClose();
}
}
private async void Save_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
var submitted = await _vm.SubmitAsync();
if (submitted)
{
AfterSubmit?.Invoke();
}
}
}
private async void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await CloseAsync();
}
}
private void AdjustToolbar()
{
_saveItem.IsEnabled = _vm.SendEnabled;
}
private Task HandleCreateRequest()
{
if (_appOptions?.CreateSend == null)
{
return Task.CompletedTask;
}
_vm.IsAddFromShare = true;
_vm.CopyInsteadOfShareAfterSaving = _appOptions.CopyInsteadOfShareAfterSaving;
var name = _appOptions.CreateSend.Item2;
_vm.Send.Name = name;
var type = _appOptions.CreateSend.Item1;
if (type == SendType.File)
{
_vm.FileData = _appOptions.CreateSend.Item3;
_vm.FileName = name;
}
else
{
var text = _appOptions.CreateSend.Item4;
_vm.Send.Text.Text = text;
_vm.TriggerSendTextPropertyChanged();
}
_appOptions.CreateSend = null;
return Task.CompletedTask;
}
void OptionsHeader_Tapped(object sender, EventArgs e)
{
_vm.ToggleOptionsCommand.Execute(null);
if (!_lazyOptionsView.IsLoaded)
{
_lazyOptionsView.MainScrollView = _scrollView;
_lazyOptionsView.LoadViewAsync();
}
}
}
}

View File

@@ -5,6 +5,7 @@ using Bit.App.Controls;
using Bit.App.Models;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -68,21 +69,28 @@ namespace Bit.App.Pages
_broadcasterService.Subscribe(_pageName, async (message) =>
{
if (message.Command == "syncStarted")
try
{
Device.BeginInvokeOnMainThread(() => IsBusy = true);
}
else if (message.Command == "syncCompleted" || message.Command == "sendUpdated")
{
await Task.Delay(500);
Device.BeginInvokeOnMainThread(() =>
if (message.Command == "syncStarted")
{
IsBusy = false;
if (_vm.LoadedOnce)
Device.BeginInvokeOnMainThread(() => IsBusy = true);
}
else if (message.Command == "syncCompleted" || message.Command == "sendUpdated")
{
await Task.Delay(500);
Device.BeginInvokeOnMainThread(() =>
{
var task = _vm.LoadAsync();
}
});
IsBusy = false;
if (_vm.LoadedOnce)
{
var task = _vm.LoadAsync();
}
});
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});

View File

@@ -104,7 +104,8 @@
Grid.Row="1"
Grid.Column="1"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
IsVisible="{Binding UseOTPVerification, Converter={StaticResource inverseBool}}"/>
<Label
Text="{u:I18n ConfirmYourIdentity}"

View File

@@ -143,8 +143,7 @@ namespace Bit.App.Pages
public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.VisibilityTogglePasswordIsVisibleActivateToHide :
AppResources.VisibilityTogglePasswordIsNotVisibleActivateToHide;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public void TogglePassword()
{

View File

@@ -33,6 +33,23 @@
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" />
</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
@@ -66,31 +83,31 @@
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n DisableAutoTotpCopy}"
Text="{u:I18n CopyTotpAutomatically}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding DisableAutoTotpCopy}"
IsToggled="{Binding AutoTotpCopy}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
<Label
Text="{u:I18n DisableAutoTotpCopyDescription}"
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 DisableWebsiteIcons}"
Text="{u:I18n ShowWebsiteIcons}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding DisableFavicon}"
IsToggled="{Binding Favicon}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
<Label
Text="{u:I18n DisableWebsiteIconsDescription}"
Text="{u:I18n ShowWebsiteIconsDescription}"
StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
@@ -100,35 +117,35 @@
</StackLayout>
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n DisableSavePrompt}"
Text="{u:I18n AskToAddLogin}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding AutofillDisableSavePrompt}"
IsToggled="{Binding AutofillSavePrompt}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
<Label
Text="{u:I18n DisableSavePromptDescription}"
Text="{u:I18n AskToAddLoginDescription}"
StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
<StackLayout StyleClass="box-row, box-row-input">
<Label
Text="{u:I18n BlacklistedUris}"
Text="{u:I18n AutofillBlockedUris}"
StyleClass="box-label" />
<Editor
x:Name="_blacklistedUrisEditor"
Text="{Binding AutofillBlacklistedUris}"
x:Name="_autofillBlockedUrisEditor"
Text="{Binding AutofillBlockedUris}"
StyleClass="box-value"
AutoSize="TextChanges"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
Keyboard="Url"
Unfocused="BlacklistedUrisEditor_Unfocused" />
Unfocused="AutofillBlockedUrisEditor_Unfocused" />
</StackLayout>
<Label
Text="{u:I18n BlacklistedUrisDescription}"
Text="{u:I18n AutofillBlockedUrisDescription}"
StyleClass="box-footer-label" />
</StackLayout>
</StackLayout>

View File

@@ -19,6 +19,7 @@ namespace Bit.App.Pages
_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)
@@ -29,6 +30,7 @@ namespace Bit.App.Pages
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);
}
@@ -43,12 +45,12 @@ namespace Bit.App.Pages
protected async override void OnDisappearing()
{
base.OnDisappearing();
await _vm.UpdateAutofillBlacklistedUris();
await _vm.UpdateAutofillBlockedUris();
}
private async void BlacklistedUrisEditor_Unfocused(object sender, FocusEventArgs e)
private async void AutofillBlockedUrisEditor_Unfocused(object sender, FocusEventArgs e)
{
await _vm.UpdateAutofillBlacklistedUris();
await _vm.UpdateAutofillBlockedUris();
}
private async void Close_Clicked(object sender, System.EventArgs e)

View File

@@ -12,17 +12,17 @@ namespace Bit.App.Pages
{
public class OptionsPageViewModel : BaseViewModel
{
private readonly ITotpService _totpService;
private readonly IStateService _stateService;
private readonly IMessagingService _messagingService;
private bool _autofillDisableSavePrompt;
private string _autofillBlacklistedUris;
private bool _disableFavicon;
private bool _disableAutoTotpCopy;
private bool _autofillSavePrompt;
private string _autofillBlockedUris;
private bool _favicon;
private bool _autoTotpCopy;
private int _clearClipboardSelectedIndex;
private int _themeSelectedIndex;
private int _autoDarkThemeSelectedIndex;
private int _uriMatchSelectedIndex;
private bool _inited;
private bool _updatingAutofill;
@@ -30,7 +30,6 @@ namespace Bit.App.Pages
public OptionsPageViewModel()
{
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
@@ -53,10 +52,16 @@ namespace Bit.App.Pages
ThemeOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(null, AppResources.ThemeDefault),
new KeyValuePair<string, string>("light", AppResources.Light),
new KeyValuePair<string, string>("dark", AppResources.Dark),
new KeyValuePair<string, string>("black", AppResources.Black),
new KeyValuePair<string, string>("nord", "Nord"),
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),
};
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),
};
UriMatchOptions = new List<KeyValuePair<UriMatchType?, string>>
{
@@ -71,6 +76,7 @@ namespace Bit.App.Pages
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 int ClearClipboardSelectedIndex
@@ -80,7 +86,7 @@ namespace Bit.App.Pages
{
if (SetProperty(ref _clearClipboardSelectedIndex, value))
{
var task = SaveClipboardChangedAsync();
SaveClipboardChangedAsync().FireAndForget();
}
}
}
@@ -90,9 +96,25 @@ namespace Bit.App.Pages
get => _themeSelectedIndex;
set
{
if (SetProperty(ref _themeSelectedIndex, value))
if (SetProperty(ref _themeSelectedIndex, value,
additionalPropertyNames: new[] { nameof(ShowAutoDarkThemeOptions) })
)
{
var task = SaveThemeAsync();
SaveThemeAsync().FireAndForget();
}
}
}
public bool ShowAutoDarkThemeOptions => ThemeOptions[ThemeSelectedIndex].Key == null;
public int AutoDarkThemeSelectedIndex
{
get => _autoDarkThemeSelectedIndex;
set
{
if (SetProperty(ref _autoDarkThemeSelectedIndex, value))
{
SaveThemeAsync().FireAndForget();
}
}
}
@@ -104,51 +126,51 @@ namespace Bit.App.Pages
{
if (SetProperty(ref _uriMatchSelectedIndex, value))
{
var task = SaveDefaultUriAsync();
SaveDefaultUriAsync().FireAndForget();
}
}
}
public bool DisableFavicon
public bool Favicon
{
get => _disableFavicon;
get => _favicon;
set
{
if (SetProperty(ref _disableFavicon, value))
if (SetProperty(ref _favicon, value))
{
var task = UpdateDisableFaviconAsync();
UpdateFaviconAsync().FireAndForget();
}
}
}
public bool DisableAutoTotpCopy
public bool AutoTotpCopy
{
get => _disableAutoTotpCopy;
get => _autoTotpCopy;
set
{
if (SetProperty(ref _disableAutoTotpCopy, value))
if (SetProperty(ref _autoTotpCopy, value))
{
var task = UpdateAutoTotpCopyAsync();
UpdateAutoTotpCopyAsync().FireAndForget();
}
}
}
public bool AutofillDisableSavePrompt
public bool AutofillSavePrompt
{
get => _autofillDisableSavePrompt;
get => _autofillSavePrompt;
set
{
if (SetProperty(ref _autofillDisableSavePrompt, value))
if (SetProperty(ref _autofillSavePrompt, value))
{
var task = UpdateAutofillDisableSavePromptAsync();
UpdateAutofillSavePromptAsync().FireAndForget();
}
}
}
public string AutofillBlacklistedUris
public string AutofillBlockedUris
{
get => _autofillBlacklistedUris;
set => SetProperty(ref _autofillBlacklistedUris, value);
get => _autofillBlockedUris;
set => SetProperty(ref _autofillBlockedUris, value);
}
public bool ShowAndroidAutofillSettings
@@ -159,13 +181,15 @@ namespace Bit.App.Pages
public async Task InitAsync()
{
AutofillDisableSavePrompt = (await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
var blacklistedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
AutofillBlacklistedUris = blacklistedUrisList != null ? string.Join(", ", blacklistedUrisList) : null;
DisableAutoTotpCopy = !(await _totpService.IsAutoCopyEnabledAsync());
DisableFavicon = (await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
AutofillBlockedUris = blockedUrisList != null ? string.Join(", ", blockedUrisList) : null;
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);
@@ -178,15 +202,17 @@ namespace Bit.App.Pages
{
if (_inited)
{
await _stateService.SetDisableAutoTotpCopyAsync(DisableAutoTotpCopy);
// TODO: [PS-961] Fix negative function names
await _stateService.SetDisableAutoTotpCopyAsync(!AutoTotpCopy);
}
}
private async Task UpdateDisableFaviconAsync()
private async Task UpdateFaviconAsync()
{
if (_inited)
{
await _stateService.SetDisableFaviconAsync(DisableFavicon);
// TODO: [PS-961] Fix negative function names
await _stateService.SetDisableFaviconAsync(!Favicon);
}
}
@@ -202,8 +228,8 @@ namespace Bit.App.Pages
{
if (_inited && ThemeSelectedIndex > -1)
{
var theme = ThemeOptions[ThemeSelectedIndex].Key;
await _stateService.SetThemeAsync(theme);
await _stateService.SetThemeAsync(ThemeOptions[ThemeSelectedIndex].Key);
await _stateService.SetAutoDarkThemeAsync(AutoDarkThemeOptions[AutoDarkThemeSelectedIndex].Key);
ThemeManager.SetTheme(Application.Current.Resources);
_messagingService.Send("updatedTheme");
}
@@ -217,27 +243,28 @@ namespace Bit.App.Pages
}
}
private async Task UpdateAutofillDisableSavePromptAsync()
private async Task UpdateAutofillSavePromptAsync()
{
if (_inited)
{
await _stateService.SetAutofillDisableSavePromptAsync(AutofillDisableSavePrompt);
// TODO: [PS-961] Fix negative function names
await _stateService.SetAutofillDisableSavePromptAsync(!AutofillSavePrompt);
}
}
public async Task UpdateAutofillBlacklistedUris()
public async Task UpdateAutofillBlockedUris()
{
if (_inited)
{
if (string.IsNullOrWhiteSpace(AutofillBlacklistedUris))
if (string.IsNullOrWhiteSpace(AutofillBlockedUris))
{
await _stateService.SetAutofillBlacklistedUrisAsync(null);
AutofillBlacklistedUris = null;
AutofillBlockedUris = null;
return;
}
try
{
var csv = AutofillBlacklistedUris;
var csv = AutofillBlockedUris;
var urisList = new List<string>();
foreach (var uri in csv.Split(','))
{
@@ -254,7 +281,7 @@ namespace Bit.App.Pages
urisList.Add(cleanedUri);
}
await _stateService.SetAutofillBlacklistedUrisAsync(urisList);
AutofillBlacklistedUris = string.Join(", ", urisList);
AutofillBlockedUris = string.Join(", ", urisList);
}
catch { }
}

View File

@@ -2,18 +2,13 @@
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Pages.Accounts;
using Bit.App.Resources;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class SettingsPage : BaseContentPage
{
private readonly IDeviceActionService _deviceActionService;
private readonly TabsPage _tabsPage;
private SettingsPageViewModel _vm;
@@ -21,7 +16,6 @@ namespace Bit.App.Pages
{
_tabsPage = tabsPage;
InitializeComponent();
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_vm = BindingContext as SettingsPageViewModel;
_vm.Page = this;
}
@@ -67,122 +61,12 @@ namespace Bit.App.Pages
}
}
private async void RowSelected(object sender, SelectionChangedEventArgs e)
private void RowSelected(object sender, SelectionChangedEventArgs e)
{
((ExtendedCollectionView)sender).SelectedItem = null;
if (!DoOnce())
if (e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item)
{
return;
}
if (!(e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item))
{
return;
}
if (item.Name == AppResources.Sync)
{
await Navigation.PushModalAsync(new NavigationPage(new SyncPage()));
}
else if (item.Name == AppResources.AutofillServices)
{
await Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(this)));
}
else if (item.Name == AppResources.PasswordAutofill)
{
await Navigation.PushModalAsync(new NavigationPage(new AutofillPage()));
}
else if (item.Name == AppResources.AppExtension)
{
await Navigation.PushModalAsync(new NavigationPage(new ExtensionPage()));
}
else if (item.Name == AppResources.Options)
{
await Navigation.PushModalAsync(new NavigationPage(new OptionsPage()));
}
else if (item.Name == AppResources.Folders)
{
await Navigation.PushModalAsync(new NavigationPage(new FoldersPage()));
}
else if (item.Name == AppResources.About)
{
await _vm.AboutAsync();
}
else if (item.Name == AppResources.HelpAndFeedback)
{
_vm.Help();
}
else if (item.Name == AppResources.FingerprintPhrase)
{
await _vm.FingerprintAsync();
}
else if (item.Name == AppResources.RateTheApp)
{
_vm.Rate();
}
else if (item.Name == AppResources.ImportItems)
{
_vm.Import();
}
else if (item.Name == AppResources.ExportVault)
{
await Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()));
}
else if (item.Name == AppResources.LearnOrg)
{
await _vm.ShareAsync();
}
else if (item.Name == AppResources.WebVault)
{
_vm.WebVault();
}
else if (item.Name == AppResources.ChangeMasterPassword)
{
await _vm.ChangePasswordAsync();
}
else if (item.Name == AppResources.TwoStepLogin)
{
await _vm.TwoStepAsync();
}
else if (item.Name == AppResources.LogOut)
{
await _vm.LogOutAsync();
}
else if (item.Name == AppResources.DeleteAccount)
{
await Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage()));
}
else if (item.Name == AppResources.LockNow)
{
await _vm.LockAsync();
}
else if (item.Name == AppResources.VaultTimeout)
{
await _vm.VaultTimeoutAsync();
}
else if (item.Name == AppResources.VaultTimeoutAction)
{
await _vm.VaultTimeoutActionAsync();
}
else if (item.Name == AppResources.UnlockWithPIN)
{
await _vm.UpdatePinAsync();
}
else if (item.Name == AppResources.SubmitCrashLogs)
{
await _vm.LoggerReportingAsync();
}
else
{
var biometricName = AppResources.Biometrics;
if (Device.RuntimePlatform == Device.iOS)
{
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
biometricName = supportsFace ? AppResources.FaceID : AppResources.TouchID;
}
if (item.Name == string.Format(AppResources.UnlockWith, biometricName))
{
await _vm.UpdateBiometricAsync();
}
_vm?.ExecuteSettingItemCommand.Execute(item);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Bit.App.Resources;
using Bit.App.Utilities;
using Xamarin.Forms;
@@ -12,7 +13,9 @@ namespace Bit.App.Pages
public string SubLabel { get; set; }
public TimeSpan? Time { get; set; }
public bool UseFrame { get; set; }
public bool SubLabelTextEnabled => SubLabel == AppResources.Enabled;
public Func<Task> ExecuteAsync { get; set; }
public bool SubLabelTextEnabled => SubLabel == AppResources.On;
public string LineBreakMode => SubLabel == null ? "TailTruncation" : "";
public bool ShowSubLabel => SubLabel.Length != 0;
public bool ShowTimeInput => Time != null;

View File

@@ -3,11 +3,11 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Pages.Accounts;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
@@ -30,11 +30,13 @@ namespace Bit.App.Pages
private readonly IKeyConnectorService _keyConnectorService;
private readonly IClipboardService _clipboardService;
private readonly ILogger _loggerService;
private const int CustomVaultTimeoutValue = -100;
private bool _supportsBiometric;
private bool _pin;
private bool _biometric;
private bool _screenCaptureAllowed;
private string _lastSyncDate;
private string _vaultTimeoutDisplayValue;
private string _vaultTimeoutActionDisplayValue;
@@ -84,10 +86,14 @@ namespace Bit.App.Pages
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
PageTitle = AppResources.Settings;
ExecuteSettingItemCommand = new AsyncCommand<SettingsPageListItem>(item => item.ExecuteAsync(), onException: _loggerService.Exception, allowsMultipleExecutions: false);
}
public ObservableRangeCollection<ISettingsPageListItem> GroupedItems { get; set; }
public IAsyncCommand<SettingsPageListItem> ExecuteSettingItemCommand { get; }
public async Task InitAsync()
{
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
@@ -117,6 +123,7 @@ namespace Bit.App.Pages
var pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
_pin = pinSet.Item1 || pinSet.Item2;
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
_screenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
if (_vaultTimeoutDisplayValue == null)
{
@@ -257,6 +264,17 @@ namespace Bit.App.Pages
}
var cleanSelection = selection.Replace("✓ ", string.Empty);
var selectionOption = _vaultTimeouts.FirstOrDefault(o => o.Key == cleanSelection);
// Check if the selected Timeout action is "Never" and if it's different from the previous selected value
if (selectionOption.Value == null && selectionOption.Value != oldTimeout)
{
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.NeverLockWarning,
AppResources.Warning, AppResources.Yes, AppResources.Cancel);
if (!confirmed)
{
return;
}
}
_vaultTimeoutDisplayValue = selectionOption.Key;
newTimeout = selectionOption.Value;
}
@@ -423,6 +441,8 @@ namespace Bit.App.Pages
public void BuildList()
{
//TODO: Refactor this once navigation is abstracted so that it doesn't depend on Page, e.g. Page.Navigation.PushModalAsync...
var doUpper = Device.RuntimePlatform != Device.Android;
var autofillItems = new List<SettingsPageListItem>();
if (Device.RuntimePlatform == Device.Android)
@@ -430,38 +450,69 @@ namespace Bit.App.Pages
autofillItems.Add(new SettingsPageListItem
{
Name = AppResources.AutofillServices,
SubLabel = _deviceActionService.AutofillServicesEnabled() ?
AppResources.Enabled : AppResources.Disabled
SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.On : AppResources.Off,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage)))
});
}
else
{
if (_deviceActionService.SystemMajorVersion() >= 12)
{
autofillItems.Add(new SettingsPageListItem { Name = AppResources.PasswordAutofill });
autofillItems.Add(new SettingsPageListItem
{
Name = AppResources.PasswordAutofill,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillPage()))
});
}
autofillItems.Add(new SettingsPageListItem { Name = AppResources.AppExtension });
autofillItems.Add(new SettingsPageListItem
{
Name = AppResources.AppExtension,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExtensionPage()))
});
}
var manageItems = new List<SettingsPageListItem>
{
new SettingsPageListItem { Name = AppResources.Folders },
new SettingsPageListItem { Name = AppResources.Sync, SubLabel = _lastSyncDate }
new SettingsPageListItem
{
Name = AppResources.Folders,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new FoldersPage()))
},
new SettingsPageListItem
{
Name = AppResources.Sync,
SubLabel = _lastSyncDate,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new SyncPage()))
}
};
var securityItems = new List<SettingsPageListItem>
{
new SettingsPageListItem { Name = AppResources.VaultTimeout, SubLabel = _vaultTimeoutDisplayValue },
new SettingsPageListItem
{
Name = AppResources.VaultTimeout,
SubLabel = _vaultTimeoutDisplayValue,
ExecuteAsync = () => VaultTimeoutAsync() },
new SettingsPageListItem
{
Name = AppResources.VaultTimeoutAction,
SubLabel = _vaultTimeoutActionDisplayValue
SubLabel = _vaultTimeoutActionDisplayValue,
ExecuteAsync = () => VaultTimeoutActionAsync()
},
new SettingsPageListItem
{
Name = AppResources.UnlockWithPIN,
SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled
SubLabel = _pin ? AppResources.On : AppResources.Off,
ExecuteAsync = () => UpdatePinAsync()
},
new SettingsPageListItem { Name = AppResources.LockNow },
new SettingsPageListItem { Name = AppResources.TwoStepLogin }
new SettingsPageListItem
{
Name = AppResources.LockNow,
ExecuteAsync = () => LockAsync()
},
new SettingsPageListItem
{
Name = AppResources.TwoStepLogin,
ExecuteAsync = () => TwoStepAsync()
}
};
if (_supportsBiometric || _biometric)
{
@@ -474,7 +525,8 @@ namespace Bit.App.Pages
var item = new SettingsPageListItem
{
Name = string.Format(AppResources.UnlockWith, biometricName),
SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled
SubLabel = _biometric ? AppResources.On : AppResources.Off,
ExecuteAsync = () => UpdateBiometricAsync()
};
securityItems.Insert(2, item);
}
@@ -497,40 +549,98 @@ namespace Bit.App.Pages
UseFrame = true,
});
}
if (Device.RuntimePlatform == Device.Android)
{
securityItems.Add(new SettingsPageListItem
{
Name = AppResources.AllowScreenCapture,
SubLabel = _screenCaptureAllowed ? AppResources.On : AppResources.Off,
ExecuteAsync = () => SetScreenCaptureAllowedAsync()
});
}
var accountItems = new List<SettingsPageListItem>
{
new SettingsPageListItem { Name = AppResources.FingerprintPhrase },
new SettingsPageListItem { Name = AppResources.LogOut }
new SettingsPageListItem
{
Name = AppResources.FingerprintPhrase,
ExecuteAsync = () => FingerprintAsync()
},
new SettingsPageListItem
{
Name = AppResources.LogOut,
ExecuteAsync = () => LogOutAsync()
}
};
if (_showChangeMasterPassword)
{
accountItems.Insert(0, new SettingsPageListItem { Name = AppResources.ChangeMasterPassword });
accountItems.Insert(0, new SettingsPageListItem
{
Name = AppResources.ChangeMasterPassword,
ExecuteAsync = () => ChangePasswordAsync()
});
}
var toolsItems = new List<SettingsPageListItem>
{
new SettingsPageListItem { Name = AppResources.ImportItems },
new SettingsPageListItem { Name = AppResources.ExportVault }
new SettingsPageListItem
{
Name = AppResources.ImportItems,
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Import())
},
new SettingsPageListItem
{
Name = AppResources.ExportVault,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()))
}
};
if (IncludeLinksWithSubscriptionInfo())
{
toolsItems.Add(new SettingsPageListItem { Name = AppResources.LearnOrg });
toolsItems.Add(new SettingsPageListItem { Name = AppResources.WebVault });
toolsItems.Add(new SettingsPageListItem
{
Name = AppResources.LearnOrg,
ExecuteAsync = () => ShareAsync()
});
toolsItems.Add(new SettingsPageListItem
{
Name = AppResources.WebVault,
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => WebVault())
});
}
var otherItems = new List<SettingsPageListItem>
{
new SettingsPageListItem { Name = AppResources.Options },
new SettingsPageListItem { Name = AppResources.About },
new SettingsPageListItem { Name = AppResources.HelpAndFeedback },
new SettingsPageListItem
{
Name = AppResources.Options,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new OptionsPage()))
},
new SettingsPageListItem
{
Name = AppResources.About,
ExecuteAsync = () => AboutAsync()
},
new SettingsPageListItem
{
Name = AppResources.HelpAndFeedback,
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Help())
},
#if !FDROID
new SettingsPageListItem
{
Name = AppResources.SubmitCrashLogs,
SubLabel = _reportLoggingEnabled ? AppResources.Enabled : AppResources.Disabled,
SubLabel = _reportLoggingEnabled ? AppResources.On : AppResources.Off,
ExecuteAsync = () => LoggerReportingAsync()
},
#endif
new SettingsPageListItem { Name = AppResources.RateTheApp },
new SettingsPageListItem { Name = AppResources.DeleteAccount }
new SettingsPageListItem
{
Name = AppResources.RateTheApp,
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Rate())
},
new SettingsPageListItem
{
Name = AppResources.DeleteAccount,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage()))
}
};
// TODO: improve this. Leaving this as is to reduce error possibility on the hotfix.
@@ -610,5 +720,33 @@ namespace Bit.App.Pages
private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option;
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == $"✓ {compareTo}";
public async Task SetScreenCaptureAllowedAsync()
{
if (CoreHelpers.ForceScreenCaptureEnabled())
{
return;
}
try
{
if (!_screenCaptureAllowed
&&
!await Page.DisplayAlert(AppResources.AllowScreenCapture, AppResources.AreYouSureYouWantToEnableScreenCapture, AppResources.Yes, AppResources.No))
{
return;
}
await _stateService.SetScreenCaptureAllowedAsync(!_screenCaptureAllowed);
_screenCaptureAllowed = !_screenCaptureAllowed;
await _deviceActionService.SetScreenCaptureAllowedAsync();
BuildList();
}
catch (Exception ex)
{
_loggerService.Exception(ex);
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
}
}
}

View File

@@ -6,6 +6,7 @@ using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -55,21 +56,28 @@ namespace Bit.App.Pages
_broadcasterService.Subscribe(nameof(AutofillCiphersPage), async (message) =>
{
if (message.Command == "syncStarted")
try
{
Device.BeginInvokeOnMainThread(() => IsBusy = true);
}
else if (message.Command == "syncCompleted")
{
await Task.Delay(500);
Device.BeginInvokeOnMainThread(() =>
if (message.Command == "syncStarted")
{
IsBusy = false;
if (_vm.LoadedOnce)
Device.BeginInvokeOnMainThread(() => IsBusy = true);
}
else if (message.Command == "syncCompleted")
{
await Task.Delay(500);
Device.BeginInvokeOnMainThread(() =>
{
var task = _vm.LoadAsync();
}
});
IsBusy = false;
if (_vm.LoadedOnce)
{
var task = _vm.LoadAsync();
}
});
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
@@ -129,11 +137,11 @@ namespace Bit.App.Pages
}
if (_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login)
{
var pageForOther = new AddEditPage(type: _appOptions.FillType, fromAutofill: true);
var pageForOther = new CipherAddEditPage(type: _appOptions.FillType, fromAutofill: true);
await Navigation.PushModalAsync(new NavigationPage(pageForOther));
return;
}
var pageForLogin = new AddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
fromAutofill: true);
await Navigation.PushModalAsync(new NavigationPage(pageForLogin));
}

View File

@@ -0,0 +1,76 @@
using System;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
namespace Bit.App.Pages
{
public abstract class BaseCipherViewModel : BaseViewModel
{
private readonly IAuditService _auditService;
protected readonly IDeviceActionService _deviceActionService;
protected readonly ILogger _logger;
protected readonly IPlatformUtilsService _platformUtilsService;
private CipherView _cipher;
protected abstract string[] AdditionalPropertiesToRaiseOnCipherChanged { get; }
public BaseCipherViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
CheckPasswordCommand = new AsyncCommand(CheckPasswordAsync, allowsMultipleExecutions: false);
}
public CipherView Cipher
{
get => _cipher;
set => SetProperty(ref _cipher, value, additionalPropertyNames: AdditionalPropertiesToRaiseOnCipherChanged);
}
public AsyncCommand CheckPasswordCommand { get; }
protected async Task CheckPasswordAsync()
{
try
{
if (string.IsNullOrWhiteSpace(Cipher?.Login?.Password))
{
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(matches > 0
? string.Format(AppResources.PasswordExposed, matches.ToString("N0"))
: AppResources.PasswordSafe);
}
catch (ApiException apiException)
{
_logger.Exception(apiException);
await _deviceActionService.HideLoadingAsync();
if (apiException?.Error != null)
{
await _platformUtilsService.ShowDialogAsync(apiException.Error.GetSingleMessage(),
AppResources.AnErrorHasOccurred);
}
}
catch (Exception ex)
{
_logger.Exception(ex);
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
}
}
}
}

View File

@@ -2,7 +2,7 @@
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.AddEditPage"
x:Class="Bit.App.Pages.CipherAddEditPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
@@ -10,11 +10,11 @@
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:AddEditPageViewModel"
x:DataType="pages:CipherAddEditPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:AddEditPageViewModel />
<pages:CipherAddEditPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
@@ -161,7 +161,8 @@
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
IsVisible="{Binding Cipher.ViewPassword}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
@@ -183,31 +184,61 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n AuthenticatorKey}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Frame
IsVisible="{Binding HasTotpValue, Converter={StaticResource inverseBool}}"
Margin="0,5,0,0"
StyleClass="btn-icon-row"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
Padding="0"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3">
<Frame.GestureRecognizers>
<TapGestureRecognizer Tapped="ScanTotp_Clicked" />
</Frame.GestureRecognizers>
<controls:IconLabel
Text="{Binding SetupTotpText}"
Padding="0,15"
HorizontalOptions="Center"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center" />
</Frame>
<controls:MonoEntry
x:Name="_loginTotpEntry"
Text="{Binding Cipher.Login.Totp}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
IsVisible="{Binding HasTotpValue}"
IsPassword="{Binding Cipher.ViewPassword, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding Cipher.ViewPassword}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="{Binding TotpColumnSpan}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
IsVisible="{Binding HasTotpValue}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Camera}}"
Clicked="ScanTotp_Clicked"
Grid.Row="0"
Grid.Column="1"
Grid.Column="2"
Grid.RowSpan="2"
IsVisible="{Binding Cipher.ViewPassword}"
IsVisible="{Binding HasTotpValue}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ScanQrTitle}" />
</Grid>
@@ -607,7 +638,7 @@
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Fields}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:AddEditPageFieldViewModel">
<DataTemplate x:DataType="pages:CipherAddEditPageFieldViewModel">
<StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row, box-row-input">
<Grid.RowDefinitions>

View File

@@ -14,7 +14,7 @@ using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
namespace Bit.App.Pages
{
public partial class AddEditPage : BaseContentPage
public partial class CipherAddEditPage : BaseContentPage
{
private readonly AppOptions _appOptions;
private readonly IStateService _stateService;
@@ -22,10 +22,10 @@ namespace Bit.App.Pages
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IKeyConnectorService _keyConnectorService;
private AddEditPageViewModel _vm;
private CipherAddEditPageViewModel _vm;
private bool _fromAutofill;
public AddEditPage(
public CipherAddEditPage(
string cipherId = null,
CipherType? type = null,
string folderId = null,
@@ -36,7 +36,7 @@ namespace Bit.App.Pages
bool fromAutofill = false,
AppOptions appOptions = null,
bool cloneMode = false,
ViewPage viewPage = null)
CipherDetailsPage cipherDetailsPage = null)
{
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
@@ -47,7 +47,7 @@ namespace Bit.App.Pages
_fromAutofill = fromAutofill;
FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false;
InitializeComponent();
_vm = BindingContext as AddEditPageViewModel;
_vm = BindingContext as CipherAddEditPageViewModel;
_vm.Page = this;
_vm.CipherId = cipherId;
_vm.FolderId = folderId == "none" ? null : folderId;
@@ -57,7 +57,7 @@ namespace Bit.App.Pages
_vm.DefaultName = name ?? appOptions?.SaveName;
_vm.DefaultUri = uri ?? appOptions?.Uri;
_vm.CloneMode = cloneMode;
_vm.ViewPage = viewPage;
_vm.CipherDetailsPage = cipherDetailsPage;
_vm.Init();
SetActivityIndicator();
if (_vm.EditMode && !_vm.CloneMode && Device.RuntimePlatform == Device.Android)
@@ -145,7 +145,7 @@ namespace Bit.App.Pages
}
public bool FromAutofillFramework { get; set; }
public AddEditPageViewModel ViewModel => _vm;
public CipherAddEditPageViewModel ViewModel => _vm;
protected override async void OnAppearing()
{

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.Core;
@@ -11,26 +10,23 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class AddEditPageViewModel : BaseViewModel
public class CipherAddEditPageViewModel : BaseCipherViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly ICipherService _cipherService;
private readonly IFolderService _folderService;
private readonly ICollectionService _collectionService;
private readonly IStateService _stateService;
private readonly IOrganizationService _organizationService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuditService _auditService;
private readonly IMessagingService _messagingService;
private readonly IEventService _eventService;
private readonly IPolicyService _policyService;
private readonly ILogger _logger;
private readonly IClipboardService _clipboardService;
private CipherView _cipher;
private bool _showNotesSeparator;
private bool _showPassword;
private bool _showCardNumber;
@@ -44,7 +40,7 @@ namespace Bit.App.Pages
private bool _hasCollections;
private string _previousCipherId;
private List<Core.Models.View.CollectionView> _writeableCollections;
private string[] _additionalCipherProperties = new string[]
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
{
nameof(IsLogin),
nameof(IsIdentity),
@@ -53,7 +49,9 @@ namespace Bit.App.Pages
nameof(ShowUris),
nameof(ShowAttachments),
nameof(ShowCollections),
nameof(HasTotpValue)
};
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
new List<KeyValuePair<UriMatchType?, string>>
{
@@ -66,31 +64,28 @@ namespace Bit.App.Pages
new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never)
};
public AddEditPageViewModel()
public CipherAddEditPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
ToggleCardNumberCommand = new Command(ToggleCardNumber);
ToggleCardCodeCommand = new Command(ToggleCardCode);
CheckPasswordCommand = new Command(CheckPasswordAsync);
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
FieldOptionsCommand = new Command<AddEditPageFieldViewModel>(FieldOptions);
FieldOptionsCommand = new Command<CipherAddEditPageFieldViewModel>(FieldOptions);
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
Uris = new ExtendedObservableCollection<LoginUriView>();
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
Collections = new ExtendedObservableCollection<CollectionViewModel>();
AllowPersonal = true;
@@ -146,10 +141,10 @@ namespace Bit.App.Pages
public Command TogglePasswordCommand { get; set; }
public Command ToggleCardNumberCommand { get; set; }
public Command ToggleCardCodeCommand { get; set; }
public Command CheckPasswordCommand { get; set; }
public Command UriOptionsCommand { get; set; }
public Command FieldOptionsCommand { get; set; }
public Command PasswordPromptHelpCommand { get; set; }
public AsyncCommand CopyCommand { get; set; }
public string CipherId { get; set; }
public string OrganizationId { get; set; }
public string FolderId { get; set; }
@@ -164,7 +159,7 @@ namespace Bit.App.Pages
public List<KeyValuePair<string, string>> FolderOptions { get; set; }
public List<KeyValuePair<string, string>> OwnershipOptions { get; set; }
public ExtendedObservableCollection<LoginUriView> Uris { get; set; }
public ExtendedObservableCollection<AddEditPageFieldViewModel> Fields { get; set; }
public ExtendedObservableCollection<CipherAddEditPageFieldViewModel> Fields { get; set; }
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
public int TypeSelectedIndex
@@ -233,11 +228,6 @@ namespace Bit.App.Pages
}
}
}
public CipherView Cipher
{
get => _cipher;
set => SetProperty(ref _cipher, value, additionalPropertyNames: _additionalCipherProperties);
}
public bool ShowNotesSeparator
{
get => _showNotesSeparator;
@@ -285,7 +275,7 @@ namespace Bit.App.Pages
public bool ShowOwnershipOptions => !EditMode || CloneMode;
public bool OwnershipPolicyInEffect => ShowOwnershipOptions && !AllowPersonal;
public bool CloneMode { get; set; }
public ViewPage ViewPage { get; set; }
public CipherDetailsPage CipherDetailsPage { get; set; }
public bool IsLogin => Cipher?.Type == CipherType.Login;
public bool IsIdentity => Cipher?.Type == CipherType.Identity;
public bool IsCard => Cipher?.Type == CipherType.Card;
@@ -299,9 +289,9 @@ namespace Bit.App.Pages
public int TotpColumnSpan => Cipher.ViewPassword ? 1 : 2;
public bool AllowPersonal { get; set; }
public bool PasswordPrompt => Cipher.Reprompt != CipherRepromptType.None;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.VisibilityTogglePasswordIsVisibleActivateToHide :
AppResources.VisibilityTogglePasswordIsNotVisibleActivateToHide;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public bool HasTotpValue => IsLogin && !string.IsNullOrEmpty(Cipher?.Login?.Totp);
public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTotp}";
public void Init()
{
PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem;
@@ -422,7 +412,7 @@ namespace Bit.App.Pages
}
if (Cipher.Fields != null)
{
Fields.ResetWithRange(Cipher.Fields?.Select(f => new AddEditPageFieldViewModel(Cipher, f)));
Fields.ResetWithRange(Cipher.Fields?.Select(f => new CipherAddEditPageFieldViewModel(Cipher, f)));
}
}
@@ -510,7 +500,7 @@ namespace Bit.App.Pages
EditMode && !CloneMode ? AppResources.ItemUpdated : AppResources.NewItemCreated);
_messagingService.Send(EditMode && !CloneMode ? "editedCipher" : "addedCipher", Cipher.Id);
if (Page is AddEditPage page && page.FromAutofillFramework)
if (Page is CipherAddEditPage page && page.FromAutofillFramework)
{
// Close and go back to app
_deviceActionService.CloseAutofill();
@@ -519,7 +509,7 @@ namespace Bit.App.Pages
{
if (CloneMode)
{
ViewPage?.UpdateCipherId(this.Cipher.Id);
CipherDetailsPage?.UpdateCipherId(this.Cipher.Id);
}
// if the app is tombstoned then PopModalAsync would throw index out of bounds
if (Page.Navigation?.ModalStack?.Count > 0)
@@ -604,7 +594,7 @@ namespace Bit.App.Pages
public async void UriOptions(LoginUriView uri)
{
if (!(Page as AddEditPage).DoOnce())
if (!(Page as CipherAddEditPage).DoOnce())
{
return;
}
@@ -640,9 +630,9 @@ namespace Bit.App.Pages
Uris.Add(new LoginUriView());
}
public async void FieldOptions(AddEditPageFieldViewModel field)
public async void FieldOptions(CipherAddEditPageFieldViewModel field)
{
if (!(Page as AddEditPage).DoOnce())
if (!(Page as CipherAddEditPage).DoOnce())
{
return;
}
@@ -702,10 +692,10 @@ namespace Bit.App.Pages
}
if (Fields == null)
{
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
}
var type = fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key;
Fields.Add(new AddEditPageFieldViewModel(Cipher, new FieldView
Fields.Add(new CipherAddEditPageFieldViewModel(Cipher, new FieldView
{
Type = type,
Name = string.IsNullOrWhiteSpace(name) ? null : name,
@@ -833,35 +823,24 @@ namespace Bit.App.Pages
private void TriggerCipherChanged()
{
TriggerPropertyChanged(nameof(Cipher), _additionalCipherProperties);
TriggerPropertyChanged(nameof(Cipher), AdditionalPropertiesToRaiseOnCipherChanged);
}
private async void CheckPasswordAsync()
private async Task CopyTotpClipboardAsync()
{
if (!(Page as BaseContentPage).DoOnce())
try
{
return;
await _clipboardService.CopyTextAsync(Cipher.Login.Totp);
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.AuthenticatorKeyScanner));
}
if (string.IsNullOrWhiteSpace(Cipher.Login?.Password))
catch (Exception ex)
{
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
await _deviceActionService.HideLoadingAsync();
if (matches > 0)
{
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed,
matches.ToString("N0")));
}
else
{
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
_logger.Exception(ex);
}
}
}
public class AddEditPageFieldViewModel : ExtendedViewModel
public class CipherAddEditPageFieldViewModel : ExtendedViewModel
{
private II18nService _i18nService;
private FieldView _field;
@@ -877,7 +856,7 @@ namespace Bit.App.Pages
nameof(IsLinkedType),
};
public AddEditPageFieldViewModel(CipherView cipher, FieldView field)
public CipherAddEditPageFieldViewModel(CipherView cipher, FieldView field)
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_cipher = cipher;

View File

@@ -1,19 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage
<?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.ViewPage"
x:Class="Bit.App.Pages.CipherDetailsPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:ViewPageViewModel"
x:DataType="pages:CipherDetailsPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:ViewPageViewModel />
<pages:CipherDetailsPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
@@ -22,15 +22,15 @@
<u:StringHasValueConverter x:Key="stringHasValue" />
<u:IsNotNullConverter x:Key="notNull" />
<ToolbarItem Text="{u:I18n Collections}"
x:Key="collectionsItem"
x:Name="_collectionsItem"
Clicked="Collections_Clicked"
Order="Secondary" />
x:Key="collectionsItem"
x:Name="_collectionsItem"
Clicked="Collections_Clicked"
Order="Secondary" />
<ToolbarItem Text="{u:I18n MoveToOrganization}"
x:Key="shareItem"
x:Name="_shareItem"
Clicked="Share_Clicked"
Order="Secondary" />
x:Key="shareItem"
x:Name="_shareItem"
Clicked="Share_Clicked"
Order="Secondary" />
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
x:Name="_closeItem" x:Key="closeItem" />
<ToolbarItem Clicked="EditToolbarItem_Clicked" Order="Primary"
@@ -83,7 +83,7 @@
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0" />
<controls:IconButton
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
@@ -96,7 +96,7 @@
</Grid>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}" />
<Grid StyleClass="box-row"
<Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -126,7 +126,7 @@
Grid.Column="0"
LineBreakMode="CharacterWrap"
IsVisible="{Binding ShowPassword}" />
<controls:IconButton
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
Command="{Binding CheckPasswordCommand}"
@@ -136,7 +136,7 @@
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CheckPassword}"
IsVisible="{Binding Cipher.ViewPassword}" />
<controls:IconButton
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
@@ -144,9 +144,10 @@
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding PasswordVisibilityAccessibilityText}"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
IsVisible="{Binding Cipher.ViewPassword}" />
<controls:IconButton
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
@@ -164,10 +165,11 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="40" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
@@ -177,29 +179,49 @@
Grid.Column="0" />
<controls:MonoLabel
Text="{Binding TotpCodeFormatted, Mode=OneWay}"
IsVisible="{Binding ShowUpgradePremiumTotpText, Converter={StaticResource inverseBool}}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0" />
<Label
Text="{Binding TotpSec, Mode=OneWay}"
Style="{DynamicResource textTotp}"
Margin="0, 0, 10, 0"
Grid.Column="0"
VerticalTextAlignment="Start"
VerticalOptions="Start" />
<controls:CircularProgressbarView
Progress="{Binding TotpProgress}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
HorizontalOptions="End"
HorizontalTextAlignment="End"
VerticalOptions="CenterAndExpand" />
<controls:IconButton
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" />
<Label
Text="{Binding TotpSec, Mode=OneWay}"
Style="{DynamicResource textTotp}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
StyleClass="text-sm"
VerticalTextAlignment="Center"
HorizontalTextAlignment="Center"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
IsVisible="{Binding CanAccessPremium}"
CommandParameter="LoginTotp"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyTotp}" />
<Label
Text="{u:I18n PremiumSubscriptionRequired}"
StyleClass="box-footer-label"
IsVisible="{Binding ShowUpgradePremiumTotpText}"
Margin="0,5,0,2"
Grid.Column="0"
Grid.Row="1"
HorizontalOptions="FillAndExpand" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowTotp}" />
</StackLayout>
@@ -243,7 +265,7 @@
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowCardNumber}" />
<controls:IconButton
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowCardNumberIcon}"
Command="{Binding ToggleCardNumberCommand}"
@@ -252,7 +274,7 @@
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
<controls:IconButton
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
@@ -315,7 +337,7 @@
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowCardCode}" />
<controls:IconButton
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowCardCodeIcon}"
Command="{Binding ToggleCardCodeCommand}"
@@ -324,7 +346,7 @@
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
<controls:IconButton
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
@@ -528,7 +550,7 @@
<StackLayout StyleClass="box-row">
<controls:SelectableLabel
Text="{Binding Cipher.Notes, Mode=OneWay}"
StyleClass="box-value"/>
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
@@ -539,7 +561,7 @@
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Fields}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:ViewPageFieldViewModel">
<DataTemplate x:DataType="pages:CipherDetailsPageFieldViewModel">
<StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
@@ -570,6 +592,8 @@
IsVisible="{Binding IsLinkedType}" />
<controls:IconLabel
Text="{Binding ValueText, Mode=OneWay}"
AutomationProperties.IsInAccessibleTree="true"
AutomationProperties.Name="{Binding ValueAccessibilityText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
@@ -587,7 +611,7 @@
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
</StackLayout>
<controls:IconButton
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowHiddenValueIcon}"
Command="{Binding ToggleHiddenValueCommand}"
@@ -635,7 +659,7 @@
StyleClass="box-sub-label"
HorizontalTextAlignment="End"
VerticalTextAlignment="Center" />
<controls:IconButton
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Download}}"
Command="{Binding BindingContext.DownloadAttachmentCommand, Source={x:Reference _page}}"
@@ -699,4 +723,4 @@
</Button>
</AbsoluteLayout>
</pages:BaseContentPage>
</pages:BaseContentPage>

View File

@@ -3,23 +3,24 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class ViewPage : BaseContentPage
public partial class CipherDetailsPage : BaseContentPage
{
private readonly IBroadcasterService _broadcasterService;
private readonly ISyncService _syncService;
private ViewPageViewModel _vm;
private CipherDetailsPageViewModel _vm;
public ViewPage(string cipherId)
public CipherDetailsPage(string cipherId)
{
InitializeComponent();
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
_vm = BindingContext as ViewPageViewModel;
_vm = BindingContext as CipherDetailsPageViewModel;
_vm.Page = this;
_vm.CipherId = cipherId;
SetActivityIndicator(_mainContent);
@@ -39,7 +40,7 @@ namespace Bit.App.Pages
}
}
public ViewPageViewModel ViewModel => _vm;
public CipherDetailsPageViewModel ViewModel => _vm;
public void UpdateCipherId(string cipherId)
{
@@ -54,39 +55,46 @@ namespace Bit.App.Pages
IsBusy = true;
}
_broadcasterService.Subscribe(nameof(ViewPage), async (message) =>
_broadcasterService.Subscribe(nameof(CipherDetailsPage), async (message) =>
{
if (message.Command == "syncStarted")
try
{
Device.BeginInvokeOnMainThread(() => IsBusy = true);
}
else if (message.Command == "syncCompleted")
{
await Task.Delay(500);
Device.BeginInvokeOnMainThread(() =>
if (message.Command == "syncStarted")
{
IsBusy = false;
if (message.Data is Dictionary<string, object> data && data.ContainsKey("successfully"))
Device.BeginInvokeOnMainThread(() => IsBusy = true);
}
else if (message.Command == "syncCompleted")
{
await Task.Delay(500);
Device.BeginInvokeOnMainThread(() =>
{
var success = data["successfully"] as bool?;
if (success.GetValueOrDefault())
IsBusy = false;
if (message.Data is Dictionary<string, object> data && data.ContainsKey("successfully"))
{
var task = _vm.LoadAsync(() => AdjustToolbar());
var success = data["successfully"] as bool?;
if (success.GetValueOrDefault())
{
var task = _vm.LoadAsync(() => AdjustToolbar());
}
}
}
});
}
else if (message.Command == "selectSaveFileResult")
{
Device.BeginInvokeOnMainThread(() =>
});
}
else if (message.Command == "selectSaveFileResult")
{
var data = message.Data as Tuple<string, string>;
if (data == null)
Device.BeginInvokeOnMainThread(() =>
{
return;
}
_vm.SaveFileSelected(data.Item1, data.Item2);
});
var data = message.Data as Tuple<string, string>;
if (data == null)
{
return;
}
_vm.SaveFileSelected(data.Item1, data.Item2);
});
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
await LoadOnAppearedAsync(_scrollView, true, async () =>
@@ -103,8 +111,8 @@ namespace Bit.App.Pages
{
base.OnDisappearing();
IsBusy = false;
_broadcasterService.Unsubscribe(nameof(ViewPage));
_vm.CleanUp();
_vm.StopCiphersTotpTick().FireAndForget();
_broadcasterService.Unsubscribe(nameof(CipherDetailsPage));
}
private async void PasswordHistory_Tapped(object sender, System.EventArgs e)
@@ -132,7 +140,7 @@ namespace Bit.App.Pages
{
return;
}
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId)));
await Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(_vm.CipherId)));
}
}
}
@@ -204,7 +212,7 @@ namespace Bit.App.Pages
{
return;
}
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
@@ -259,7 +267,7 @@ namespace Bit.App.Pages
}
else if (selection == AppResources.Clone)
{
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
@@ -11,26 +13,24 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class ViewPageViewModel : BaseViewModel
public class CipherDetailsPageViewModel : BaseCipherViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly ICipherService _cipherService;
private readonly IStateService _stateService;
private readonly ITotpService _totpService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuditService _auditService;
private readonly ITotpService _totpService;
private readonly IMessagingService _messagingService;
private readonly IEventService _eventService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly ILocalizeService _localizeService;
private readonly IClipboardService _clipboardService;
private CipherView _cipher;
private List<ViewPageFieldViewModel> _fields;
private List<CipherDetailsPageFieldViewModel> _fields;
private bool _canAccessPremium;
private bool _showPassword;
private bool _showCardNumber;
@@ -44,67 +44,62 @@ namespace Bit.App.Pages
private byte[] _attachmentData;
private string _attachmentFilename;
private bool _passwordReprompted;
private TotpHelper _totpTickHelper;
private CancellationTokenSource _totpTickCancellationToken;
private Task _totpTickTask;
public ViewPageViewModel()
public CipherDetailsPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
CopyCommand = new Command<string>((id) => CopyAsync(id, null));
CopyUriCommand = new Command<LoginUriView>(CopyUri);
CopyFieldCommand = new Command<FieldView>(CopyField);
CopyCommand = new AsyncCommand<string>((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
CopyUriCommand = new AsyncCommand<LoginUriView>(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
CopyFieldCommand = new AsyncCommand<FieldView>(field => CopyAsync(field.Type == FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
LaunchUriCommand = new Command<LoginUriView>(LaunchUri);
TogglePasswordCommand = new Command(TogglePassword);
ToggleCardNumberCommand = new Command(ToggleCardNumber);
ToggleCardCodeCommand = new Command(ToggleCardCode);
CheckPasswordCommand = new Command(CheckPasswordAsync);
DownloadAttachmentCommand = new Command<AttachmentView>(DownloadAttachmentAsync);
DownloadAttachmentCommand = new AsyncCommand<AttachmentView>(DownloadAttachmentAsync, allowsMultipleExecutions: false);
PageTitle = AppResources.ViewItem;
}
public Command CopyCommand { get; set; }
public Command CopyUriCommand { get; set; }
public Command CopyFieldCommand { get; set; }
public ICommand CopyCommand { get; set; }
public ICommand CopyUriCommand { get; set; }
public ICommand CopyFieldCommand { get; set; }
public Command LaunchUriCommand { get; set; }
public Command TogglePasswordCommand { get; set; }
public Command ToggleCardNumberCommand { get; set; }
public Command ToggleCardCodeCommand { get; set; }
public Command CheckPasswordCommand { get; set; }
public Command DownloadAttachmentCommand { get; set; }
public AsyncCommand<AttachmentView> DownloadAttachmentCommand { get; set; }
public string CipherId { get; set; }
public CipherView Cipher
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
{
get => _cipher;
set => SetProperty(ref _cipher, value,
additionalPropertyNames: new string[]
{
nameof(IsLogin),
nameof(IsIdentity),
nameof(IsCard),
nameof(IsSecureNote),
nameof(ShowUris),
nameof(ShowAttachments),
nameof(ShowTotp),
nameof(ColoredPassword),
nameof(UpdatedText),
nameof(PasswordUpdatedText),
nameof(PasswordHistoryText),
nameof(ShowIdentityAddress),
nameof(IsDeleted),
nameof(CanEdit),
});
}
public List<ViewPageFieldViewModel> Fields
nameof(IsLogin),
nameof(IsIdentity),
nameof(IsCard),
nameof(IsSecureNote),
nameof(ShowUris),
nameof(ShowAttachments),
nameof(ShowTotp),
nameof(ColoredPassword),
nameof(UpdatedText),
nameof(PasswordUpdatedText),
nameof(PasswordHistoryText),
nameof(ShowIdentityAddress),
nameof(IsDeleted),
nameof(CanEdit),
nameof(ShowUpgradePremiumTotpText)
};
public List<CipherDetailsPageFieldViewModel> Fields
{
get => _fields;
set => SetProperty(ref _fields, value);
@@ -203,22 +198,22 @@ namespace Bit.App.Pages
return fs;
}
}
public bool ShowUpgradePremiumTotpText => !CanAccessPremium && !Cipher.OrganizationUseTotp && ShowTotp;
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
public bool ShowIdentityAddress => IsIdentity && (
!string.IsNullOrWhiteSpace(Cipher.Identity.Address1) ||
!string.IsNullOrWhiteSpace(Cipher.Identity.City) ||
!string.IsNullOrWhiteSpace(Cipher.Identity.Country));
public bool ShowAttachments => Cipher.HasAttachments && (CanAccessPremium || Cipher.OrganizationId != null);
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
!string.IsNullOrWhiteSpace(TotpCodeFormatted);
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp);
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string ShowCardNumberIcon => ShowCardNumber ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string ShowCardCodeIcon => ShowCardCode ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.VisibilityTogglePasswordIsVisibleActivateToHide :
AppResources.VisibilityTogglePasswordIsNotVisibleActivateToHide;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string TotpCodeFormatted
{
get => _totpCodeFormatted;
get => ShowUpgradePremiumTotpText ? string.Empty : _totpCodeFormatted;
set => SetProperty(ref _totpCodeFormatted, value,
additionalPropertyNames: new string[]
{
@@ -228,7 +223,11 @@ namespace Bit.App.Pages
public string TotpSec
{
get => _totpSec;
set => SetProperty(ref _totpSec, value);
set => SetProperty(ref _totpSec, value,
additionalPropertyNames: new string[]
{
nameof(TotpProgress)
});
}
public bool TotpLow
{
@@ -239,12 +238,12 @@ namespace Bit.App.Pages
Page.Resources["textTotp"] = ThemeManager.Resources()[value ? "text-danger" : "text-default"];
}
}
public double TotpProgress => string.IsNullOrEmpty(TotpSec) ? 0 : double.Parse(TotpSec) * 100 / 30;
public bool IsDeleted => Cipher.IsDeleted;
public bool CanEdit => !Cipher.IsDeleted;
public async Task<bool> LoadAsync(Action finishedLoadingAction = null)
{
CleanUp();
var cipher = await _cipherService.GetAsync(CipherId);
if (cipher == null)
{
@@ -253,24 +252,15 @@ namespace Bit.App.Pages
}
Cipher = await cipher.DecryptAsync();
CanAccessPremium = await _stateService.CanAccessPremiumAsync();
Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(this, Cipher, f)).ToList();
Fields = Cipher.Fields?.Select(f => new CipherDetailsPageFieldViewModel(this, Cipher, f)).ToList();
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
(Cipher.OrganizationUseTotp || CanAccessPremium))
{
await TotpUpdateCodeAsync();
var interval = _totpService.GetTimeInterval(Cipher.Login.Totp);
await TotpTickAsync(interval);
_totpInterval = DateTime.UtcNow;
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
{
if (_totpInterval == null)
{
return false;
}
var task = TotpTickAsync(interval);
return true;
});
_totpTickHelper = new TotpHelper(Cipher);
_totpTickCancellationToken?.Cancel();
_totpTickCancellationToken = new CancellationTokenSource();
_totpTickTask = new TimerTask(_logger, StartCiphersTotpTick, _totpTickCancellationToken).RunPeriodic();
}
if (_previousCipherId != CipherId)
{
@@ -281,9 +271,27 @@ namespace Bit.App.Pages
return true;
}
public void CleanUp()
private async void StartCiphersTotpTick()
{
_totpInterval = null;
try
{
await _totpTickHelper.GenerateNewTotpValues();
TotpSec = _totpTickHelper.TotpSec;
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
}
catch (Exception ex)
{
_logger.Exception(ex);
}
}
public async Task StopCiphersTotpTick()
{
_totpTickCancellationToken?.Cancel();
if (_totpTickTask != null)
{
await _totpTickTask;
}
}
public async void TogglePassword()
@@ -452,86 +460,52 @@ namespace Bit.App.Pages
}
}
private async void CheckPasswordAsync()
private async Task DownloadAttachmentAsync(AttachmentView attachment)
{
if (!(Page as BaseContentPage).DoOnce())
{
return;
}
if (string.IsNullOrWhiteSpace(Cipher.Login?.Password))
{
return;
}
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
await _deviceActionService.HideLoadingAsync();
if (matches > 0)
{
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed,
matches.ToString("N0")));
}
else
{
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
}
}
private async void DownloadAttachmentAsync(AttachmentView attachment)
{
if (!(Page as BaseContentPage).DoOnce())
{
return;
}
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return;
}
if (Cipher.OrganizationId == null && !CanAccessPremium)
{
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
return;
}
if (attachment.FileSize >= 10485760) // 10 MB
{
var confirmed = await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
AppResources.Yes, AppResources.No);
if (!confirmed)
{
return;
}
}
var canOpenFile = true;
if (!_deviceActionService.CanOpenFile(attachment.FileName))
{
if (Device.RuntimePlatform == Device.iOS)
{
// iOS is currently hardcoded to always return CanOpenFile == true, but should it ever return false
// for any reason we want to be sure to catch it here.
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
return;
}
canOpenFile = false;
}
if (!await PromptPasswordAsync())
{
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
try
{
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return;
}
if (Cipher.OrganizationId == null && !CanAccessPremium)
{
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
return;
}
if (attachment.FileSize >= 10485760) // 10 MB
{
var confirmed = await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
AppResources.Yes, AppResources.No);
if (!confirmed)
{
return;
}
}
var canOpenFile = true;
if (!_deviceActionService.CanOpenFile(attachment.FileName))
{
if (Device.RuntimePlatform == Device.iOS)
{
// iOS is currently hardcoded to always return CanOpenFile == true, but should it ever return false
// for any reason we want to be sure to catch it here.
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
return;
}
canOpenFile = false;
}
if (!await PromptPasswordAsync())
{
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
var data = await _cipherService.DownloadAndDecryptAttachmentAsync(Cipher.Id, attachment, Cipher.OrganizationId);
await _deviceActionService.HideLoadingAsync();
if (data == null)
@@ -558,9 +532,11 @@ namespace Bit.App.Pages
OpenAttachment(data, attachment);
}
}
catch
catch (Exception ex)
{
_logger.Exception(ex);
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
}
}
@@ -617,7 +593,7 @@ namespace Bit.App.Pages
_attachmentFilename = null;
}
private async void CopyAsync(string id, string text = null)
private async Task CopyAsync(string id, string text = null)
{
if (_passwordRepromptService.ProtectedFields.Contains(id) && !await PromptPasswordAsync())
{
@@ -637,7 +613,7 @@ namespace Bit.App.Pages
}
else if (id == "LoginTotp")
{
text = _totpCode;
text = TotpCodeFormatted.Replace(" ", string.Empty);
name = AppResources.VerificationCodeTotp;
}
else if (id == "LoginUri")
@@ -681,16 +657,6 @@ namespace Bit.App.Pages
}
}
private void CopyUri(LoginUriView uri)
{
CopyAsync("LoginUri", uri.Uri);
}
private void CopyField(FieldView field)
{
CopyAsync(field.Type == Core.Enums.FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value);
}
private void LaunchUri(LoginUriView uri)
{
if (uri.CanLaunch && (Page as BaseContentPage).DoOnce())
@@ -710,15 +676,15 @@ namespace Bit.App.Pages
}
}
public class ViewPageFieldViewModel : ExtendedViewModel
public class CipherDetailsPageFieldViewModel : ExtendedViewModel
{
private II18nService _i18nService;
private ViewPageViewModel _vm;
private CipherDetailsPageViewModel _vm;
private FieldView _field;
private CipherView _cipher;
private bool _showHiddenValue;
public ViewPageFieldViewModel(ViewPageViewModel vm, CipherView cipher, FieldView field)
public CipherDetailsPageFieldViewModel(CipherDetailsPageViewModel vm, CipherView cipher, FieldView field)
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_vm = vm;
@@ -734,6 +700,7 @@ namespace Bit.App.Pages
additionalPropertyNames: new string[]
{
nameof(ValueText),
nameof(ValueAccessibilityText),
nameof(IsBooleanType),
nameof(IsHiddenType),
nameof(IsTextType),
@@ -757,7 +724,7 @@ namespace Bit.App.Pages
{
if (IsBooleanType)
{
return _field.Value == "true" ? BitwardenIcons.Square : BitwardenIcons.CheckSquare;
return _field.BoolValue ? BitwardenIcons.CheckSquare : BitwardenIcons.Square;
}
else if (IsLinkedType)
{
@@ -771,6 +738,19 @@ namespace Bit.App.Pages
}
}
public string ValueAccessibilityText
{
get
{
if (IsBooleanType)
{
return _field.BoolValue ? AppResources.Enabled : AppResources.Disabled;
}
return ValueText;
}
}
public FormattedString ColoredHiddenValue => PasswordFormatter.FormatPassword(_field.Value);
public Command ToggleHiddenValueCommand { get; set; }

View File

@@ -3,22 +3,18 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
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.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class CiphersPageViewModel : BaseViewModel
public class CiphersPageViewModel : VaultFilterViewModel
{
private readonly IPlatformUtilsService _platformUtilsService;
private readonly ICipherService _cipherService;
@@ -31,12 +27,9 @@ namespace Bit.App.Pages
private CancellationTokenSource _searchCancellationTokenSource;
private readonly ILogger _logger;
private bool _showVaultFilter;
private string _vaultFilterSelection;
private bool _showNoData;
private bool _showList;
private bool _websiteIconsEnabled;
private List<Organization> _organizations;
public CiphersPageViewModel()
{
@@ -52,18 +45,19 @@ namespace Bit.App.Pages
Ciphers = new ExtendedObservableCollection<CipherView>();
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
onException: ex => _logger.Exception(ex),
allowsMultipleExecutions: false);
}
public Command CipherOptionsCommand { get; set; }
public ICommand VaultFilterCommand { get; }
public ExtendedObservableCollection<CipherView> Ciphers { get; set; }
public Func<CipherView, bool> Filter { get; set; }
public string AutofillUrl { get; set; }
public bool Deleted { get; set; }
protected override ICipherService cipherService => _cipherService;
protected override IPolicyService policyService => _policyService;
protected override IOrganizationService organizationService => _organizationService;
protected override ILogger logger => _logger;
public bool ShowNoData
{
get => _showNoData;
@@ -81,23 +75,6 @@ namespace Bit.App.Pages
nameof(ShowSearchDirection)
});
}
public bool ShowVaultFilter
{
get => _showVaultFilter;
set => SetProperty(ref _showVaultFilter, value);
}
public string VaultFilterDescription
{
get
{
if (_vaultFilterSelection == null || _vaultFilterSelection == AppResources.AllVaults)
{
return string.Format(AppResources.VaultFilterDescription, AppResources.All);
}
return string.Format(AppResources.VaultFilterDescription, _vaultFilterSelection);
}
set => SetProperty(ref _vaultFilterSelection, value);
}
public bool ShowSearchDirection => !ShowList && !ShowNoData;
@@ -109,12 +86,7 @@ namespace Bit.App.Pages
public async Task InitAsync()
{
_organizations = await _organizationService.GetAllAsync();
ShowVaultFilter = await _policyService.ShouldShowVaultFilterAsync();
if (ShowVaultFilter && _vaultFilterSelection == null)
{
_vaultFilterSelection = AppResources.AllVaults;
}
await InitVaultFilterAsync(true);
WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
PerformSearchIfPopulated();
}
@@ -184,7 +156,7 @@ namespace Bit.App.Pages
}
if (selection == AppResources.View || string.IsNullOrWhiteSpace(AutofillUrl))
{
var page = new ViewPage(cipher.Id);
var page = new CipherDetailsPage(cipher.Id);
await Page.Navigation.PushModalAsync(new NavigationPage(page));
}
else if (selection == AppResources.Autofill || selection == AppResources.AutofillAndSave)
@@ -237,50 +209,11 @@ namespace Bit.App.Pages
}
}
private async Task VaultFilterOptionsAsync()
protected override async Task OnVaultFilterSelectedAsync()
{
var options = new List<string> { AppResources.AllVaults, AppResources.MyVault };
if (_organizations.Any())
{
options.AddRange(_organizations.OrderBy(o => o.Name).Select(o => o.Name));
}
var selection = await Page.DisplayActionSheet(AppResources.FilterByVault, AppResources.Cancel, null,
options.ToArray());
if (selection == null || selection == AppResources.Cancel ||
(_vaultFilterSelection == null && selection == AppResources.AllVaults) ||
(_vaultFilterSelection != null && _vaultFilterSelection == selection))
{
return;
}
VaultFilterDescription = selection;
PerformSearchIfPopulated();
}
private async Task<List<CipherView>> GetAllCiphersAsync()
{
var decCiphers = await _cipherService.GetAllDecryptedAsync();
if (IsVaultFilterMyVault)
{
return decCiphers.Where(c => c.OrganizationId == null).ToList();
}
if (IsVaultFilterOrgVault)
{
var orgId = GetVaultFilterOrgId();
return decCiphers.Where(c => c.OrganizationId == orgId).ToList();
}
return decCiphers;
}
private bool IsVaultFilterMyVault => _vaultFilterSelection == AppResources.MyVault;
private bool IsVaultFilterOrgVault => _vaultFilterSelection != AppResources.AllVaults &&
_vaultFilterSelection != AppResources.MyVault;
private string GetVaultFilterOrgId()
{
return _organizations?.FirstOrDefault(o => o.Name == _vaultFilterSelection)?.Id;
}
private async void CipherOptionsAsync(CipherView cipher)
{
if ((Page as BaseContentPage).DoOnce())

View File

@@ -6,6 +6,7 @@
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:GroupingsPageViewModel"
Title="{Binding PageTitle}"
@@ -53,6 +54,14 @@
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" />
</DataTemplate>
<DataTemplate x:Key="authenticatorTemplate"
x:DataType="pages:GroupingsPageTOTPListItem">
<controls:AuthenticatorViewCell
Cipher="{Binding Cipher}"
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}"
TotpSec="{Binding TotpSec}"/>
</DataTemplate>
<DataTemplate x:Key="groupTemplate"
x:DataType="pages:GroupingsPageListItem">
<controls:ExtendedStackLayout Orientation="Horizontal"
@@ -104,6 +113,7 @@
<pages:GroupingsPageListItemSelector x:Key="listItemDataTemplateSelector"
HeaderTemplate="{StaticResource headerTemplate}"
CipherTemplate="{StaticResource cipherTemplate}"
AuthenticatorTemplate="{StaticResource authenticatorTemplate}"
GroupTemplate="{StaticResource groupTemplate}" />
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
@@ -130,7 +140,6 @@
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Filter}" />
</StackLayout>
<StackLayout
VerticalOptions="CenterAndExpand"
Padding="20, 0"

View File

@@ -7,6 +7,7 @@ using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -21,6 +22,7 @@ namespace Bit.App.Pages
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ICipherService _cipherService;
private readonly IDeviceActionService _deviceActionService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly GroupingsPageViewModel _vm;
private readonly string _pageName;
@@ -28,7 +30,7 @@ namespace Bit.App.Pages
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
string collectionId = null, string pageTitle = null, string vaultFilterSelection = null,
PreviousPageInfo previousPage = null, bool deleted = false)
PreviousPageInfo previousPage = null, bool deleted = false, bool showTotp = false)
{
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
InitializeComponent();
@@ -40,6 +42,7 @@ namespace Bit.App.Pages
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_vm = BindingContext as GroupingsPageViewModel;
_vm.Page = this;
_vm.MainPage = mainPage;
@@ -47,6 +50,7 @@ namespace Bit.App.Pages
_vm.FolderId = folderId;
_vm.CollectionId = collectionId;
_vm.Deleted = deleted;
_vm.ShowTotp = showTotp;
_previousPage = previousPage;
if (pageTitle != null)
{
@@ -68,7 +72,7 @@ namespace Bit.App.Pages
ToolbarItems.Add(_lockItem);
ToolbarItems.Add(_exitItem);
}
if (deleted)
if (deleted || showTotp)
{
_absLayout.Children.Remove(_fab);
ToolbarItems.Remove(_addItem);
@@ -95,21 +99,28 @@ namespace Bit.App.Pages
_broadcasterService.Subscribe(_pageName, async (message) =>
{
if (message.Command == "syncStarted")
try
{
Device.BeginInvokeOnMainThread(() => IsBusy = true);
}
else if (message.Command == "syncCompleted")
{
await Task.Delay(500);
Device.BeginInvokeOnMainThread(() =>
if (message.Command == "syncStarted")
{
IsBusy = false;
if (_vm.LoadedOnce)
Device.BeginInvokeOnMainThread(() => IsBusy = true);
}
else if (message.Command == "syncCompleted")
{
await Task.Delay(500);
Device.BeginInvokeOnMainThread(() =>
{
var task = _vm.LoadAsync();
}
});
IsBusy = false;
if (_vm.LoadedOnce)
{
var task = _vm.LoadAsync();
}
});
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
@@ -181,10 +192,11 @@ namespace Bit.App.Pages
return false;
}
protected override void OnDisappearing()
protected override async void OnDisappearing()
{
base.OnDisappearing();
IsBusy = false;
_vm.StopCiphersTotpTick().FireAndForget();
_broadcasterService.Unsubscribe(_pageName);
_vm.DisableRefreshing();
_accountAvatar?.OnDisappearing();
@@ -192,35 +204,54 @@ namespace Bit.App.Pages
private async void RowSelected(object sender, SelectionChangedEventArgs e)
{
((ExtendedCollectionView)sender).SelectedItem = null;
if (!DoOnce())
try
{
return;
}
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
{
return;
}
((ExtendedCollectionView)sender).SelectedItem = null;
if (!DoOnce())
{
return;
}
if (item.IsTrash)
{
await _vm.SelectTrashAsync();
if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageTOTPListItem totpItem)
{
await _vm.SelectCipherAsync(totpItem.Cipher);
return;
}
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
{
return;
}
if (item.IsTrash)
{
await _vm.SelectTrashAsync();
}
else if (item.IsTotpCode)
{
await _vm.SelectTotpCodesAsync();
}
else if (item.Cipher != null)
{
await _vm.SelectCipherAsync(item.Cipher);
}
else if (item.Folder != null)
{
await _vm.SelectFolderAsync(item.Folder);
}
else if (item.Collection != null)
{
await _vm.SelectCollectionAsync(item.Collection);
}
else if (item.Type != null)
{
await _vm.SelectTypeAsync(item.Type.Value);
}
}
else if (item.Cipher != null)
catch (Exception ex)
{
await _vm.SelectCipherAsync(item.Cipher);
}
else if (item.Folder != null)
{
await _vm.SelectFolderAsync(item.Folder);
}
else if (item.Collection != null)
{
await _vm.SelectCollectionAsync(item.Collection);
}
else if (item.Type != null)
{
await _vm.SelectTypeAsync(item.Type.Value);
LoggerHelper.LogEvenIfCantBeResolved(ex);
_platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
}
}
@@ -263,7 +294,7 @@ namespace Bit.App.Pages
}
if (!_vm.Deleted && DoOnce())
{
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
var page = new CipherAddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
@@ -277,11 +308,11 @@ namespace Bit.App.Pages
await _accountListOverlay.HideAsync();
if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
{
await Navigation.PushModalAsync(new NavigationPage(new ViewPage(_previousPage.CipherId)));
await Navigation.PushModalAsync(new NavigationPage(new CipherDetailsPage(_previousPage.CipherId)));
}
else if (_previousPage.Page == "edit" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
{
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_previousPage.CipherId)));
await Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(_previousPage.CipherId)));
}
_previousPage = null;
}

View File

@@ -2,13 +2,13 @@
namespace Bit.App.Pages
{
public class GroupingsPageListGroup : List<GroupingsPageListItem>
public class GroupingsPageListGroup : List<IGroupingsPageListItem>
{
public GroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false)
: this(new List<GroupingsPageListItem>(), name, count, doUpper, first)
: this(new List<IGroupingsPageListItem>(), name, count, doUpper, first)
{ }
public GroupingsPageListGroup(List<GroupingsPageListItem> groupItems, string name, int count,
public GroupingsPageListGroup(IEnumerable<IGroupingsPageListItem> groupItems, string name, int count,
bool doUpper = true, bool first = false)
{
AddRange(groupItems);

View File

@@ -17,6 +17,7 @@ namespace Bit.App.Pages
public string ItemCount { get; set; }
public bool FuzzyAutofill { get; set; }
public bool IsTrash { get; set; }
public bool IsTotpCode { get; set; }
public string Name
{
@@ -38,6 +39,10 @@ namespace Bit.App.Pages
{
_name = Collection.Name;
}
else if (IsTotpCode)
{
_name = AppResources.VerificationCodes;
}
else if (Type != null)
{
switch (Type.Value)
@@ -82,6 +87,10 @@ namespace Bit.App.Pages
{
_icon = BitwardenIcons.Collection;
}
else if (IsTotpCode)
{
_icon = BitwardenIcons.Clock;
}
else if (Type != null)
{
switch (Type.Value)

View File

@@ -7,6 +7,7 @@ namespace Bit.App.Pages
public DataTemplate HeaderTemplate { get; set; }
public DataTemplate CipherTemplate { get; set; }
public DataTemplate GroupTemplate { get; set; }
public DataTemplate AuthenticatorTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
@@ -15,10 +16,16 @@ namespace Bit.App.Pages
return HeaderTemplate;
}
if (item is GroupingsPageTOTPListItem)
{
return AuthenticatorTemplate;
}
if (item is GroupingsPageListItem listItem)
{
return listItem.Cipher != null ? CipherTemplate : GroupTemplate;
}
return null;
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Threading.Tasks;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class GroupingsPageTOTPListItem : ExtendedViewModel, IGroupingsPageListItem
{
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private readonly ITotpService _totpService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IClipboardService _clipboardService;
private CipherView _cipher;
private bool _websiteIconsEnabled;
private string _iconImageSource = string.Empty;
public int interval { get; set; }
private double _progress;
private string _totpSec;
private string _totpCodeFormatted;
private TotpHelper _totpTickHelper;
public GroupingsPageTOTPListItem(CipherView cipherView, bool websiteIconsEnabled)
{
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
Cipher = cipherView;
WebsiteIconsEnabled = websiteIconsEnabled;
interval = _totpService.GetTimeInterval(Cipher.Login.Totp);
CopyCommand = new AsyncCommand(CopyToClipboardAsync,
onException: ex => _logger.Value.Exception(ex),
allowsMultipleExecutions: false);
_totpTickHelper = new TotpHelper(cipherView);
}
public AsyncCommand CopyCommand { get; set; }
public CipherView Cipher
{
get => _cipher;
set => SetProperty(ref _cipher, value);
}
public string TotpCodeFormatted
{
get => _totpCodeFormatted;
set => SetProperty(ref _totpCodeFormatted, value,
additionalPropertyNames: new string[]
{
nameof(TotpCodeFormattedStart),
nameof(TotpCodeFormattedEnd),
});
}
public string TotpSec
{
get => _totpSec;
set => SetProperty(ref _totpSec, value);
}
public double Progress
{
get => _progress;
set => SetProperty(ref _progress, value);
}
public bool WebsiteIconsEnabled
{
get => _websiteIconsEnabled;
set => SetProperty(ref _websiteIconsEnabled, value);
}
public bool ShowIconImage
{
get => WebsiteIconsEnabled
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
&& IconImageSource != null;
}
public string IconImageSource
{
get
{
if (_iconImageSource == string.Empty) // default value since icon source can return null
{
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
}
return _iconImageSource;
}
}
public string TotpCodeFormattedStart => TotpCodeFormatted?.Split(' ')[0];
public string TotpCodeFormattedEnd => TotpCodeFormatted?.Split(' ')[1];
public async Task CopyToClipboardAsync()
{
await _clipboardService.CopyTextAsync(TotpCodeFormatted?.Replace(" ", string.Empty));
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
}
public async Task TotpTickAsync()
{
await _totpTickHelper.GenerateNewTotpValues();
MainThread.BeginInvokeOnMainThread(() =>
{
TotpSec = _totpTickHelper.TotpSec;
Progress = _totpTickHelper.Progress;
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
});
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
@@ -17,7 +18,7 @@ using Xamarin.Forms;
namespace Bit.App.Pages
{
public class GroupingsPageViewModel : BaseViewModel
public class GroupingsPageViewModel : VaultFilterViewModel
{
private const int NoFolderListSize = 100;
@@ -30,16 +31,16 @@ namespace Bit.App.Pages
private bool _showList;
private bool _websiteIconsEnabled;
private bool _syncRefreshing;
private bool _showVaultFilter;
private string _vaultFilterSelection;
private bool _showTotpFilter;
private bool _totpFilterEnable;
private string _noDataText;
private List<Organization> _organizations;
private List<CipherView> _allCiphers;
private Dictionary<string, int> _folderCounts = new Dictionary<string, int>();
private Dictionary<string, int> _collectionCounts = new Dictionary<string, int>();
private Dictionary<CipherType, int> _typeCounts = new Dictionary<CipherType, int>();
private int _deletedCount = 0;
private CancellationTokenSource _totpTickCts;
private Task _totpTickTask;
private readonly ICipherService _cipherService;
private readonly IFolderService _folderService;
private readonly ICollectionService _collectionService;
@@ -101,6 +102,7 @@ namespace Bit.App.Pages
&& NoFolderCiphers.Count < NoFolderListSize
&& (Collections is null || !Collections.Any());
public List<CipherView> Ciphers { get; set; }
public List<CipherView> TOTPCiphers { get; set; }
public List<CipherView> FavoriteCiphers { get; set; }
public List<CipherView> NoFolderCiphers { get; set; }
public List<FolderView> Folders { get; set; }
@@ -108,6 +110,11 @@ namespace Bit.App.Pages
public List<Core.Models.View.CollectionView> Collections { get; set; }
public List<TreeNode<Core.Models.View.CollectionView>> NestedCollections { get; set; }
protected override ICipherService cipherService => _cipherService;
protected override IPolicyService policyService => _policyService;
protected override IOrganizationService organizationService => _organizationService;
protected override ILogger logger => _logger;
public bool Refreshing
{
get => _refreshing;
@@ -153,30 +160,15 @@ namespace Bit.App.Pages
get => _websiteIconsEnabled;
set => SetProperty(ref _websiteIconsEnabled, value);
}
public bool ShowVaultFilter
public bool ShowTotp
{
get => _showVaultFilter;
set => SetProperty(ref _showVaultFilter, value);
get => _showTotpFilter;
set => SetProperty(ref _showTotpFilter, value);
}
public string VaultFilterDescription
{
get
{
if (_vaultFilterSelection == null || _vaultFilterSelection == AppResources.AllVaults)
{
return string.Format(AppResources.VaultFilterDescription, AppResources.All);
}
return string.Format(AppResources.VaultFilterDescription, _vaultFilterSelection);
}
set => SetProperty(ref _vaultFilterSelection, value);
}
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
public Command RefreshCommand { get; set; }
public Command<CipherView> CipherOptionsCommand { get; set; }
public ICommand VaultFilterCommand { get; }
public bool LoadedOnce { get; set; }
public async Task LoadAsync()
@@ -201,23 +193,20 @@ namespace Bit.App.Pages
return;
}
_organizations = await _organizationService.GetAllAsync();
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget();
await InitVaultFilterAsync(MainPage);
if (MainPage)
{
ShowVaultFilter = await _policyService.ShouldShowVaultFilterAsync();
if (ShowVaultFilter && _vaultFilterSelection == null)
{
_vaultFilterSelection = AppResources.AllVaults;
}
PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault;
}
_doingLoad = true;
LoadedOnce = true;
ShowNoData = false;
Loading = true;
ShowList = false;
ShowAddCipherButton = !Deleted;
var groupedItems = new List<GroupingsPageListGroup>();
var page = Page as GroupingsPage;
@@ -241,6 +230,8 @@ namespace Bit.App.Pages
}
if (MainPage)
{
AddTotpGroupItem(groupedItems, uppercaseGroupNames);
groupedItems.Add(new GroupingsPageListGroup(
AppResources.Types, 4, uppercaseGroupNames, !hasFavorites)
{
@@ -297,10 +288,12 @@ namespace Bit.App.Pages
}
if (Ciphers?.Any() ?? false)
{
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted)
.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
CreateCipherGroupedItems(groupedItems);
}
if (ShowTotp && (!TOTPCiphers?.Any() ?? false))
{
Page.Navigation.PopAsync();
return;
}
if (ShowNoFolderCipherGroup)
{
@@ -388,39 +381,74 @@ namespace Bit.App.Pages
}
}
private void AddTotpGroupItem(List<GroupingsPageListGroup> groupedItems, bool uppercaseGroupNames)
{
if (TOTPCiphers?.Any() == true)
{
groupedItems.Insert(0, new GroupingsPageListGroup(
AppResources.Totp, 1, uppercaseGroupNames, false)
{
new GroupingsPageListItem
{
IsTotpCode = true,
Type = CipherType.Login,
ItemCount = TOTPCiphers.Count().ToString("N0")
}
});
}
}
private void CreateCipherGroupedItems(List<GroupingsPageListGroup> groupedItems)
{
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
_totpTickCts?.Cancel();
if (ShowTotp)
{
var ciphersListItems = TOTPCiphers.Select(c => new GroupingsPageTOTPListItem(c, true)).ToList();
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
StartCiphersTotpTick(ciphersListItems);
}
else
{
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted)
.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
}
}
private void StartCiphersTotpTick(List<GroupingsPageTOTPListItem> ciphersListItems)
{
_totpTickCts?.Cancel();
_totpTickCts = new CancellationTokenSource();
_totpTickTask = new TimerTask(logger, () => ciphersListItems.ForEach(i => i.TotpTickAsync()), _totpTickCts).RunPeriodic();
}
public async Task StopCiphersTotpTick()
{
_totpTickCts?.Cancel();
if (_totpTickTask != null)
{
await _totpTickTask;
}
}
public void DisableRefreshing()
{
Refreshing = false;
SyncRefreshing = false;
}
public async Task VaultFilterOptionsAsync()
protected override async Task OnVaultFilterSelectedAsync()
{
var options = new List<string> { AppResources.AllVaults, AppResources.MyVault };
if (_organizations.Any())
{
options.AddRange(_organizations.OrderBy(o => o.Name).Select(o => o.Name));
}
var selection = await Page.DisplayActionSheet(AppResources.FilterByVault, AppResources.Cancel, null,
options.ToArray());
if (selection == null || selection == AppResources.Cancel ||
(_vaultFilterSelection == null && selection == AppResources.AllVaults) ||
(_vaultFilterSelection != null && _vaultFilterSelection == selection))
{
return;
}
VaultFilterDescription = selection;
await LoadAsync();
}
public string GetVaultFilterOrgId()
{
return _organizations?.FirstOrDefault(o => o.Name == _vaultFilterSelection)?.Id;
}
public async Task SelectCipherAsync(CipherView cipher)
{
var page = new ViewPage(cipher.Id);
var page = new CipherDetailsPage(cipher.Id);
await Page.Navigation.PushModalAsync(new NavigationPage(page));
}
@@ -467,6 +495,13 @@ namespace Bit.App.Pages
await Page.Navigation.PushAsync(page);
}
public async Task SelectTotpCodesAsync()
{
var page = new GroupingsPage(false, CipherType.Login, null, null, AppResources.VerificationCodes, _vaultFilterSelection, null,
false, true);
await Page.Navigation.PushAsync(page);
}
public async Task ExitAsync()
{
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ExitConfirmation,
@@ -501,9 +536,11 @@ namespace Bit.App.Pages
private async Task LoadDataAsync()
{
var orgId = await FillAllCiphersAndGetOrgIdIfNeededAsync();
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
NoDataText = AppResources.NoItems;
_allCiphers = await GetAllCiphersAsync();
HasCiphers = _allCiphers.Any();
TOTPCiphers = _allCiphers.Where(c => c.IsDeleted == Deleted && c.Type == CipherType.Login && !string.IsNullOrEmpty(c.Login?.Totp) && (c.OrganizationUseTotp || canAccessPremium)).ToList();
FavoriteCiphers?.Clear();
NoFolderCiphers?.Clear();
_folderCounts.Clear();
@@ -516,7 +553,7 @@ namespace Bit.App.Pages
if (MainPage)
{
await FillFoldersAndCollectionsAsync(orgId);
await FillFoldersAndCollectionsAsync();
NestedFolders = await _folderService.GetAllNestedAsync(Folders);
HasFolders = NestedFolders.Any(f => f.Node?.Id != null);
NestedCollections = Collections != null ? await _collectionService.GetAllNestedAsync(Collections) : null;
@@ -529,6 +566,10 @@ namespace Bit.App.Pages
Filter = c => c.IsDeleted;
NoDataText = AppResources.NoItemsTrash;
}
else if (ShowTotp)
{
Filter = c => c.Type == CipherType.Login && !c.IsDeleted && !string.IsNullOrEmpty(c.Login?.Totp);
}
else if (Type != null)
{
Filter = c => c.Type == Type.Value && !c.IsDeleted;
@@ -640,28 +681,9 @@ namespace Bit.App.Pages
}
}
private async Task<string> FillAllCiphersAndGetOrgIdIfNeededAsync()
{
string orgId = null;
var decCiphers = await _cipherService.GetAllDecryptedAsync();
if (IsVaultFilterMyVault)
{
_allCiphers = decCiphers.Where(c => c.OrganizationId == null).ToList();
}
else if (IsVaultFilterOrgVault)
{
orgId = GetVaultFilterOrgId();
_allCiphers = decCiphers.Where(c => c.OrganizationId == orgId).ToList();
}
else
{
_allCiphers = decCiphers;
}
return orgId;
}
private async Task FillFoldersAndCollectionsAsync(string orgId)
private async Task FillFoldersAndCollectionsAsync()
{
var orgId = GetVaultFilterOrgId();
var decFolders = await _folderService.GetAllDecryptedAsync();
var decCollections = await _collectionService.GetAllDecryptedAsync();
if (IsVaultFilterMyVault)
@@ -681,11 +703,6 @@ namespace Bit.App.Pages
}
}
private bool IsVaultFilterMyVault => _vaultFilterSelection == AppResources.MyVault;
private bool IsVaultFilterOrgVault => _vaultFilterSelection != AppResources.AllVaults &&
_vaultFilterSelection != AppResources.MyVault;
private List<FolderView> BuildFolders(List<FolderView> decFolders)
{
var folders = decFolders.Where(f => _allCiphers.Any(c => c.FolderId == f.Id)).ToList();

View File

@@ -1,13 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage
<?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.ScanPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:forms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms"
x:Name="_page"
Title="{u:I18n ScanQrTitle}">
Title="{Binding ScanQrPageTitle}">
<ContentPage.BindingContext>
<pages:ScanPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
@@ -16,67 +29,114 @@
<Grid
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<zxing:ZXingScannerView
x:Name="_zxing"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
AutomationId="zxingScannerView"
OnScanResult="OnScanResult">
</zxing:ZXingScannerView>
<Grid
VerticalOptions="FillAndExpand"
IsVisible="{Binding ShowScanner}"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="3"
OnScanResult="OnScanResult"/>
<StackLayout
VerticalOptions="Center"
HorizontalOptions="FillAndExpand"
AutomationId="zxingDefaultOverlay">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
IsVisible="{Binding ShowScanner}"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
Margin="30,0">
<BoxView
Grid.Column="0"
Grid.Row="0"
VerticalOptions="Fill"
HorizontalOptions="FillAndExpand"
BackgroundColor="Black"
Opacity="0.7" />
<Label
Text="{u:I18n CameraInstructionTop}"
AutomationId="zxingDefaultOverlay_TopTextLabel"
Grid.Column="0"
Grid.Row="0"
VerticalOptions="Center"
<forms:SKCanvasView
x:Name="SkCanvasView"
Margin="0,50,0,0"
WidthRequest="250"
HeightRequest="250"
IsVisible="{Binding ShowScanner}"
VerticalOptions="Center"
HorizontalOptions="Center"
TextColor="White" />
PaintSurface="OnCanvasViewPaintSurface"/>
<BoxView
Grid.Column="0"
Grid.Row="1"
VerticalOptions="Fill"
<controls:IconButton
x:Name="_checkIcon"
IsVisible="{Binding ShowScanner}"
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
HorizontalOptions="Center"
VerticalOptions="Start"
FontSize="Title"
TextColor="Transparent"/>
</StackLayout>
<BoxView
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
IsVisible="{Binding ShowScanner, Converter={StaticResource inverseBool}}"
BackgroundColor="{DynamicResource BackgroundColor}"/>
<StackLayout
VerticalOptions="Center"
HorizontalOptions="FillAndExpand"
BackgroundColor="Transparent" />
<BoxView
IsVisible="{Binding ShowScanner, Converter={StaticResource inverseBool}}"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
Margin="30,0">
<Label
Text="{u:I18n EnterKeyManually}"
FontSize="Title" />
<Label
Text="{u:I18n AuthenticatorKeyScanner}"
StyleClass="box-label" />
<controls:MonoEntry
x:Name="_authenticationKeyEntry"
Text="{Binding TotpAuthenticationKey}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
StyleClass="box-value" />
<Button
Text="{u:I18n AddTotp}"
StyleClass="box-button-row"
Clicked="AddAuthenticationKey_OnClicked"/>
</StackLayout>
<BoxView
Grid.Column="0"
Grid.Row="2"
VerticalOptions="Fill"
HorizontalOptions="FillAndExpand"
BackgroundColor="Black"
Opacity="0.7" />
<StackLayout
VerticalOptions="Start"
HorizontalOptions="Center"
Grid.Column="0"
Grid.Row="2">
<Label
Text="{u:I18n CameraInstructionBottom}"
AutomationId="zxingDefaultOverlay_BottomTextLabel"
Text="{Binding CameraInstructionTop}"
AutomationId="zxingDefaultOverlay_TopTextLabel"
Margin="30,15,30,0"
HorizontalOptions="Center"
HorizontalTextAlignment="Center"
StyleClass="text-sm"
TextColor="White" />
</StackLayout>
<Label
FormattedText="{Binding ToggleScanModeLabel}"
Grid.Column="0"
Grid.Row="2"
VerticalOptions="Center"
HorizontalOptions="Center"
TextColor="White" />
</Grid>
Margin="0,15"
StyleClass="text-sm"
FontAttributes="Bold"
VerticalOptions="End"
HorizontalOptions="Center" >
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="ToggleScanMode_OnTapped" />
</Label.GestureRecognizers>
</Label>
</Grid>
</pages:BaseContentPage>
</pages:BaseContentPage>

View File

@@ -1,19 +1,31 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using SkiaSharp;
using SkiaSharp.Views.Forms;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class ScanPage : BaseContentPage
{
private ScanPageViewModel ViewModel => BindingContext as ScanPageViewModel;
private readonly Action<string> _callback;
private CancellationTokenSource _autofocusCts;
private Task _continuousAutofocusTask;
private readonly Color _greenColor;
private readonly SKColor _blueSKColor;
private readonly SKColor _greenSKColor;
private readonly Stopwatch _stopwatch;
private bool _pageIsActive;
private bool _qrcodeFound;
private float _scale;
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
@@ -32,6 +44,12 @@ namespace Bit.App.Pages
{
ToolbarItems.RemoveAt(0);
}
_greenColor = ThemeManager.GetResourceColor("SuccessColor");
_greenSKColor = _greenColor.ToSKColor();
_blueSKColor = ThemeManager.GetResourceColor("PrimaryColor").ToSKColor();
_stopwatch = new Stopwatch();
_qrcodeFound = false;
}
protected override void OnAppearing()
@@ -58,7 +76,14 @@ namespace Bit.App.Pages
{
if (!autofocusCts.IsCancellationRequested)
{
_zxing.AutoFocus();
try
{
_zxing.AutoFocus();
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
}
}
});
}
@@ -69,49 +94,70 @@ namespace Bit.App.Pages
_logger.Value.Exception(ex);
}
}, autofocusCts.Token);
_pageIsActive = true;
AnimationLoopAsync();
}
protected override async void OnDisappearing()
{
_autofocusCts?.Cancel();
if (_continuousAutofocusTask != null)
{
await _continuousAutofocusTask;
}
_zxing.IsScanning = false;
_pageIsActive = false;
base.OnDisappearing();
}
private void OnScanResult(ZXing.Result result)
private async void OnScanResult(ZXing.Result result)
{
// Stop analysis until we navigate away so we don't keep reading barcodes
_zxing.IsAnalyzing = false;
_zxing.IsScanning = false;
var text = result?.Text;
if (!string.IsNullOrWhiteSpace(text))
try
{
if (text.StartsWith("otpauth://totp"))
// Stop analysis until we navigate away so we don't keep reading barcodes
_zxing.IsAnalyzing = false;
var text = result?.Text;
if (!string.IsNullOrWhiteSpace(text))
{
_callback(text);
return;
}
else if (Uri.TryCreate(text, UriKind.Absolute, out Uri uri) &&
!string.IsNullOrWhiteSpace(uri?.Query))
{
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
foreach (var part in queryParts)
if (text.StartsWith("otpauth://totp"))
{
if (part.StartsWith("secret="))
await QrCodeFoundAsync();
_callback(text);
return;
}
else if (Uri.TryCreate(text, UriKind.Absolute, out Uri uri) &&
!string.IsNullOrWhiteSpace(uri?.Query))
{
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
foreach (var part in queryParts)
{
_callback(part.Substring(7)?.ToUpperInvariant());
return;
if (part.StartsWith("secret="))
{
await QrCodeFoundAsync();
var subResult = part.Substring(7);
if (!string.IsNullOrEmpty(subResult))
{
_callback(subResult.ToUpperInvariant());
}
return;
}
}
}
}
_callback(null);
}
_callback(null);
catch (Exception ex)
{
_logger?.Value?.Exception(ex);
}
}
private async Task QrCodeFoundAsync()
{
_qrcodeFound = true;
Vibration.Vibrate();
await Task.Delay(1000);
_zxing.IsScanning = false;
}
private async void Close_Clicked(object sender, System.EventArgs e)
@@ -121,5 +167,89 @@ namespace Bit.App.Pages
await Navigation.PopModalAsync();
}
}
private void AddAuthenticationKey_OnClicked(object sender, EventArgs e)
{
if (!string.IsNullOrWhiteSpace(ViewModel.TotpAuthenticationKey))
{
_callback(ViewModel.TotpAuthenticationKey);
return;
}
_callback(null);
}
private void ToggleScanMode_OnTapped(object sender, EventArgs e)
{
ViewModel.ToggleScanModeCommand.Execute(null);
if (!ViewModel.ShowScanner)
{
_authenticationKeyEntry.Focus();
}
}
private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
var info = args.Info;
var surface = args.Surface;
var canvas = surface.Canvas;
var margins = 20;
var maxSquareSize = (Math.Min(info.Height, info.Width) * 0.9f - margins) * _scale;
var squareSize = maxSquareSize;
var lineSize = squareSize * 0.15f;
var startXPoint = (info.Width / 2) - (squareSize / 2);
var startYPoint = (info.Height / 2) - (squareSize / 2);
canvas.Clear(SKColors.Transparent);
using (var strokePaint = new SKPaint
{
Color = _qrcodeFound ? _greenSKColor : _blueSKColor,
StrokeWidth = 9 * _scale,
StrokeCap = SKStrokeCap.Round,
})
{
canvas.Scale(1, 1);
//top left
canvas.DrawLine(startXPoint, startYPoint, startXPoint, startYPoint + lineSize, strokePaint);
canvas.DrawLine(startXPoint, startYPoint, startXPoint + lineSize, startYPoint, strokePaint);
//bot left
canvas.DrawLine(startXPoint, startYPoint + squareSize, startXPoint, startYPoint + squareSize - lineSize, strokePaint);
canvas.DrawLine(startXPoint, startYPoint + squareSize, startXPoint + lineSize, startYPoint + squareSize, strokePaint);
//top right
canvas.DrawLine(startXPoint + squareSize, startYPoint, startXPoint + squareSize - lineSize, startYPoint, strokePaint);
canvas.DrawLine(startXPoint + squareSize, startYPoint, startXPoint + squareSize, startYPoint + lineSize, strokePaint);
//bot right
canvas.DrawLine(startXPoint + squareSize, startYPoint + squareSize, startXPoint + squareSize - lineSize, startYPoint + squareSize, strokePaint);
canvas.DrawLine(startXPoint + squareSize, startYPoint + squareSize, startXPoint + squareSize, startYPoint + squareSize - lineSize, strokePaint);
}
}
async Task AnimationLoopAsync()
{
try
{
_stopwatch.Start();
while (_pageIsActive)
{
var t = _stopwatch.Elapsed.TotalSeconds % 2 / 2;
_scale = (20 - (1 - (float)Math.Sin(4 * Math.PI * t))) / 20;
SkCanvasView.InvalidateSurface();
await Task.Delay(TimeSpan.FromSeconds(1.0 / 30));
if (_qrcodeFound && _scale > 0.98f)
{
_checkIcon.TextColor = _greenColor;
SkCanvasView.InvalidateSurface();
break;
}
}
}
catch (Exception ex)
{
_logger?.Value?.Exception(ex);
}
finally
{
_stopwatch?.Stop();
}
}
}
}

View File

@@ -0,0 +1,61 @@
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class ScanPageViewModel : BaseViewModel
{
private bool _showScanner = true;
private string _totpAuthenticationKey;
public ScanPageViewModel()
{
ToggleScanModeCommand = new Command(() => ShowScanner = !ShowScanner);
}
public Command ToggleScanModeCommand { get; set; }
public string ScanQrPageTitle => ShowScanner ? AppResources.ScanQrTitle : AppResources.AuthenticatorKeyScanner;
public string CameraInstructionTop => ShowScanner ? AppResources.PointYourCameraAtTheQRCode : AppResources.OnceTheKeyIsSuccessfullyEntered;
public string TotpAuthenticationKey
{
get => _totpAuthenticationKey;
set => SetProperty(ref _totpAuthenticationKey, value,
additionalPropertyNames: new string[]
{
nameof(ToggleScanModeLabel)
});
}
public bool ShowScanner
{
get => _showScanner;
set => SetProperty(ref _showScanner, value,
additionalPropertyNames: new string[]
{
nameof(ToggleScanModeLabel),
nameof(ScanQrPageTitle),
nameof(CameraInstructionTop)
});
}
public FormattedString ToggleScanModeLabel
{
get
{
var fs = new FormattedString();
fs.Spans.Add(new Span
{
Text = ShowScanner ? AppResources.CannotScanQRCode : AppResources.CannotAddAuthenticatorKey,
TextColor = ThemeManager.GetResourceColor("TitleTextColor")
});
fs.Spans.Add(new Span
{
Text = ShowScanner ? AppResources.EnterKeyManually : AppResources.ScanQRCode,
TextColor = ThemeManager.GetResourceColor("ScanningToggleModeTextColor")
});
return fs;
}
}
}
}

View File

@@ -0,0 +1,123 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Models.View;
using Xamarin.CommunityToolkit.ObjectModel;
namespace Bit.App.Pages
{
public abstract class VaultFilterViewModel : BaseViewModel
{
protected abstract ICipherService cipherService { get; }
protected abstract IPolicyService policyService { get; }
protected abstract IOrganizationService organizationService { get; }
protected abstract ILogger logger { get; }
protected bool _showVaultFilter;
protected bool _personalOwnershipPolicyApplies;
protected string _vaultFilterSelection;
protected List<Organization> _organizations;
public VaultFilterViewModel()
{
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
onException: ex => logger.Exception(ex),
allowsMultipleExecutions: false);
}
public ICommand VaultFilterCommand { get; set; }
public bool ShowVaultFilter
{
get => _showVaultFilter;
set => SetProperty(ref _showVaultFilter, value);
}
public string VaultFilterDescription
{
get
{
if (_vaultFilterSelection == null || _vaultFilterSelection == AppResources.AllVaults)
{
return string.Format(AppResources.VaultFilterDescription, AppResources.All);
}
return string.Format(AppResources.VaultFilterDescription, _vaultFilterSelection);
}
set => SetProperty(ref _vaultFilterSelection, value);
}
public string GetVaultFilterOrgId()
{
return _organizations?.FirstOrDefault(o => o.Name == _vaultFilterSelection)?.Id;
}
protected bool IsVaultFilterMyVault => _vaultFilterSelection == AppResources.MyVault;
protected bool IsVaultFilterOrgVault => _vaultFilterSelection != AppResources.AllVaults &&
_vaultFilterSelection != AppResources.MyVault;
protected async Task InitVaultFilterAsync(bool shouldUpdateShowVaultFilter)
{
_organizations = await organizationService.GetAllAsync();
if (_organizations?.Any() ?? false)
{
_personalOwnershipPolicyApplies = await policyService.PolicyAppliesToUser(PolicyType.PersonalOwnership);
var singleOrgPolicyApplies = await policyService.PolicyAppliesToUser(PolicyType.OnlyOrg);
if (_vaultFilterSelection == null || (_personalOwnershipPolicyApplies && singleOrgPolicyApplies))
{
VaultFilterDescription = AppResources.AllVaults;
}
}
if (shouldUpdateShowVaultFilter)
{
await Task.Delay(100);
ShowVaultFilter = await policyService.ShouldShowVaultFilterAsync();
}
}
protected async Task<List<CipherView>> GetAllCiphersAsync()
{
var decCiphers = await cipherService.GetAllDecryptedAsync();
if (IsVaultFilterMyVault)
{
return decCiphers.Where(c => c.OrganizationId == null).ToList();
}
if (IsVaultFilterOrgVault)
{
var orgId = GetVaultFilterOrgId();
return decCiphers.Where(c => c.OrganizationId == orgId).ToList();
}
return decCiphers;
}
protected async Task VaultFilterOptionsAsync()
{
var options = new List<string> { AppResources.AllVaults };
if (!_personalOwnershipPolicyApplies)
{
options.Add(AppResources.MyVault);
}
if (_organizations.Any())
{
options.AddRange(_organizations.OrderBy(o => o.Name).Select(o => o.Name));
}
var selection = await Page.DisplayActionSheet(AppResources.FilterByVault, AppResources.Cancel, null,
options.ToArray());
if (selection == null || selection == AppResources.Cancel ||
(_vaultFilterSelection == null && selection == AppResources.AllVaults) ||
(_vaultFilterSelection != null && _vaultFilterSelection == selection))
{
return;
}
VaultFilterDescription = selection;
await OnVaultFilterSelectedAsync();
}
protected abstract Task OnVaultFilterSelectedAsync();
}
}

View File

@@ -353,6 +353,12 @@ namespace Bit.App.Resources {
}
}
public static string Authenticator {
get {
return ResourceManager.GetString("Authenticator", resourceCulture);
}
}
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
@@ -1235,6 +1241,18 @@ namespace Bit.App.Resources {
}
}
public static string Off {
get {
return ResourceManager.GetString("Off", resourceCulture);
}
}
public static string On {
get {
return ResourceManager.GetString("On", resourceCulture);
}
}
public static string Status {
get {
return ResourceManager.GetString("Status", resourceCulture);
@@ -1445,15 +1463,9 @@ namespace Bit.App.Resources {
}
}
public static string CameraInstructionBottom {
public static string PointYourCameraAtTheQRCode {
get {
return ResourceManager.GetString("CameraInstructionBottom", resourceCulture);
}
}
public static string CameraInstructionTop {
get {
return ResourceManager.GetString("CameraInstructionTop", resourceCulture);
return ResourceManager.GetString("PointYourCameraAtTheQRCode", resourceCulture);
}
}
@@ -1481,15 +1493,15 @@ namespace Bit.App.Resources {
}
}
public static string DisableAutoTotpCopyDescription {
public static string CopyTotpAutomaticallyDescription {
get {
return ResourceManager.GetString("DisableAutoTotpCopyDescription", resourceCulture);
return ResourceManager.GetString("CopyTotpAutomaticallyDescription", resourceCulture);
}
}
public static string DisableAutoTotpCopy {
public static string CopyTotpAutomatically {
get {
return ResourceManager.GetString("DisableAutoTotpCopy", resourceCulture);
return ResourceManager.GetString("CopyTotpAutomatically", resourceCulture);
}
}
@@ -1919,15 +1931,15 @@ namespace Bit.App.Resources {
}
}
public static string DisableWebsiteIcons {
public static string ShowWebsiteIcons {
get {
return ResourceManager.GetString("DisableWebsiteIcons", resourceCulture);
return ResourceManager.GetString("ShowWebsiteIcons", resourceCulture);
}
}
public static string DisableWebsiteIconsDescription {
public static string ShowWebsiteIconsDescription {
get {
return ResourceManager.GetString("DisableWebsiteIconsDescription", resourceCulture);
return ResourceManager.GetString("ShowWebsiteIconsDescription", resourceCulture);
}
}
@@ -2699,6 +2711,18 @@ namespace Bit.App.Resources {
}
}
public static string DefaultDarkTheme {
get {
return ResourceManager.GetString("DefaultDarkTheme", resourceCulture);
}
}
public static string DefaultDarkThemeDescription {
get {
return ResourceManager.GetString("DefaultDarkThemeDescription", resourceCulture);
}
}
public static string CopyNotes {
get {
return ResourceManager.GetString("CopyNotes", resourceCulture);
@@ -2729,27 +2753,33 @@ namespace Bit.App.Resources {
}
}
public static string BlacklistedUris {
public static string Nord {
get {
return ResourceManager.GetString("BlacklistedUris", resourceCulture);
return ResourceManager.GetString("Nord", resourceCulture);
}
}
public static string BlacklistedUrisDescription {
public static string AutofillBlockedUris {
get {
return ResourceManager.GetString("BlacklistedUrisDescription", resourceCulture);
return ResourceManager.GetString("AutofillBlockedUris", resourceCulture);
}
}
public static string DisableSavePrompt {
public static string AutofillBlockedUrisDescription {
get {
return ResourceManager.GetString("DisableSavePrompt", resourceCulture);
return ResourceManager.GetString("AutofillBlockedUrisDescription", resourceCulture);
}
}
public static string DisableSavePromptDescription {
public static string AskToAddLogin {
get {
return ResourceManager.GetString("DisableSavePromptDescription", resourceCulture);
return ResourceManager.GetString("AskToAddLogin", resourceCulture);
}
}
public static string AskToAddLoginDescription {
get {
return ResourceManager.GetString("AskToAddLoginDescription", resourceCulture);
}
}
@@ -3977,15 +4007,15 @@ namespace Bit.App.Resources {
}
}
public static string VisibilityTogglePasswordIsVisibleActivateToHide {
public static string PasswordIsVisibleTapToHide {
get {
return ResourceManager.GetString("VisibilityTogglePasswordIsVisibleActivateToHide", resourceCulture);
return ResourceManager.GetString("PasswordIsVisibleTapToHide", resourceCulture);
}
}
public static string VisibilityTogglePasswordIsNotVisibleActivateToHide {
public static string PasswordIsNotVisibleTapToShow {
get {
return ResourceManager.GetString("VisibilityTogglePasswordIsNotVisibleActivateToHide", resourceCulture);
return ResourceManager.GetString("PasswordIsNotVisibleTapToShow", resourceCulture);
}
}
@@ -4018,5 +4048,107 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("All", resourceCulture);
}
}
public static string Totp {
get {
return ResourceManager.GetString("Totp", resourceCulture);
}
}
public static string VerificationCodes {
get {
return ResourceManager.GetString("VerificationCodes", resourceCulture);
}
}
public static string PremiumSubscriptionRequired {
get {
return ResourceManager.GetString("PremiumSubscriptionRequired", resourceCulture);
}
}
public static string CannotAddAuthenticatorKey {
get {
return ResourceManager.GetString("CannotAddAuthenticatorKey", resourceCulture);
}
}
public static string ScanQRCode {
get {
return ResourceManager.GetString("ScanQRCode", resourceCulture);
}
}
public static string CannotScanQRCode {
get {
return ResourceManager.GetString("CannotScanQRCode", resourceCulture);
}
}
public static string AuthenticatorKeyScanner {
get {
return ResourceManager.GetString("AuthenticatorKeyScanner", resourceCulture);
}
}
public static string EnterKeyManually {
get {
return ResourceManager.GetString("EnterKeyManually", resourceCulture);
}
}
public static string AddTotp {
get {
return ResourceManager.GetString("AddTotp", resourceCulture);
}
}
public static string SetupTotp {
get {
return ResourceManager.GetString("SetupTotp", resourceCulture);
}
}
public static string OnceTheKeyIsSuccessfullyEntered {
get {
return ResourceManager.GetString("OnceTheKeyIsSuccessfullyEntered", resourceCulture);
}
}
public static string SelectAddTotpToStoreTheKeySafely {
get {
return ResourceManager.GetString("SelectAddTotpToStoreTheKeySafely", resourceCulture);
}
}
public static string NeverLockWarning {
get {
return ResourceManager.GetString("NeverLockWarning", resourceCulture);
}
}
public static string EnvironmentPageUrlsError {
get {
return ResourceManager.GetString("EnvironmentPageUrlsError", resourceCulture);
}
}
public static string GenericErrorMessage {
get {
return ResourceManager.GetString("GenericErrorMessage", resourceCulture);
}
}
public static string AllowScreenCapture {
get {
return ResourceManager.GetString("AllowScreenCapture", resourceCulture);
}
}
public static string AreYouSureYouWantToEnableScreenCapture {
get {
return ResourceManager.GetString("AreYouSureYouWantToEnableScreenCapture", resourceCulture);
}
}
}
}

View File

@@ -276,16 +276,16 @@
<value>Is u seker u wil uitteken?</value>
</data>
<data name="RemoveAccount" xml:space="preserve">
<value>Remove Account</value>
<value>Verwyder rekening</value>
</data>
<data name="RemoveAccountConfirmation" xml:space="preserve">
<value>Are you sure you want to remove this account?</value>
<value>Is u seker u wil hierdie rekening verwyder?</value>
</data>
<data name="AccountAlreadyAdded" xml:space="preserve">
<value>Account Already Added</value>
<value>Rekening reeds toegevoeg</value>
</data>
<data name="SwitchToAlreadyAddedAccountConfirmation" xml:space="preserve">
<value>Would you like to switch to it now?</value>
<value>Wil u nou daarheen wissel?</value>
</data>
<data name="MasterPassword" xml:space="preserve">
<value>Hoofwagwoord</value>
@@ -299,6 +299,10 @@
<value>My kluis</value>
<comment>The title for the vault page.</comment>
</data>
<data name="Authenticator" xml:space="preserve">
<value>Authenticator</value>
<comment>Authenticator TOTP feature</comment>
</data>
<data name="Name" xml:space="preserve">
<value>Naam</value>
<comment>Label for an entity name.</comment>
@@ -770,6 +774,12 @@
<data name="Enabled" xml:space="preserve">
<value>Geaktiveer</value>
</data>
<data name="Off" xml:space="preserve">
<value>Af</value>
</data>
<data name="On" xml:space="preserve">
<value>Aan</value>
</data>
<data name="Status" xml:space="preserve">
<value>Status</value>
</data>
@@ -889,11 +899,9 @@
<data name="AuthenticatorKeyReadError" xml:space="preserve">
<value>Kan nie waarmerksleutel lees nie.</value>
</data>
<data name="CameraInstructionBottom" xml:space="preserve">
<value>Skandering gebeur outomaties.</value>
</data>
<data name="CameraInstructionTop" xml:space="preserve">
<value>Rig u kamera op die QR-kode.</value>
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
<value>Point your camera at the QR Code.
Scanning will happen automatically.</value>
</data>
<data name="ScanQrTitle" xml:space="preserve">
<value>Skandeer QR-kode</value>
@@ -907,11 +915,11 @@
<data name="CopyTotp" xml:space="preserve">
<value>Kopieer TOTP</value>
</data>
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
<value>Indien u aantekening aan n bevestigingskode gekoppel is, word die TOTP-bevestigingskode outomaties na die knipbord gekopieer by die aantekening se outovul.</value>
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
<value>Indien n aantekening n waarmerksleutel het, kopieer die TOTP-bevestigingskope na u knipbord wanneer u die aantekening outo-invul.</value>
</data>
<data name="DisableAutoTotpCopy" xml:space="preserve">
<value>Deaktiveer outomatiese kopieëring van TOTP</value>
<data name="CopyTotpAutomatically" xml:space="preserve">
<value>Kopieer TOTP outomaties</value>
</data>
<data name="PremiumRequired" xml:space="preserve">
<value>n Premie-lidmaatskap is nodig om hierdie funksie te gebruik.</value>
@@ -1128,11 +1136,11 @@
<data name="Expiration" xml:space="preserve">
<value>Vervaldatum</value>
</data>
<data name="DisableWebsiteIcons" xml:space="preserve">
<value>Deaktiveer webwerfikone</value>
<data name="ShowWebsiteIcons" xml:space="preserve">
<value>Toon webwerfikone</value>
</data>
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
<value>Webwerfikone verskaf n herkenbare beeld langs elke aantekenitem in u kluis.</value>
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
<value>Toon n herkenbare prent langs elke aantekening.</value>
</data>
<data name="IconsUrl" xml:space="preserve">
<value>Ikoonbedienerbronadres</value>
@@ -1372,11 +1380,15 @@
<data name="SearchCollection" xml:space="preserve">
<value>Deursoek versameling</value>
</data>
<data name="SearchFolder" xml:space="preserve">
<value>Deursoek vouer</value>
<data name="SearchFileSends" xml:space="preserve">
<value>Deursoek lêer-Sends</value>
</data>
<data name="SearchType" xml:space="preserve">
<value>Deursoek tipe</value>
<data name="SearchTextSends" xml:space="preserve">
<value>Deursoek teks-Sends</value>
</data>
<data name="SearchGroup" xml:space="preserve">
<value>Soek {0}</value>
<comment>ex: Search Logins</comment>
</data>
<data name="Type" xml:space="preserve">
<value>Tipe</value>
@@ -1537,6 +1549,12 @@
<data name="ThemeDefault" xml:space="preserve">
<value>Verstek (Stelsel)</value>
</data>
<data name="DefaultDarkTheme" xml:space="preserve">
<value>Verstekdonkertema</value>
</data>
<data name="DefaultDarkThemeDescription" xml:space="preserve">
<value>Kies welke donkertema om te gebruik as stelseltema wanneer u toestel se donkertema geaktiveer is.</value>
</data>
<data name="CopyNotes" xml:space="preserve">
<value>Kopieer notas</value>
</data>
@@ -1553,17 +1571,21 @@
<value>Swart</value>
<comment>The color black</comment>
</data>
<data name="BlacklistedUris" xml:space="preserve">
<value>Versperde URIs</value>
<data name="Nord" xml:space="preserve">
<value>Nord</value>
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
</data>
<data name="BlacklistedUrisDescription" xml:space="preserve">
<value>URIs op die swartlys word nie outomaties ingevul nie. Die lys moet met kommas geskei wees. Bv. “https://twitter.com, androidapp://com.twitter.android”.</value>
<data name="AutofillBlockedUris" xml:space="preserve">
<value>Versperde URIs vir outovul</value>
</data>
<data name="DisableSavePrompt" xml:space="preserve">
<value>Deaktiveer bewaarpor</value>
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
<value>Outovul sal nie vir versperde URIs gebied word nie. Skei meerdere URIs met n komma. Byvoorbeeld: “https://twitter.com, androidapp://com.twitter.android”.</value>
</data>
<data name="DisableSavePromptDescription" xml:space="preserve">
<value>Die “bewaarpor” vra u outomaties om nuwe items in u kluis te bewaar wanneer u dit vir die eerste maal invoer.</value>
<data name="AskToAddLogin" xml:space="preserve">
<value>Vra om aantekening toe te voeg</value>
</data>
<data name="AskToAddLoginDescription" xml:space="preserve">
<value>Vra om n item toe te voeg indien een nie in u kluis gevind word nie.</value>
</data>
<data name="OnRestart" xml:space="preserve">
<value>Nadat toep herbegin is</value>
@@ -1856,6 +1878,9 @@
<value>n Vriendelike naam om hierdie Send te beskryf.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="Text" xml:space="preserve">
<value>Teks</value>
</data>
<data name="TypeText" xml:space="preserve">
<value>Teks</value>
</data>
@@ -1872,6 +1897,18 @@
<data name="TypeFileInfo" xml:space="preserve">
<value>Die lêer wat u wil verstuur.</value>
</data>
<data name="FileTypeIsSelected" xml:space="preserve">
<value>Lêertipe is gekies.</value>
</data>
<data name="FileTypeIsNotSelected" xml:space="preserve">
<value>Lêertipe is nie gekies nie, tik om te kies.</value>
</data>
<data name="TextTypeIsSelected" xml:space="preserve">
<value>Tekstipe is gekies.</value>
</data>
<data name="TextTypeIsNotSelected" xml:space="preserve">
<value>Tekstipe is nie gekies nie, tik om te kies.</value>
</data>
<data name="DeletionDate" xml:space="preserve">
<value>Skrapdatum</value>
</data>
@@ -2105,28 +2142,28 @@
<value>Een of meer organisasiebeleide verhoed u om u persoonlike kluis uit te stuur.</value>
</data>
<data name="AddAccount" xml:space="preserve">
<value>Add Account</value>
<value>Voeg rekening toe</value>
</data>
<data name="AccountUnlocked" xml:space="preserve">
<value>Unlocked</value>
<value>Ontgrendel</value>
</data>
<data name="AccountLocked" xml:space="preserve">
<value>Locked</value>
<value>Vergrendel</value>
</data>
<data name="AccountLoggedOut" xml:space="preserve">
<value>Logged Out</value>
<value>Uitgeteken</value>
</data>
<data name="AccountSwitchedAutomatically" xml:space="preserve">
<value>Switched to next available account</value>
<value>Oorgeskakel na volgende beskikbare rekening</value>
</data>
<data name="AccountLockedSuccessfully" xml:space="preserve">
<value>Account Locked</value>
<value>Rekening is vergrendel</value>
</data>
<data name="AccountLoggedOutSuccessfully" xml:space="preserve">
<value>Account logged out successfully</value>
<value>Rekening suksesvol uitgeteken</value>
</data>
<data name="AccountRemovedSuccessfully" xml:space="preserve">
<value>Account removed successfully</value>
<value>Rekening suksesvol verwyder</value>
</data>
<data name="DeleteAccount" xml:space="preserve">
<value>Skrap rekening</value>
@@ -2147,7 +2184,7 @@
<value>Ongeldige bevestigingskode.</value>
</data>
<data name="RequestOTP" xml:space="preserve">
<value>Request one-time password</value>
<value>Versoek eenmalige wagwoord</value>
</data>
<data name="SendCode" xml:space="preserve">
<value>Verstuur kode</value>
@@ -2156,24 +2193,124 @@
<value>Verstuur</value>
</data>
<data name="CopySendLinkOnSave" xml:space="preserve">
<value>Copy Send link on save</value>
<value>Kopieer Send-skakel by bewaar</value>
</data>
<data name="SendingCode" xml:space="preserve">
<value>Sending code</value>
<value>Verstuur tans kode</value>
</data>
<data name="Verifying" xml:space="preserve">
<value>Verifying</value>
<value>Bevestiging</value>
</data>
<data name="ResendCode" xml:space="preserve">
<value>Resend Code</value>
<value>Stuur kode weer</value>
</data>
<data name="AVerificationCodeWasSentToYourEmail" xml:space="preserve">
<value>A verification code was sent to your email</value>
<value>n Bevestigingskakel is na u e-pos gestuur</value>
</data>
<data name="AnErrorOccurredWhileSendingAVerificationCodeToYourEmailPleaseTryAgain" xml:space="preserve">
<value>An error occurred while sending a verification code to your email. Please try again</value>
<value>n fout het voorgekom toe n bevestigingskakel na u e-pos gestuur is. Probeer asb. weer</value>
</data>
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
<value>Enter the verification code that was sent to your email</value>
<value>Voer die bevestigingskakel wat na u e-pos gestuur is, in</value>
</data>
<data name="SubmitCrashLogs" xml:space="preserve">
<value>Dien uitbomverslae in</value>
</data>
<data name="SubmitCrashLogsDescription" xml:space="preserve">
<value>Help Bitwarden om toepstabiliteit te verbeter deur uitbomverslae in te dien.</value>
</data>
<data name="OptionsExpanded" xml:space="preserve">
<value>Opsies is uitgevou, tik om in te vou.</value>
</data>
<data name="OptionsCollapsed" xml:space="preserve">
<value>Opsies is ingevou, tik om uit te vou.</value>
</data>
<data name="UppercaseAtoZ" xml:space="preserve">
<value>Hoofletters (A tot Z)</value>
</data>
<data name="LowercaseAtoZ" xml:space="preserve">
<value>Kleinletters (A tot Z)</value>
</data>
<data name="NumbersZeroToNine" xml:space="preserve">
<value>Syfers (0 tot 9)</value>
</data>
<data name="SpecialCharacters" xml:space="preserve">
<value>Spesiale karakters (!@#$%^&amp;*)</value>
</data>
<data name="TapToGoBack" xml:space="preserve">
<value>Tik om terug te gaan</value>
</data>
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
<value>Wagwoord is sigbaar, tik om te verberg.</value>
</data>
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
<value>Wagwoord is nie sigbaar, tik om te toon.</value>
</data>
<data name="FilterByVault" xml:space="preserve">
<value>Filter items per kluis</value>
</data>
<data name="AllVaults" xml:space="preserve">
<value>Alle kluise</value>
</data>
<data name="Vaults" xml:space="preserve">
<value>Kluise</value>
</data>
<data name="VaultFilterDescription" xml:space="preserve">
<value>Kluis: {0}</value>
</data>
<data name="All" xml:space="preserve">
<value>Alle</value>
</data>
<data name="Totp" xml:space="preserve">
<value>TOTP</value>
</data>
<data name="VerificationCodes" xml:space="preserve">
<value>Verification Codes</value>
</data>
<data name="PremiumSubscriptionRequired" xml:space="preserve">
<value>Premium subscription required</value>
</data>
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
<value>Cannot add authenticator key? </value>
</data>
<data name="ScanQRCode" xml:space="preserve">
<value>Scan QR Code</value>
</data>
<data name="CannotScanQRCode" xml:space="preserve">
<value>Cannot scan QR Code? </value>
</data>
<data name="AuthenticatorKeyScanner" xml:space="preserve">
<value>Authenticator Key</value>
</data>
<data name="EnterKeyManually" xml:space="preserve">
<value>Enter Key Manually</value>
</data>
<data name="AddTotp" xml:space="preserve">
<value>Add TOTP</value>
</data>
<data name="SetupTotp" xml:space="preserve">
<value>Set up TOTP</value>
</data>
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
<value>Once the key is successfully entered,
select Add TOTP to store the key safely</value>
</data>
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
<value></value>
</data>
<data name="NeverLockWarning" xml:space="preserve">
<value>Deur u vergrendelopsies na “Nooit” te stel, is u kluis beskikbaar aan enigeen met toegang tot u toestel. Indien u hierdie opsie gebruik moet u seker maak dat u u toestel voldoende beskerm.</value>
</data>
<data name="EnvironmentPageUrlsError" xml:space="preserve">
<value>Een of meer ingevoerde bronadresse is ongeldig. Hersien dit asb. en probeer weer bewaar.</value>
</data>
<data name="GenericErrorMessage" xml:space="preserve">
<value>Ons kon nie u versoek verwerk nie. Probeer asb. weer of kontak ons.</value>
</data>
<data name="AllowScreenCapture" xml:space="preserve">
<value>Laat skermopname toe</value>
</data>
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
<value>Is u seker u wil skermopname aktiveer?</value>
</data>
</root>

File diff suppressed because it is too large Load Diff

View File

@@ -299,6 +299,10 @@
<value>Anbarım</value>
<comment>The title for the vault page.</comment>
</data>
<data name="Authenticator" xml:space="preserve">
<value>Kimlik təsdiqləyici</value>
<comment>Authenticator TOTP feature</comment>
</data>
<data name="Name" xml:space="preserve">
<value>Ad</value>
<comment>Label for an entity name.</comment>
@@ -770,6 +774,12 @@
<data name="Enabled" xml:space="preserve">
<value>Fəallaşdırıldı</value>
</data>
<data name="Off" xml:space="preserve">
<value>Bağlı</value>
</data>
<data name="On" xml:space="preserve">
<value>Açıq</value>
</data>
<data name="Status" xml:space="preserve">
<value>Vəziyyət</value>
</data>
@@ -889,11 +899,9 @@
<data name="AuthenticatorKeyReadError" xml:space="preserve">
<value>Kimlik təsdiqləyici açarı oxuna bilmir.</value>
</data>
<data name="CameraInstructionBottom" xml:space="preserve">
<value>Skan avtomatik olaraq icra ediləcək.</value>
</data>
<data name="CameraInstructionTop" xml:space="preserve">
<value>Kameranı QR koduna yönləndirin.</value>
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
<value>Kameranızı QR koduna yönəldin.
Skan prosesi avtomatik baş tutacaq.</value>
</data>
<data name="ScanQrTitle" xml:space="preserve">
<value>QR kodu skan edin</value>
@@ -907,11 +915,11 @@
<data name="CopyTotp" xml:space="preserve">
<value>TOTP-ni kopyala</value>
</data>
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
<value>Hesabınıza əlavə edilən kimlik təsdiqləyici açarı varsa, giriş məlumatları avto-doldurulanda TOTP təsdiqləmə kodu da avtomatik olaraq lövhəyə kopyalanacaq.</value>
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
<value>Bir girişin, kimlik təsdiqləyici açarı varsa, giriş məlumatları avto-doldurulanda TOTP təsdiqləmə kodunu kopyalayın.</value>
</data>
<data name="DisableAutoTotpCopy" xml:space="preserve">
<value>Avtomatik TOTP kopyalamasını sıradan çıxart</value>
<data name="CopyTotpAutomatically" xml:space="preserve">
<value>TOTP-ni avtomatik kopyala</value>
</data>
<data name="PremiumRequired" xml:space="preserve">
<value>Bu özəlliyi istifadə etmək üçün premium üzvlük lazımdır.</value>
@@ -1128,11 +1136,11 @@
<data name="Expiration" xml:space="preserve">
<value>Bitmə vaxtı</value>
</data>
<data name="DisableWebsiteIcons" xml:space="preserve">
<value>Veb sayt nişanlarını sıradan çıxart</value>
<data name="ShowWebsiteIcons" xml:space="preserve">
<value>Veb sayt nişanlarını göstər</value>
</data>
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
<value>Veb sayt nişanları, anbarınızda hər bir giriş elementinin yanında tanımağınıza kömək edən bir təsvir təqdim edir.</value>
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
<value>Hər girişin yanında tanına bilən təsvir göstər.</value>
</data>
<data name="IconsUrl" xml:space="preserve">
<value>Nişan server URL-si</value>
@@ -1372,11 +1380,15 @@
<data name="SearchCollection" xml:space="preserve">
<value>Kolleksiya axtar</value>
</data>
<data name="SearchFolder" xml:space="preserve">
<value>Qovluq axtar</value>
<data name="SearchFileSends" xml:space="preserve">
<value>Fayl "Send"lərini axtar</value>
</data>
<data name="SearchType" xml:space="preserve">
<value>Axtarış növü</value>
<data name="SearchTextSends" xml:space="preserve">
<value>Mətn "Send"lərini axtar</value>
</data>
<data name="SearchGroup" xml:space="preserve">
<value>Axtar: {0}</value>
<comment>ex: Search Logins</comment>
</data>
<data name="Type" xml:space="preserve">
<value>Növ</value>
@@ -1537,6 +1549,12 @@
<data name="ThemeDefault" xml:space="preserve">
<value>İlkin (Sistem)</value>
</data>
<data name="DefaultDarkTheme" xml:space="preserve">
<value>İlkin tünd tema</value>
</data>
<data name="DefaultDarkThemeDescription" xml:space="preserve">
<value>Cihazınızda tünd rejim fəal olanda İlkin (Sistem) temanı istifadə edərkən istifadə ediləcək tünd temanı seçin</value>
</data>
<data name="CopyNotes" xml:space="preserve">
<value>Qeydləri kopyala</value>
</data>
@@ -1553,17 +1571,21 @@
<value>Qara</value>
<comment>The color black</comment>
</data>
<data name="BlacklistedUris" xml:space="preserve">
<value>Qara siyahıdakı URI-lər</value>
<data name="Nord" xml:space="preserve">
<value>Nord</value>
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
</data>
<data name="BlacklistedUrisDescription" xml:space="preserve">
<value>Qara siyahıdakı URI-lərə avto-doldurma təklif edilməyəcək. Siyahı vergüllə ayrılmalıdır. Məs. "https://twitter.com, androidapp://com.twitter.android".</value>
<data name="AutofillBlockedUris" xml:space="preserve">
<value>Əngəllənən URI-lərin avto-doldurulması</value>
</data>
<data name="DisableSavePrompt" xml:space="preserve">
<value>Saxlama istəyini sıradan çıxart</value>
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
<value>Əngəllənən URI-lər üçün avto-doldurma təklif edilmir. Çoxlu URI-ni vergüllə ayırır. Nümunə: "https://twitter.com, androidapp://com.twitter.android".</value>
</data>
<data name="DisableSavePromptDescription" xml:space="preserve">
<value>"Saxlama istəyi", ilk dəfə istifadə etdiyiniz məlumatları anbarda saxlamaq istəyib-istəmədiyinizi avtomatik olaraq soruşur.</value>
<data name="AskToAddLogin" xml:space="preserve">
<value>Giriş əlavə etmək üçün soruş</value>
</data>
<data name="AskToAddLoginDescription" xml:space="preserve">
<value>Anbarınızda yoxdursa, bir element əlavə etməyi soruşun.</value>
</data>
<data name="OnRestart" xml:space="preserve">
<value>Tətbiq yenidən başladılanda</value>
@@ -1856,6 +1878,9 @@
<value>Bu "Send"i açıqlayan bir ad.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="Text" xml:space="preserve">
<value>Mətn</value>
</data>
<data name="TypeText" xml:space="preserve">
<value>Mətn</value>
</data>
@@ -1872,6 +1897,18 @@
<data name="TypeFileInfo" xml:space="preserve">
<value>Göndərmək istədiyiniz fayl.</value>
</data>
<data name="FileTypeIsSelected" xml:space="preserve">
<value>Fayl növü seçildi.</value>
</data>
<data name="FileTypeIsNotSelected" xml:space="preserve">
<value>Fayl növü seçilmədi, seçmək üçün toxunun.</value>
</data>
<data name="TextTypeIsSelected" xml:space="preserve">
<value>Mətn növü seçildi.</value>
</data>
<data name="TextTypeIsNotSelected" xml:space="preserve">
<value>Mətn növü seçilmədi, seçmək üçün toxunun.</value>
</data>
<data name="DeletionDate" xml:space="preserve">
<value>Silinmə tarixi</value>
</data>
@@ -2176,4 +2213,103 @@
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
<value>E-poçtunuza göndərilmiş təsdiqləmə kodunu daxil edin</value>
</data>
<data name="SubmitCrashLogs" xml:space="preserve">
<value>Çökmə jurnallarını göndər</value>
</data>
<data name="SubmitCrashLogsDescription" xml:space="preserve">
<value>Çökmə hesabatlarını göndərərək Bitwarden-in tətbiq stabilliyini yaxşılaşdırmasına kömək edin.</value>
</data>
<data name="OptionsExpanded" xml:space="preserve">
<value>Seçimlər genişləndirildi, yığcamlaşdırmaq üçün toxunun.</value>
</data>
<data name="OptionsCollapsed" xml:space="preserve">
<value>Seçimlər yığcamlaşdırıldı, genişləndirmək üçün toxunun.</value>
</data>
<data name="UppercaseAtoZ" xml:space="preserve">
<value>Böyük hərf (A-Z)</value>
</data>
<data name="LowercaseAtoZ" xml:space="preserve">
<value>Kiçik hərf (Z-A)</value>
</data>
<data name="NumbersZeroToNine" xml:space="preserve">
<value>Rəqəmlər (0-9)</value>
</data>
<data name="SpecialCharacters" xml:space="preserve">
<value>Xüsusi simvollar (!@#$%^&amp;*)</value>
</data>
<data name="TapToGoBack" xml:space="preserve">
<value>Geriyə getmək üçün toxun</value>
</data>
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
<value>Parol görünür, gizlətmək üçün toxunun.</value>
</data>
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
<value>Parol görünmür, göstərmək üçün toxunun.</value>
</data>
<data name="FilterByVault" xml:space="preserve">
<value>Elementləri anbara görə filtrlə</value>
</data>
<data name="AllVaults" xml:space="preserve">
<value>Bütün anbarlar</value>
</data>
<data name="Vaults" xml:space="preserve">
<value>Anbarlar</value>
</data>
<data name="VaultFilterDescription" xml:space="preserve">
<value>Anbar: {0}</value>
</data>
<data name="All" xml:space="preserve">
<value>Hamısı</value>
</data>
<data name="Totp" xml:space="preserve">
<value>TOTP</value>
</data>
<data name="VerificationCodes" xml:space="preserve">
<value>Təsdiqləmə kodları</value>
</data>
<data name="PremiumSubscriptionRequired" xml:space="preserve">
<value>Premium abunəlik tələb olunur</value>
</data>
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
<value>Kimlik təsdiqləyici açarı oxuna bilmir? </value>
</data>
<data name="ScanQRCode" xml:space="preserve">
<value>QR kodu skan edin</value>
</data>
<data name="CannotScanQRCode" xml:space="preserve">
<value>QR kodunu skan edə bilmədiniz? </value>
</data>
<data name="AuthenticatorKeyScanner" xml:space="preserve">
<value>Kimlik təsdiqləyici açarı</value>
</data>
<data name="EnterKeyManually" xml:space="preserve">
<value>Kodu əllə daxil et</value>
</data>
<data name="AddTotp" xml:space="preserve">
<value>TOTP əlavə et</value>
</data>
<data name="SetupTotp" xml:space="preserve">
<value>TOTP quraşdır</value>
</data>
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
<value>Açar uğurla daxil edildikdən sonra, açarı güvənli şəkildə saxlamaq üçün "TOTP əlavə et"i seçin</value>
</data>
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
<value></value>
</data>
<data name="NeverLockWarning" xml:space="preserve">
<value>Kilid seçimlərini "Heç vaxt" olaraq tənzimləmək, anbarınızı cihazınıza müraciəti olan hər kəsə əlçatan edir. Bu seçimi istifadə etsəniz, cihazınızı düzgün qoruduğunuza əmin olmalısınız.</value>
</data>
<data name="EnvironmentPageUrlsError" xml:space="preserve">
<value>Daxil edilən bir və ya daha çox URL yararsızdır. Zəhmət olmasa nəzər salın və yenidən saxlamağa çalışın.</value>
</data>
<data name="GenericErrorMessage" xml:space="preserve">
<value>Tələbinizi emal edə bilmədik. Zəhmət olmasa yenidən sınayın və ya bizimlə əlaqə saxlayın.</value>
</data>
<data name="AllowScreenCapture" xml:space="preserve">
<value>Ekranı çəkməyə icazə ver</value>
</data>
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
<value>Ekranın çəkilməsini fəallaşdırmaq istədiyinizə əminsiniz?</value>
</data>
</root>

View File

@@ -276,16 +276,16 @@
<value>Вы ўпэўнены, што хочаце выйсці?</value>
</data>
<data name="RemoveAccount" xml:space="preserve">
<value>Remove Account</value>
<value>Выдаліць уліковы запіс</value>
</data>
<data name="RemoveAccountConfirmation" xml:space="preserve">
<value>Are you sure you want to remove this account?</value>
<value>Вы ўпэўнены, што хочаце выдаліць уліковы запіс?</value>
</data>
<data name="AccountAlreadyAdded" xml:space="preserve">
<value>Account Already Added</value>
<value>Уліковы запіс ужо дададзены</value>
</data>
<data name="SwitchToAlreadyAddedAccountConfirmation" xml:space="preserve">
<value>Would you like to switch to it now?</value>
<value>Хочаце пераключыцца на яго зараз?</value>
</data>
<data name="MasterPassword" xml:space="preserve">
<value>Асноўны пароль</value>
@@ -299,6 +299,10 @@
<value>Маё сховішча</value>
<comment>The title for the vault page.</comment>
</data>
<data name="Authenticator" xml:space="preserve">
<value>Authenticator</value>
<comment>Authenticator TOTP feature</comment>
</data>
<data name="Name" xml:space="preserve">
<value>Назва</value>
<comment>Label for an entity name.</comment>
@@ -770,6 +774,12 @@
<data name="Enabled" xml:space="preserve">
<value>Уключана</value>
</data>
<data name="Off" xml:space="preserve">
<value>Off</value>
</data>
<data name="On" xml:space="preserve">
<value>On</value>
</data>
<data name="Status" xml:space="preserve">
<value>Стан</value>
</data>
@@ -798,7 +808,7 @@
<value>Вы шукаеце элемент аўтазапаўнення для "{0}".</value>
</data>
<data name="LearnOrg" xml:space="preserve">
<value>Learn About Organizations</value>
<value>Learn about organizations</value>
</data>
<data name="CannotOpenApp" xml:space="preserve">
<value>Немагчыма адкрыць праграму "{0}".</value>
@@ -889,11 +899,9 @@
<data name="AuthenticatorKeyReadError" xml:space="preserve">
<value>Немагчыма прачытаць ключ праверкі аутэнтычнасці.</value>
</data>
<data name="CameraInstructionBottom" xml:space="preserve">
<value>Сканіраванне будзе адбывацца аўтаматычна.</value>
</data>
<data name="CameraInstructionTop" xml:space="preserve">
<value>Навядзіце камеру на QR-код.</value>
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
<value>Point your camera at the QR Code.
Scanning will happen automatically.</value>
</data>
<data name="ScanQrTitle" xml:space="preserve">
<value>Сканаваць QR -код</value>
@@ -907,11 +915,11 @@
<data name="CopyTotp" xml:space="preserve">
<value>Капіяваць TOTP</value>
</data>
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
<value>Калі да вашых уліковых даных прымацаваны ключ праверкі сапраўднасці, то код пацвярджэння TOTP будзе скапіяваны пры аўтазапаўненні ўліковых даных.</value>
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
<value>If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login.</value>
</data>
<data name="DisableAutoTotpCopy" xml:space="preserve">
<value>Адключыць аўтаматычнае капіяванне TOTP</value>
<data name="CopyTotpAutomatically" xml:space="preserve">
<value>Copy TOTP automatically</value>
</data>
<data name="PremiumRequired" xml:space="preserve">
<value>Для выкарыстання гэтай функцыі патрабуецца прэміяльны статус.</value>
@@ -1128,11 +1136,11 @@
<data name="Expiration" xml:space="preserve">
<value>Тэрмін дзеяння</value>
</data>
<data name="DisableWebsiteIcons" xml:space="preserve">
<value>Адключыць значкі вэб-сайтаў</value>
<data name="ShowWebsiteIcons" xml:space="preserve">
<value>Show website icons</value>
</data>
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
<value>Значкі вэб-сайтаў паказваюцца з кожным элементам у вашым сховішчы.</value>
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
<value>Show a recognizable image next to each login.</value>
</data>
<data name="IconsUrl" xml:space="preserve">
<value>URL-адрас сервера значкоў</value>
@@ -1372,11 +1380,15 @@
<data name="SearchCollection" xml:space="preserve">
<value>Пошук у калекцыі</value>
</data>
<data name="SearchFolder" xml:space="preserve">
<value>Пошук у папцы</value>
<data name="SearchFileSends" xml:space="preserve">
<value>Search File Sends</value>
</data>
<data name="SearchType" xml:space="preserve">
<value>Пошук па тыпу</value>
<data name="SearchTextSends" xml:space="preserve">
<value>Search Text Sends</value>
</data>
<data name="SearchGroup" xml:space="preserve">
<value>Search {0}</value>
<comment>ex: Search Logins</comment>
</data>
<data name="Type" xml:space="preserve">
<value>Тып</value>
@@ -1537,6 +1549,12 @@
<data name="ThemeDefault" xml:space="preserve">
<value>Default (System)</value>
</data>
<data name="DefaultDarkTheme" xml:space="preserve">
<value>Default dark theme</value>
</data>
<data name="DefaultDarkThemeDescription" xml:space="preserve">
<value>Choose the dark theme to use when using Default (System) theme while your device's dark mode is enabled.</value>
</data>
<data name="CopyNotes" xml:space="preserve">
<value>Капіяваць нататк</value>
</data>
@@ -1553,17 +1571,21 @@
<value>Чорная</value>
<comment>The color black</comment>
</data>
<data name="BlacklistedUris" xml:space="preserve">
<value>URI у чорным спісе</value>
<data name="Nord" xml:space="preserve">
<value>Nord</value>
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
</data>
<data name="BlacklistedUrisDescription" xml:space="preserve">
<value>Аўтазапаўненне не будзе прапаноўвацца для URI з чорнага спіса. Элементы гэтага спіса трэба падзяляць коскай. Напрыклад: "https://twitter.com, androidapp://com.twitter.android".</value>
<data name="AutofillBlockedUris" xml:space="preserve">
<value>Auto-fill blocked URIs</value>
</data>
<data name="DisableSavePrompt" xml:space="preserve">
<value>Выключыць запыт на захаванне.</value>
<data name="AutofillBlockedUrisDescription" xml:space="preserve">
<value>Auto-fill will not be offered for blocked URIs. Separate multiple URIs with a comma. For example: "https://twitter.com, androidapp://com.twitter.android".</value>
</data>
<data name="DisableSavePromptDescription" xml:space="preserve">
<value>Апавяшчэнне аб захаванні аўтаматычна прапануе вам захаваць новыя элементы ў сховішчы пасля іх дадання.</value>
<data name="AskToAddLogin" xml:space="preserve">
<value>Ask to add login</value>
</data>
<data name="AskToAddLoginDescription" xml:space="preserve">
<value>Ask to add an item if one isn't found in your vault.</value>
</data>
<data name="OnRestart" xml:space="preserve">
<value>Пры перазапуску</value>
@@ -1856,6 +1878,9 @@
<value>Зразумелая назва для апісання адпраўлення</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="Text" xml:space="preserve">
<value>Text</value>
</data>
<data name="TypeText" xml:space="preserve">
<value>Тэкст</value>
</data>
@@ -1872,6 +1897,18 @@
<data name="TypeFileInfo" xml:space="preserve">
<value>Файл, які вы хочаце адправіць.</value>
</data>
<data name="FileTypeIsSelected" xml:space="preserve">
<value>File type is selected.</value>
</data>
<data name="FileTypeIsNotSelected" xml:space="preserve">
<value>File type is not selected, tap to select.</value>
</data>
<data name="TextTypeIsSelected" xml:space="preserve">
<value>Text type is selected.</value>
</data>
<data name="TextTypeIsNotSelected" xml:space="preserve">
<value>Text type is not selected, tap to select.</value>
</data>
<data name="DeletionDate" xml:space="preserve">
<value>Дата выдалення</value>
</data>
@@ -2129,13 +2166,13 @@
<value>Account removed successfully</value>
</data>
<data name="DeleteAccount" xml:space="preserve">
<value>Delete Account</value>
<value>Delete account</value>
</data>
<data name="DeletingYourAccountIsPermanent" xml:space="preserve">
<value>Deleting your account is permanent</value>
</data>
<data name="DeleteAccountExplanation" xml:space="preserve">
<value>Your account and all associated data will be erased and unrecoverable. Are you sure you want to continue?</value>
<value>Your account and all vault data will be erased and unrecoverable. Are you sure you want to continue?</value>
</data>
<data name="DeletingYourAccount" xml:space="preserve">
<value>Deleting your account</value>
@@ -2176,4 +2213,104 @@
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
<value>Enter the verification code that was sent to your email</value>
</data>
<data name="SubmitCrashLogs" xml:space="preserve">
<value>Submit crash logs</value>
</data>
<data name="SubmitCrashLogsDescription" xml:space="preserve">
<value>Help Bitwarden improve app stability by submitting crash reports.</value>
</data>
<data name="OptionsExpanded" xml:space="preserve">
<value>Options are expanded, tap to collapse.</value>
</data>
<data name="OptionsCollapsed" xml:space="preserve">
<value>Options are collapsed, tap to expand.</value>
</data>
<data name="UppercaseAtoZ" xml:space="preserve">
<value>Uppercase (A to Z)</value>
</data>
<data name="LowercaseAtoZ" xml:space="preserve">
<value>Lowercase (A to Z)</value>
</data>
<data name="NumbersZeroToNine" xml:space="preserve">
<value>Numbers (0 to 9)</value>
</data>
<data name="SpecialCharacters" xml:space="preserve">
<value>Special Characters (!@#$%^&amp;*)</value>
</data>
<data name="TapToGoBack" xml:space="preserve">
<value>Tap to go back</value>
</data>
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
<value>Password is visible, tap to hide.</value>
</data>
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
<value>Password is not visible, tap to show.</value>
</data>
<data name="FilterByVault" xml:space="preserve">
<value>Filter items by vault</value>
</data>
<data name="AllVaults" xml:space="preserve">
<value>All Vaults</value>
</data>
<data name="Vaults" xml:space="preserve">
<value>Vaults</value>
</data>
<data name="VaultFilterDescription" xml:space="preserve">
<value>Vault: {0}</value>
</data>
<data name="All" xml:space="preserve">
<value>All</value>
</data>
<data name="Totp" xml:space="preserve">
<value>TOTP</value>
</data>
<data name="VerificationCodes" xml:space="preserve">
<value>Verification Codes</value>
</data>
<data name="PremiumSubscriptionRequired" xml:space="preserve">
<value>Premium subscription required</value>
</data>
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
<value>Cannot add authenticator key? </value>
</data>
<data name="ScanQRCode" xml:space="preserve">
<value>Scan QR Code</value>
</data>
<data name="CannotScanQRCode" xml:space="preserve">
<value>Cannot scan QR Code? </value>
</data>
<data name="AuthenticatorKeyScanner" xml:space="preserve">
<value>Authenticator Key</value>
</data>
<data name="EnterKeyManually" xml:space="preserve">
<value>Enter Key Manually</value>
</data>
<data name="AddTotp" xml:space="preserve">
<value>Add TOTP</value>
</data>
<data name="SetupTotp" xml:space="preserve">
<value>Set up TOTP</value>
</data>
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
<value>Once the key is successfully entered,
select Add TOTP to store the key safely</value>
</data>
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
<value></value>
</data>
<data name="NeverLockWarning" xml:space="preserve">
<value>Setting your lock options to “Never” keeps your vault available to anyone with access to your device. If you use this option, you should ensure that you keep your device properly protected.</value>
</data>
<data name="EnvironmentPageUrlsError" xml:space="preserve">
<value>One or more of the URLs entered are invalid. Please revise it and try to save again.</value>
</data>
<data name="GenericErrorMessage" xml:space="preserve">
<value>We were unable to process your request. Please try again or contact us.</value>
</data>
<data name="AllowScreenCapture" xml:space="preserve">
<value>Allow screen capture</value>
</data>
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
<value>Are you sure you want to enable Screen Capture?</value>
</data>
</root>

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