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

Compare commits

..

1 Commits

Author SHA1 Message Date
André Bispo
9f2f96de9d [PM-2470] Load environment from file in env page. 2023-06-12 22:39:11 +01:00
466 changed files with 8348 additions and 30826 deletions

31
.github/CODEOWNERS vendored
View File

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

View File

@@ -71,11 +71,6 @@ jobs:
with: with:
nuget-version: 5.9.0 nuget-version: 5.9.0
- name: Set up .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '3.1.x'
- name: Set up MSBuild - name: Set up MSBuild
uses: microsoft/setup-msbuild@1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c # v1.3.1 uses: microsoft/setup-msbuild@1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c # v1.3.1
@@ -525,7 +520,7 @@ jobs:
submodules: 'true' submodules: 'true'
- name: Login to Azure - CI Subscription - name: Login to Azure - CI Subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6 uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@@ -667,22 +662,6 @@ jobs:
$configuration = "AppStore"; $configuration = "AppStore";
$platform = "iPhone"; $platform = "iPhone";
Write-Output "########################################"
Write-Output "##### Archive $configuration Configuration for $platform Platform"
Write-Output "########################################"
msbuild "$($env:GITHUB_WORKSPACE + "/src/iOS/iOS.csproj")" "/p:Platform=$platform" `
"/p:Configuration=$configuration" "/p:ArchiveOnBuild=true" "/t:`"Build`""
Write-Output "########################################"
Write-Output "##### Done"
Write-Output "########################################"
shell: pwsh
- name: Archive Build for Mobile Automation
run: |
$configuration = "Release";
$platform = "iPhoneSimulator";
Write-Output "########################################" Write-Output "########################################"
Write-Output "##### Archive $configuration Configuration for $platform Platform" Write-Output "##### Archive $configuration Configuration for $platform Platform"
Write-Output "########################################" Write-Output "########################################"
@@ -705,15 +684,6 @@ jobs:
-exportOptionsPlist $EXPORT_OPTIONS_PATH -exportOptionsPlist $EXPORT_OPTIONS_PATH
shell: bash shell: bash
- name: Export .app for Automation CI
run: |
ARCHIVE_PATH="./src/iOS/bin/iPhoneSimulator/Release/BitwardeniOS.app"
EXPORT_PATH="./bitwarden-export"
zip -r -q BitwardeniOS.app.zip $ARCHIVE_PATH
mv BitwardeniOS.app.zip $EXPORT_PATH
shell: bash
- name: Copy all dSYMs files to upload - name: Copy all dSYMs files to upload
run: | run: |
ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs" ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs"
@@ -736,13 +706,6 @@ jobs:
./bitwarden-export/dSYMs/*.* ./bitwarden-export/dSYMs/*.*
if-no-files-found: error if-no-files-found: error
- name: Upload .app file for Automation CI
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
with:
name: BitwardeniOS.app.zip
path: ./bitwarden-export/BitwardeniOS.app.zip
if-no-files-found: error
- name: Install AppCenter CLI - name: Install AppCenter CLI
if: | if: |
(github.ref == 'refs/heads/master' (github.ref == 'refs/heads/master'
@@ -811,7 +774,7 @@ jobs:
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: Login to Azure - CI Subscription - name: Login to Azure - CI Subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6 uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@@ -830,7 +793,7 @@ jobs:
done done
- name: Upload Sources - name: Upload Sources
uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0 uses: crowdin/github-action@ecd7eb0ef6f3cfa16293c79e9cbc4bc5b5fd9c49 # v1.4.9
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
@@ -877,7 +840,7 @@ jobs:
fi fi
- name: Login to Azure - CI Subscription - name: Login to Azure - CI Subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6 uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
if: failure() if: failure()
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

View File

@@ -18,19 +18,19 @@ jobs:
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: Login to Azure - CI Subscription - name: Login to Azure - CI Subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6 uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets - name: Retrieve secrets
id: retrieve-secrets id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67 uses: bitwarden/gh-actions/get-keyvault-secrets@34ecb67b2a357795dc893549df0795e7383ff50f
with: with:
keyvault: "bitwarden-ci" keyvault: "bitwarden-ci"
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase" secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Download translations - name: Download translations
uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0 uses: crowdin/github-action@ecd7eb0ef6f3cfa16293c79e9cbc4bc5b5fd9c49 # v1.4.9
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ jobs:
version_number: ${{ steps.version.outputs.new-version }} version_number: ${{ steps.version.outputs.new-version }}
steps: steps:
- name: Checkout Branch - name: Checkout Branch
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: Calculate bumped version - name: Calculate bumped version
id: version id: version
@@ -32,8 +32,14 @@ jobs:
echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT
trigger_version_bump: trigger_version_bump:
name: Bump version to ${{ needs.setup.outputs.version_number }} name: "Version bump"
needs: setup runs-on: ubuntu-22.04
needs:
- setup
steps:
- name: Bump version to ${{ needs.setup.outputs.version_number }}
uses: ./.github/workflows/version-bump.yml uses: ./.github/workflows/version-bump.yml
secrets:
AZURE_PROD_KV_CREDENTIALS: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
with: with:
version_number: ${{ needs.setup.outputs.version_number }} version_number: ${{ needs.setup.outputs.version_number }}

View File

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

View File

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

View File

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

View File

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

View File

@@ -77,21 +77,21 @@
<PackageReference Include="Portable.BouncyCastle"> <PackageReference Include="Portable.BouncyCastle">
<Version>1.9.0</Version> <Version>1.9.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.6.1.3" /> <PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1.1" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" /> <PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.16" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.21" /> <PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.19" />
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.10.1.2" /> <PackageReference Include="Xamarin.AndroidX.Core" Version="1.10.0" />
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.4.0.2" /> <PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1.1" />
<PackageReference Include="Xamarin.Essentials"> <PackageReference Include="Xamarin.Essentials">
<Version>1.8.0</Version> <Version>1.7.5</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Xamarin.Firebase.Messaging"> <PackageReference Include="Xamarin.Firebase.Messaging">
<Version>123.1.2.2</Version> <Version>123.1.1.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.9.0.2" /> <PackageReference Include="Xamarin.Google.Android.Material" Version="1.8.0" />
<PackageReference Include="Xamarin.Google.Dagger" Version="2.46.1.2" /> <PackageReference Include="Xamarin.Google.Dagger" Version="2.44.2.1" />
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet"> <PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
<Version>118.0.1.5</Version> <Version>118.0.1.3</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -159,7 +159,6 @@
<Compile Include="Constants.cs" /> <Compile Include="Constants.cs" />
<Compile Include="Effects\RemoveFontPaddingEffect.cs" /> <Compile Include="Effects\RemoveFontPaddingEffect.cs" />
<Compile Include="Services\WatchDeviceService.cs" /> <Compile Include="Services\WatchDeviceService.cs" />
<Compile Include="Renderers\CustomLabelRenderer.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidAsset Include="Assets\bwi-font.ttf" /> <AndroidAsset Include="Assets\bwi-font.ttf" />
@@ -233,26 +232,6 @@
<SubType></SubType> <SubType></SubType>
<Generator></Generator> <Generator></Generator>
</AndroidResource> </AndroidResource>
<AndroidResource Include="Resources\layout\validatable_input_dialog_layout.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\empty_uris_placeholder.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\empty_uris_placeholder_dark.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\empty_login_requests.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\empty_login_requests_dark.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidResource Include="Resources\drawable\splash_screen.xml" /> <AndroidResource Include="Resources\drawable\splash_screen.xml" />

Binary file not shown.

View File

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

View File

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

View File

@@ -54,15 +54,16 @@ namespace Bit.Droid.Autofill
if (HintToFieldsMap.ContainsKey(View.AutofillHintPassword)) if (HintToFieldsMap.ContainsKey(View.AutofillHintPassword))
{ {
_passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]); _passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]);
return _passwordFields;
} }
} }
else
{
_passwordFields = Fields.Where(f => FieldIsPassword(f)).ToList(); _passwordFields = Fields.Where(f => FieldIsPassword(f)).ToList();
if (!_passwordFields.Any()) if (!_passwordFields.Any())
{ {
_passwordFields = Fields.Where(f => FieldHasPasswordTerms(f)).ToList(); _passwordFields = Fields.Where(f => FieldHasPasswordTerms(f)).ToList();
} }
}
return _passwordFields; return _passwordFields;
} }
} }
@@ -86,12 +87,9 @@ namespace Bit.Droid.Autofill
{ {
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]); _usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]);
} }
if (_usernameFields.Any()) }
else
{ {
return _usernameFields;
}
}
foreach (var passwordField in PasswordFields) foreach (var passwordField in PasswordFields)
{ {
var usernameField = Fields.TakeWhile(f => f.AutofillId != passwordField.AutofillId) var usernameField = Fields.TakeWhile(f => f.AutofillId != passwordField.AutofillId)
@@ -106,6 +104,7 @@ namespace Bit.Droid.Autofill
{ {
_usernameFields = Fields.Where(f => FieldIsUsername(f)).ToList(); _usernameFields = Fields.Where(f => FieldIsUsername(f)).ToList();
} }
}
return _usernameFields; return _usernameFields;
} }
} }

View File

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

View File

@@ -44,6 +44,7 @@ namespace Bit.Droid
private IAppIdService _appIdService; private IAppIdService _appIdService;
private IEventService _eventService; private IEventService _eventService;
private IPushNotificationListenerService _pushNotificationListenerService; private IPushNotificationListenerService _pushNotificationListenerService;
private IVaultTimeoutService _vaultTimeoutService;
private ILogger _logger; private ILogger _logger;
private PendingIntent _eventUploadPendingIntent; private PendingIntent _eventUploadPendingIntent;
private AppOptions _appOptions; private AppOptions _appOptions;
@@ -68,6 +69,7 @@ namespace Bit.Droid
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService"); _appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService"); _eventService = ServiceContainer.Resolve<IEventService>("eventService");
_pushNotificationListenerService = ServiceContainer.Resolve<IPushNotificationListenerService>(); _pushNotificationListenerService = ServiceContainer.Resolve<IPushNotificationListenerService>();
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
_logger = ServiceContainer.Resolve<ILogger>("logger"); _logger = ServiceContainer.Resolve<ILogger>("logger");
TabLayoutResource = Resource.Layout.Tabbar; TabLayoutResource = Resource.Layout.Tabbar;
@@ -116,7 +118,7 @@ namespace Bit.Droid
{ {
ListenYubiKey((bool)message.Data); ListenYubiKey((bool)message.Data);
} }
else if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY) else if (message.Command == "updatedTheme")
{ {
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => AppearanceAdjustments()); Xamarin.Forms.Device.BeginInvokeOnMainThread(() => AppearanceAdjustments());
} }
@@ -232,29 +234,26 @@ namespace Bit.Droid
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{ {
_vaultTimeoutService.ResetTimeoutDelay = true;
if (resultCode == Result.Ok && if (resultCode == Result.Ok &&
(requestCode == Core.Constants.SelectFileRequestCode || requestCode == Core.Constants.SaveFileRequestCode)) (requestCode == Core.Constants.SelectFileRequestCode || requestCode == Core.Constants.SaveFileRequestCode))
{ {
Android.Net.Uri uri = null; Android.Net.Uri uri = null;
string fileName = null; string fileName = null;
if (data != null && data.Data != null) if (data != null && data.Data != null)
{
if (data.Data.ToString()?.Contains(Constants.PACKAGE_NAME) != true)
{ {
uri = data.Data; uri = data.Data;
fileName = AndroidHelpers.GetFileName(ApplicationContext, uri); fileName = AndroidHelpers.GetFileName(ApplicationContext, uri);
} }
}
else else
{ {
// camera // camera
var tmpDir = new Java.IO.File(FilesDir, Constants.TEMP_CAMERA_IMAGE_DIR); var file = new Java.IO.File(FilesDir, "temp_camera_photo.jpg");
var file = new Java.IO.File(tmpDir, Constants.TEMP_CAMERA_IMAGE_NAME);
uri = FileProvider.GetUriForFile(this, "com.x8bit.bitwarden.fileprovider", file); uri = FileProvider.GetUriForFile(this, "com.x8bit.bitwarden.fileprovider", file);
fileName = $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg"; fileName = $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg";
} }
if (uri == null || fileName == null) if (uri == null)
{ {
return; return;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,13 +13,12 @@ using Android.Views.InputMethods;
using Android.Widget; using Android.Widget;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.App.Utilities.Prompts;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Droid.Utilities; using Bit.Droid.Utilities;
using Plugin.CurrentActivity; using Plugin.CurrentActivity;
using Xamarin.Forms.Platform.Android; using static Bit.App.Pages.SettingsPageViewModel;
namespace Bit.Droid.Services namespace Bit.Droid.Services
{ {
@@ -210,7 +209,10 @@ namespace Bit.Droid.Services
} }
if (numericKeyboard) if (numericKeyboard)
{ {
SetNumericKeyboardTo(input); input.InputType = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned;
#pragma warning disable CS0618 // Type or member is obsolete
input.KeyListener = DigitsKeyListener.GetInstance(false, false);
#pragma warning restore CS0618 // Type or member is obsolete
} }
if (password) if (password)
{ {
@@ -246,83 +248,6 @@ namespace Bit.Droid.Services
return result.Task; return result.Task;
} }
public Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config)
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
if (activity == null)
{
return Task.FromResult<ValidatablePromptResponse?>(null);
}
var alertBuilder = new AlertDialog.Builder(activity);
alertBuilder.SetTitle(config.Title);
var view = activity.LayoutInflater.Inflate(Resource.Layout.validatable_input_dialog_layout, null);
alertBuilder.SetView(view);
var result = new TaskCompletionSource<ValidatablePromptResponse?>();
alertBuilder.SetOnCancelListener(new BasicDialogWithResultCancelListener(result));
alertBuilder.SetPositiveButton(config.OkButtonText ?? AppResources.Ok, listener: null);
alertBuilder.SetNegativeButton(config.CancelButtonText ?? AppResources.Cancel, (sender, args) => result.TrySetResult(null));
if (!string.IsNullOrEmpty(config.ThirdButtonText))
{
alertBuilder.SetNeutralButton(config.ThirdButtonText, (sender, args) => result.TrySetResult(new ValidatablePromptResponse(null, true)));
}
var alert = alertBuilder.Create();
var input = view.FindViewById<EditText>(Resource.Id.txtValue);
var lblHeader = view.FindViewById<TextView>(Resource.Id.lblHeader);
var lblValueSubinfo = view.FindViewById<TextView>(Resource.Id.lblValueSubinfo);
lblHeader.Text = config.Subtitle;
lblValueSubinfo.Text = config.ValueSubInfo;
var defaultSubInfoColor = lblValueSubinfo.TextColors;
input.InputType = InputTypes.ClassText;
if (config.NumericKeyboard)
{
SetNumericKeyboardTo(input);
}
input.ImeOptions = input.ImeOptions | (ImeAction)ImeFlags.NoPersonalizedLearning | (ImeAction)ImeFlags.NoExtractUi;
input.Text = config.Text ?? string.Empty;
input.SetSelection(config.Text?.Length ?? 0);
input.AfterTextChanged += (sender, args) =>
{
if (lblValueSubinfo.Text != config.ValueSubInfo)
{
lblValueSubinfo.Text = config.ValueSubInfo;
lblHeader.SetTextColor(defaultSubInfoColor);
lblValueSubinfo.SetTextColor(defaultSubInfoColor);
}
};
alert.Window.SetSoftInputMode(SoftInput.StateVisible);
alert.Show();
var positiveButton = alert.GetButton((int)DialogButtonType.Positive);
positiveButton.Click += (sender, args) =>
{
var error = config.ValidateText(input.Text);
if (error != null)
{
lblHeader.SetTextColor(ThemeManager.GetResourceColor("DangerColor").ToAndroid());
lblValueSubinfo.SetTextColor(ThemeManager.GetResourceColor("DangerColor").ToAndroid());
lblValueSubinfo.Text = error;
lblValueSubinfo.SendAccessibilityEvent(Android.Views.Accessibility.EventTypes.ViewFocused);
return;
}
result.TrySetResult(new ValidatablePromptResponse(input.Text, false));
alert.Dismiss();
};
return result.Task;
}
public void RateApp() public void RateApp()
{ {
var activity = (MainActivity)CrossCurrentActivity.Current.Activity; var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
@@ -547,12 +472,6 @@ namespace Bit.Droid.Services
return true; return true;
} }
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;
public bool SupportsDrawOver() => Build.VERSION.SdkInt >= BuildVersionCodes.M;
private Intent RateIntentForUrl(string url, Activity activity) private Intent RateIntentForUrl(string url, Activity activity)
{ {
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}")); var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
@@ -606,61 +525,5 @@ namespace Bit.Droid.Services
// only used by iOS // only used by iOS
throw new NotImplementedException(); throw new NotImplementedException();
} }
public string GetAutofillAccessibilityDescription()
{
if (Build.VERSION.SdkInt <= BuildVersionCodes.LollipopMr1)
{
return AppResources.AccessibilityDescription;
}
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
{
return AppResources.AccessibilityDescription2;
}
if (Build.VERSION.SdkInt <= BuildVersionCodes.NMr1)
{
return AppResources.AccessibilityDescription3;
}
return AppResources.AccessibilityDescription4;
}
public string GetAutofillDrawOverDescription()
{
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
{
return AppResources.DrawOverDescription;
}
if (Build.VERSION.SdkInt <= BuildVersionCodes.NMr1)
{
return AppResources.DrawOverDescription2;
}
return AppResources.DrawOverDescription3;
}
private void SetNumericKeyboardTo(EditText editText)
{
editText.InputType = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned;
#pragma warning disable CS0618 // Type or member is obsolete
editText.KeyListener = DigitsKeyListener.GetInstance(false, false);
#pragma warning restore CS0618 // Type or member is obsolete
}
}
class BasicDialogWithResultCancelListener : Java.Lang.Object, IDialogInterfaceOnCancelListener
{
private readonly TaskCompletionSource<ValidatablePromptResponse?> _taskCompletionSource;
public BasicDialogWithResultCancelListener(TaskCompletionSource<ValidatablePromptResponse?> taskCompletionSource)
{
_taskCompletionSource = taskCompletionSource;
}
public void OnCancel(IDialogInterface dialog)
{
_taskCompletionSource?.TrySetResult(null);
dialog?.Dismiss();
}
} }
} }

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Utilities.Prompts;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models; using Bit.Core.Models;
@@ -19,7 +18,6 @@ namespace Bit.App.Abstractions
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null, Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false, string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
bool autofocus = true, bool password = false); bool autofocus = true, bool password = false);
Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config);
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons); Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons); Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
@@ -28,9 +26,6 @@ namespace Bit.App.Abstractions
bool SupportsNfc(); bool SupportsNfc();
bool SupportsCamera(); bool SupportsCamera();
bool SupportsFido2(); bool SupportsFido2();
bool SupportsAutofillServices();
bool SupportsInlineAutofill();
bool SupportsDrawOver();
bool LaunchApp(string appName); bool LaunchApp(string appName);
void RateApp(); void RateApp();
@@ -44,7 +39,5 @@ namespace Bit.App.Abstractions
Task SetScreenCaptureAllowedAsync(); Task SetScreenCaptureAllowedAsync();
void OpenAppSettings(); void OpenAppSettings();
void CloseExtensionPopUp(); void CloseExtensionPopUp();
string GetAutofillAccessibilityDescription();
string GetAutofillDrawOverDescription();
} }
} }

View File

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

View File

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

View File

@@ -91,7 +91,7 @@ namespace Bit.App
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed)); _messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
}); });
} }
else if (message.Command == AppHelpers.RESUMED_MESSAGE_COMMAND) else if (message.Command == "resumed")
{ {
if (Device.RuntimePlatform == Device.iOS) if (Device.RuntimePlatform == Device.iOS)
{ {
@@ -297,7 +297,7 @@ namespace Bit.App
{ {
await _vaultTimeoutService.CheckVaultTimeoutAsync(); await _vaultTimeoutService.CheckVaultTimeoutAsync();
// Reset delay on every start // Reset delay on every start
_vaultTimeoutService.DelayLockAndLogoutMs = null; _vaultTimeoutService.DelayTimeoutMs = null;
} }
await _configService.GetAsync(); await _configService.GetAsync();
@@ -365,7 +365,7 @@ namespace Bit.App
await Device.InvokeOnMainThreadAsync(() => await Device.InvokeOnMainThreadAsync(() =>
{ {
ThemeManager.SetTheme(Current.Resources); ThemeManager.SetTheme(Current.Resources);
_messagingService.Send(ThemeManager.UPDATED_THEME_MESSAGE_KEY); _messagingService.Send("updatedTheme");
}); });
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -95,6 +95,14 @@
<Label <Label
Text="{u:I18n CustomEnvironmentFooter}" Text="{u:I18n CustomEnvironmentFooter}"
StyleClass="box-footer-label" /> StyleClass="box-footer-label" />
<StackLayout StyleClass="box-row">
<Button Text="{u:I18n LoadFromFile}"
StyleClass="btn-primary"
Command="{Binding LoadFromFileCommand}" />
<Button Text="{u:I18n Clear}"
StyleClass="btn-secondary"
Command="{Binding ClearCommand}" />
</StackLayout>
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
</ScrollView> </ScrollView>

View File

@@ -1,11 +1,15 @@
using System; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Newtonsoft.Json;
using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@@ -27,9 +31,13 @@ namespace Bit.App.Pages
IconsUrl = _environmentService.IconsUrl; IconsUrl = _environmentService.IconsUrl;
NotificationsUrls = _environmentService.NotificationsUrl; NotificationsUrls = _environmentService.NotificationsUrl;
SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false); SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
LoadFromFileCommand = new AsyncCommand(LoadEnvironmentsFromFile, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
ClearCommand = new Command(ClearAllUrls);
} }
public ICommand SubmitCommand { get; } public ICommand SubmitCommand { get; }
public ICommand LoadFromFileCommand { get; }
public ICommand ClearCommand { get; }
public string BaseUrl { get; set; } public string BaseUrl { get; set; }
public string ApiUrl { get; set; } public string ApiUrl { get; set; }
public string IdentityUrl { get; set; } public string IdentityUrl { get; set; }
@@ -87,5 +95,75 @@ namespace Bit.App.Pages
_logger.Value.Exception(ex); _logger.Value.Exception(ex);
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok); Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
} }
private async Task LoadEnvironmentsFromFile()
{
try
{
string jsonString;
var result = await FilePicker.PickAsync(new PickOptions
{
PickerTitle = "This a test to pick files"
});
if (result != null)
{
if (result.FileName.EndsWith("json", StringComparison.OrdinalIgnoreCase) ||
result.FileName.EndsWith("txt", StringComparison.OrdinalIgnoreCase))
{
var stream = await result.OpenReadAsync();
using (var reader = new System.IO.StreamReader(stream))
{
jsonString = reader.ReadToEnd();
}
var envUrls = JsonConvert.DeserializeObject<EnvironmentsData>(jsonString);
BaseUrl = envUrls.Base;
ApiUrl = envUrls.Api;
IdentityUrl = envUrls.Identity;
WebVaultUrl = envUrls.Vault;
IconsUrl = envUrls.Icons;
NotificationsUrls = envUrls.Notifications;
NotifyUrlsChanged();
}
}
}
catch (Exception ex)
{
HandleException(ex);
}
}
private void ClearAllUrls()
{
BaseUrl = string.Empty;
ApiUrl = string.Empty;
IdentityUrl = string.Empty;
WebVaultUrl = string.Empty;
IconsUrl = string.Empty;
NotificationsUrls = string.Empty;
NotifyUrlsChanged();
}
private void NotifyUrlsChanged() {
TriggerPropertyChanged(nameof(BaseUrl), new[]
{
nameof(ApiUrl),
nameof(IdentityUrl),
nameof(WebVaultUrl),
nameof(IconsUrl),
nameof(NotificationsUrls)
});
}
}
public class EnvironmentsData
{
public string Base { get; set; }
public string Admin { get; set; }
public string Api { get; set; }
public string Identity { get; set; }
public string Icons { get; set; }
public string Notifications { get; set; }
public string Sso { get; set; }
public string Vault { get; set; }
} }
} }

View File

@@ -23,8 +23,7 @@
Priority="-1" Priority="-1"
UseOriginalImage="True" UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" AutomationProperties.Name="{u:I18n Account}" />
AutomationId="AccountIconButton" />
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/> <ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>

View File

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

View File

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

View File

@@ -24,8 +24,7 @@
Priority="-1" Priority="-1"
UseOriginalImage="True" UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" AutomationProperties.Name="{u:I18n Account}" />
AutomationId="AccountIconButton" />
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>
<ContentPage.Resources> <ContentPage.Resources>
@@ -46,7 +45,7 @@
<StackLayout StyleClass="box"> <StackLayout StyleClass="box">
<Grid <Grid
StyleClass="box-row" StyleClass="box-row"
IsVisible="{Binding PinEnabled}" IsVisible="{Binding PinLock}"
Padding="0, 10, 0, 0"> Padding="0, 10, 0, 0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -89,7 +88,7 @@
<Grid <Grid
x:Name="_passwordGrid" x:Name="_passwordGrid"
StyleClass="box-row" StyleClass="box-row"
IsVisible="{Binding PinEnabled, Converter={StaticResource inverseBool}}" IsVisible="{Binding PinLock, Converter={StaticResource inverseBool}}"
Padding="0, 10, 0, 0"> Padding="0, 10, 0, 0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />

View File

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

View File

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

View File

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

View File

@@ -1,64 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class LoginApproveDevicePage : BaseContentPage
{
private readonly LoginApproveDeviceViewModel _vm;
private readonly AppOptions _appOptions;
public LoginApproveDevicePage(AppOptions appOptions = null)
{
InitializeComponent();
_vm = BindingContext as LoginApproveDeviceViewModel;
_vm.LogInWithMasterPasswordAction = () => StartLogInWithMasterPasswordAsync().FireAndForget();
_vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget();
_vm.RequestAdminApprovalAction = () => RequestAdminApprovalAsync().FireAndForget();
_vm.ContinueToVaultAction = () => ContinueToVaultAsync().FireAndForget();
_vm.Page = this;
_appOptions = appOptions;
}
protected override void OnAppearing()
{
_vm.InitAsync();
}
private async Task ContinueToVaultAsync()
{
if (AppHelpers.SetAlternateMainPage(_appOptions))
{
return;
}
var previousPage = await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
private async Task StartLogInWithMasterPasswordAsync()
{
var page = new LockPage(_appOptions, checkPendingAuthRequests: false);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task StartLoginWithDeviceAsync()
{
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions, true);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task RequestAdminApprovalAsync()
{
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AdminApproval, _appOptions, true);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
}

View File

@@ -1,150 +0,0 @@
using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities.AccountManagement;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class LoginApproveDeviceViewModel : BaseViewModel
{
private bool _rememberThisDevice;
private bool _approveWithMyOtherDeviceEnabled;
private bool _requestAdminApprovalEnabled;
private bool _approveWithMasterPasswordEnabled;
private string _email;
private readonly IStateService _stateService;
private readonly IApiService _apiService;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
private readonly IAuthService _authService;
private readonly ISyncService _syncService;
private readonly IMessagingService _messagingService;
public ICommand ApproveWithMyOtherDeviceCommand { get; }
public ICommand RequestAdminApprovalCommand { get; }
public ICommand ApproveWithMasterPasswordCommand { get; }
public ICommand ContinueCommand { get; }
public ICommand LogoutCommand { get; }
public Action LogInWithMasterPasswordAction { get; set; }
public Action LogInWithDeviceAction { get; set; }
public Action RequestAdminApprovalAction { get; set; }
public Action ContinueToVaultAction { get; set; }
public LoginApproveDeviceViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>();
_apiService = ServiceContainer.Resolve<IApiService>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
_authService = ServiceContainer.Resolve<IAuthService>();
_syncService = ServiceContainer.Resolve<ISyncService>();
_messagingService = ServiceContainer.Resolve<IMessagingService>();
PageTitle = AppResources.LogInInitiated;
RememberThisDevice = true;
ApproveWithMyOtherDeviceCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithDeviceAction),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
RequestAdminApprovalCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(RequestAdminApprovalAction),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
ApproveWithMasterPasswordCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithMasterPasswordAction),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
ContinueCommand = new AsyncCommand(CreateNewSsoUserAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
LogoutCommand = new Command(() => _messagingService.Send(AccountsManagerMessageCommands.LOGOUT));
}
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
public bool RememberThisDevice
{
get => _rememberThisDevice;
set => SetProperty(ref _rememberThisDevice, value);
}
public bool ApproveWithMyOtherDeviceEnabled
{
get => _approveWithMyOtherDeviceEnabled;
set => SetProperty(ref _approveWithMyOtherDeviceEnabled, value);
}
public bool RequestAdminApprovalEnabled
{
get => _requestAdminApprovalEnabled;
set => SetProperty(ref _requestAdminApprovalEnabled, value,
additionalPropertyNames: new[] { nameof(IsNewUser) });
}
public bool ApproveWithMasterPasswordEnabled
{
get => _approveWithMasterPasswordEnabled;
set => SetProperty(ref _approveWithMasterPasswordEnabled, value,
additionalPropertyNames: new[] { nameof(IsNewUser) });
}
public bool IsNewUser => !RequestAdminApprovalEnabled && !ApproveWithMasterPasswordEnabled;
public string Email
{
get => _email;
set => SetProperty(ref _email, value, additionalPropertyNames:
new string[] {
nameof(LoggingInAsText)
});
}
public async Task InitAsync()
{
try
{
Email = await _stateService.GetActiveUserEmailAsync();
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
RequestAdminApprovalEnabled = decryptOptions?.TrustedDeviceOption?.HasAdminApproval ?? false;
ApproveWithMasterPasswordEnabled = decryptOptions?.HasMasterPassword ?? false;
ApproveWithMyOtherDeviceEnabled = decryptOptions?.TrustedDeviceOption?.HasLoginApprovingDevice ?? false;
}
catch (Exception ex)
{
HandleException(ex);
}
}
public async Task CreateNewSsoUserAsync()
{
await _authService.CreateNewSsoUserAsync(await _stateService.GetRememberedOrgIdentifierAsync());
if (RememberThisDevice)
{
await _deviceTrustCryptoService.TrustDeviceAsync();
}
_syncService.FullSyncAsync(true).FireAndForget();
await Device.InvokeOnMainThreadAsync(ContinueToVaultAction);
}
private async Task SetDeviceTrustAndInvokeAsync(Action action)
{
await _deviceTrustCryptoService.SetShouldTrustDeviceAsync(RememberThisDevice);
await Device.InvokeOnMainThreadAsync(action);
}
}
}

View File

@@ -24,8 +24,7 @@
Priority="-1" Priority="-1"
UseOriginalImage="True" UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" AutomationProperties.Name="{u:I18n Account}" />
AutomationId="AccountIconButton" />
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>
<ContentPage.Resources> <ContentPage.Resources>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,8 +25,7 @@
Priority="-2" Priority="-2"
UseOriginalImage="True" UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True" AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" AutomationProperties.Name="{u:I18n Account}" />
AutomationId="AccountIconButton" />
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" /> <ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" />
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/> <ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/>
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>
@@ -94,13 +93,13 @@
LineBreakMode="CharacterWrap" LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted" StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Start" /> HorizontalTextAlignment="Center" />
<Label <Label
Margin="0, 5, 0, 0" Margin="0, 5, 0, 0"
Text="{u:I18n MaxFileSize}" Text="{u:I18n MaxFileSize}"
StyleClass="text-sm, text-muted" StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Start" /> HorizontalTextAlignment="Center" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
<StackLayout <StackLayout
@@ -145,6 +144,19 @@
Margin="10,0,0,0" /> Margin="10,0,0,0" />
</StackLayout> </StackLayout>
</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 <StackLayout
Orientation="Horizontal" Orientation="Horizontal"
Spacing="0" Spacing="0"

View File

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

View File

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

View File

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

View File

@@ -1,85 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:Bit.App.Pages"
x:DataType="pages:AboutSettingsPageViewModel"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:Class="Bit.App.Pages.AboutSettingsPage"
Title="{u:I18n About}">
<ContentPage.BindingContext>
<pages:AboutSettingsPageViewModel />
</ContentPage.BindingContext>
<StackLayout>
<controls:SwitchItemView
Title="{u:I18n SubmitCrashLogs}"
IsToggled="{Binding ShouldSubmitCrashLogs, Mode=TwoWay}"
AutomationId="SubmitCrashLogsSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<controls:ExternalLinkItemView
Title="{u:I18n BitwardenHelpCenter}"
GoToLinkCommand="{Binding GoToHelpCenterCommand}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<controls:ExternalLinkItemView
Title="{u:I18n ContactBitwardenSupport}"
GoToLinkCommand="{Binding ContactBitwardenSupportCommand}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<controls:ExternalLinkItemView
Title="{u:I18n WebVault}"
GoToLinkCommand="{Binding GoToWebVaultCommand}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<controls:ExternalLinkItemView
Title="{u:I18n LearnOrg}"
GoToLinkCommand="{Binding GoToLearnAboutOrgsCommand}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<controls:ExternalLinkItemView
Title="{u:I18n RateTheApp}"
GoToLinkCommand="{Binding RateTheAppCommand}"
StyleClass="settings-external-link-item"
HorizontalOptions="FillAndExpand" />
<BoxView StyleClass="box-row-separator" />
<StackLayout
Padding="16,12"
Orientation="Horizontal">
<controls:CustomLabel
Text="{Binding AppInfo}"
MaxLines="10"
StyleClass="box-footer-label"
HorizontalOptions="StartAndExpand"
LineBreakMode="TailTruncation" />
<controls:IconLabel
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
TextColor="Black"
HorizontalOptions="End"
VerticalOptions="Center"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyAppInformation}">
<controls:IconLabel.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CopyAppInfoCommand}" />
</controls:IconLabel.GestureRecognizers>
</controls:IconLabel>
</StackLayout>
</StackLayout>
</pages:BaseContentPage>

View File

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

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