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

Compare commits

..

8 Commits

Author SHA1 Message Date
mp-bw
8e3521f8d9 [PM-1567] Fix for vault timeout 'never' not persisting (#2440) 2023-03-27 10:38:34 -04:00
github-actions[bot]
9a027ea4b0 Bumped version to 2023.3.2 (#2436)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
(cherry picked from commit 55a3b76f45)
2023-03-27 10:34:18 -04:00
github-actions[bot]
08d07603a8 Bumped version to 2023.3.1 (#2432)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2023-03-23 18:10:10 +01:00
mp-bw
b9bb21b1db Fix migration crash (#2430) 2023-03-23 11:48:43 -04:00
github-actions[bot]
39d275453e Bumped version to 2023.3.0 (#2423)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
(cherry picked from commit c6bdb67981)
2023-03-22 10:45:56 -04:00
André Bispo
e23ab78bd8 [PM-1497] Known device api error (#2418)
* [PM-1497] Ignore know device api error.
2023-03-17 15:08:29 +00:00
André Bispo
4ed5980cb5 [PM-1431] [Defect] [Android] New accounts are not able to log in (#2417)
* [PM-1431] Do not clear Master password if login is ongoing.
2023-03-16 12:50:26 +00:00
André Bispo
8c273f7d93 [PM-1078] Login with Device - Change mobile to not get fingerprint from API (#2390)
* [PM-1078] Fingerprint phrase gets calculated from pub key on AuthService instead of coming as a property from the api.

(cherry picked from commit ccd71202de)
2023-03-13 18:27:59 -04:00
405 changed files with 5936 additions and 30185 deletions

View File

@@ -1,8 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Customer Support
url: https://bitwarden.com/contact/
about: Please contact our customer support for account issues and general customer support.
- name: Report mobile autofill failure
url: https://docs.google.com/forms/d/e/1FAIpQLScMopHyN7KGJs8hW562VTzbIGL4KcFnx0wJcsW0GYE1BnPiGA/viewform
about: We are aware of some situations where the Bitwarden mobile app will not autofill information correctly. This is something the Bitwarden team is actively working on but need your help as a community and active Bitwarden users!
@@ -12,6 +9,9 @@ contact_links:
- name: Bitwarden Community Forums
url: https://community.bitwarden.com
about: Please visit the community forums for general community discussion, support and the development roadmap.
- name: Customer Support
url: https://bitwarden.com/contact/
about: Please contact our customer support for account issues and general customer support.
- name: Security Issues
url: https://hackerone.com/bitwarden
about: We use HackerOne to manage security disclosures.

19
.github/labeler.yml vendored
View File

@@ -1,19 +0,0 @@
android:
- src/App/*
- src/Core/*
- src/Android/*
iOS:
- src/App/*
- src/Core/*
- lib/ios/*
- src/iOS/*
- 'src/iOS.Autofill/*'
- 'src/iOS.Core/*'
- 'src/iOS.Extension/*'
- 'src/iOS.ShareExtension/*'
- 'src/iOS.Widget/*'
- src/watchOS/*
watchOS:
- src/watchOS/*

57
.github/renovate.json vendored
View File

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

View File

@@ -14,7 +14,7 @@ jobs:
# Feature request
- if: github.event.label.name == 'feature-request'
name: Feature request
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
with:
comment: |
We use GitHub issues as a place to track bugs and other development related issues. The [Bitwarden Community Forums](https://community.bitwarden.com/) has a [Feature Requests](https://community.bitwarden.com/c/feature-requests) section for submitting, voting for, and discussing requests like this one.
@@ -25,7 +25,7 @@ jobs:
# Intended behavior
- if: github.event.label.name == 'intended-behavior'
name: Intended behaviour
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
with:
comment: |
Your issue appears to be describing the intended behavior of the software. If you want this to be changed, it would be a feature request.
@@ -38,7 +38,7 @@ jobs:
# Customer support request
- if: github.event.label.name == 'customer-support'
name: Customer Support request
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
with:
comment: |
We use GitHub issues as a place to track bugs and other development related issues. Your issue appears to be a support request, or would otherwise be better handled by our dedicated Customer Success team.
@@ -49,14 +49,14 @@ jobs:
# Resolved
- if: github.event.label.name == 'resolved'
name: Resolved
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
with:
comment: |
Weve closed this issue, as it appears the original problem has been resolved. If this happens again or continues to be an problem, please respond to this issue with any additional detail to assist with reproduction and root cause analysis.
# Stale
- if: github.event.label.name == 'stale'
name: Stale
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
with:
comment: |
As we havent heard from you about this problem in some time, this issue will now be closed.

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Set up CLOC
run: |
@@ -36,7 +36,7 @@ jobs:
hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }}
steps:
- name: Checkout repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
with:
submodules: 'true'
@@ -67,17 +67,12 @@ jobs:
variant: ["prod", "qa"]
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
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
uses: microsoft/setup-msbuild@1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c # v1.3.1
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
- name: Setup Windows builder
run: choco install checksum --no-progress
@@ -110,7 +105,7 @@ jobs:
echo "GitHub event: $GITHUB_EVENT"
- name: Checkout repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
with:
fetch-depth: 0
- name: Decrypt secrets
@@ -162,7 +157,7 @@ jobs:
shell: pwsh
- name: Report test results
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226
if: always()
with:
name: Test Results
@@ -196,7 +191,7 @@ jobs:
$androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj");
$packageName = "com.x8bit.bitwarden";
if ("${{ matrix.variant }}" -ne "prod")
if ("${{ matrix.variant }}" -ne "prod")
{
$packageName = "com.x8bit.bitwarden.${{ matrix.variant }}";
}
@@ -237,7 +232,7 @@ jobs:
shell: pwsh
- name: Upload Prod .aab artifact
if: ${{ matrix.variant == 'prod' }}
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: com.x8bit.bitwarden.aab
path: ./com.x8bit.bitwarden.aab
@@ -245,7 +240,7 @@ jobs:
- name: Upload Prod .apk artifact
if: ${{ matrix.variant == 'prod' }}
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: com.x8bit.bitwarden.apk
path: ./com.x8bit.bitwarden.apk
@@ -253,7 +248,7 @@ jobs:
- name: Upload Other .apk artifact
if: ${{ matrix.variant != 'prod' }}
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk
@@ -273,7 +268,7 @@ jobs:
- name: Upload .apk sha file for prod
if: ${{ matrix.variant == 'prod' }}
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-android-apk-sha256.txt
path: ./bw-android-apk-sha256.txt
@@ -281,7 +276,7 @@ jobs:
- name: Upload .apk sha file for other
if: ${{ matrix.variant != 'prod' }}
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
@@ -308,12 +303,12 @@ jobs:
runs-on: windows-2022
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
nuget-version: 5.9.0
- name: Set up MSBuild
uses: microsoft/setup-msbuild@1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c # v1.3.1
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
- name: Setup Windows builder
run: choco install checksum --no-progress
@@ -347,7 +342,7 @@ jobs:
echo "GitHub event: $GITHUB_EVENT"
- name: Checkout repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Decrypt secrets
env:
@@ -482,7 +477,7 @@ jobs:
shell: pwsh
- name: Upload F-Droid .apk artifact
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: com.x8bit.bitwarden-fdroid.apk
path: ./com.x8bit.bitwarden-fdroid.apk
@@ -494,7 +489,7 @@ jobs:
-t sha256 | Out-File -Encoding ASCII ./bw-fdroid-apk-sha256.txt
- name: Upload F-Droid sha file
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: bw-fdroid-apk-sha256.txt
path: ./bw-fdroid-apk-sha256.txt
@@ -507,7 +502,7 @@ jobs:
needs: setup
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
nuget-version: 5.9.0
@@ -520,19 +515,19 @@ jobs:
echo "GitHub event: $GITHUB_EVENT"
- name: Checkout repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
with:
submodules: 'true'
- name: Login to Azure - CI Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
- name: Login to Azure - Prod Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
env:
KEYVAULT: bitwarden-ci
KEYVAULT: bitwarden-prod-kv
SECRETS: |
appcenter-ios-token
run: |
@@ -703,7 +698,7 @@ jobs:
shell: bash
- name: Upload App Store .ipa & dSYMs artifacts
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: Bitwarden iOS
path: |
@@ -776,17 +771,17 @@ jobs:
_CROWDIN_PROJECT_ID: "269690"
steps:
- name: Checkout repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Login to Azure - CI Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
- name: Login to Azure
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
env:
KEYVAULT: bitwarden-ci
KEYVAULT: bitwarden-prod-kv
SECRETS: |
crowdin-api-token
run: |
@@ -798,7 +793,7 @@ jobs:
done
- name: Upload Sources
uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
@@ -844,17 +839,17 @@ jobs:
exit 1
fi
- name: Login to Azure - CI Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
- name: Login to Azure - Prod Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
if: failure()
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
if: failure()
env:
KEYVAULT: bitwarden-ci
KEYVAULT: bitwarden-prod-kv
SECRETS: |
devops-alerts-slack-webhook-url
run: |
@@ -866,7 +861,7 @@ jobs:
done
- name: Notify Slack on failure
uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}

View File

@@ -15,22 +15,22 @@ jobs:
_CROWDIN_PROJECT_ID: "269690"
steps:
- name: Checkout repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Login to Azure - CI Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
- name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
with:
keyvault: "bitwarden-ci"
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Download translations
uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
@@ -48,4 +48,4 @@ jobs:
pull_request_title: "Autosync Crowdin Translations"
pull_request_body: "Autosync the updated translations"
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}

View File

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

View File

@@ -1,17 +0,0 @@
---
name: "Pull Request Labeler"
on:
pull_request_target: {}
jobs:
labeler:
name: "Pull Request Labeler"
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-20.04
steps:
- uses: actions/labeler@ba790c862c380240c6d5e7427be5ace9a05c754b # v4.0.3
with:
sync-labels: true

View File

@@ -38,11 +38,11 @@ jobs:
fi
- name: Checkout repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Check Release Version
id: version
uses: bitwarden/gh-actions/release-version-check@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/release-version-check@8f055ef543c7433c967a1b9b04a0f230923233bb
with:
release-type: ${{ github.event.inputs.release_type }}
project-type: xamarin
@@ -56,7 +56,7 @@ jobs:
- name: Create GitHub deployment
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: chrnorm/deployment-action@d42cde7132fcec920de534fffc3be83794335c00 # v2.0.5
uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48
id: deployment
with:
token: '${{ secrets.GITHUB_TOKEN }}'
@@ -68,7 +68,7 @@ jobs:
- name: Download all artifacts
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:
workflow: build.yml
workflow_conclusion: success
@@ -76,7 +76,7 @@ jobs:
- name: Dry Run - Download all artifacts
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:
workflow: build.yml
workflow_conclusion: success
@@ -93,8 +93,8 @@ jobs:
./com.x8bit.bitwarden.apk/com.x8bit.bitwarden.apk,
./com.x8bit.bitwarden-fdroid.apk/com.x8bit.bitwarden-fdroid.apk,
./Bitwarden iOS.zip,
./bw-android-apk-sha256.txt/bw-android-apk-sha256.txt,
./bw-fdroid-apk-sha256.txt/bw-fdroid-apk-sha256.txt"
./bw-android-apk-sha256.txt,
./bw-fdroid-apk-sha256.txt"
commit: ${{ github.sha }}
tag: v${{ steps.version.outputs.version }}
name: Version ${{ steps.version.outputs.version }}
@@ -104,7 +104,7 @@ jobs:
- name: Update deployment status to Success
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'success'
@@ -112,7 +112,7 @@ jobs:
- name: Update deployment status to Failure
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'failure'
@@ -126,11 +126,11 @@ jobs:
if: inputs.fdroid_publish
steps:
- name: Checkout repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
with:
workflow: build.yml
workflow_conclusion: success
@@ -139,7 +139,7 @@ jobs:
- name: Dry Run - Download F-Droid .apk artifact
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:
workflow: build.yml
workflow_conclusion: success

View File

@@ -14,7 +14,7 @@ jobs:
version_number: ${{ steps.version.outputs.new-version }}
steps:
- name: Checkout Branch
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Calculate bumped version
id: version
@@ -32,10 +32,32 @@ jobs:
echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT
trigger_version_bump:
name: Bump version to ${{ needs.setup.outputs.version_number }}
needs: setup
uses: ./.github/workflows/version-bump.yml
secrets:
AZURE_PROD_KV_CREDENTIALS: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
with:
version_number: ${{ needs.setup.outputs.version_number }}
name: "Trigger version bump workflow"
runs-on: ubuntu-22.04
needs:
- setup
steps:
- name: Login to Azure
uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
with:
keyvault: "bitwarden-prod-kv"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Call GitHub API to trigger workflow bump
env:
TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
VERSION: ${{ needs.setup.outputs.version_number}}
run: |
JSON_STRING=$(printf '{"ref":"master", "inputs": { "version_number":"%s"}}' "$VERSION")
curl \
-X POST \
-i -u bitwarden-devops-bot:$TOKEN \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/bitwarden/mobile/actions/workflows/version-bump.yml/dispatches \
-d $JSON_STRING

View File

@@ -7,14 +7,6 @@ on:
version_number:
description: "New Version"
required: true
workflow_call:
inputs:
version_number:
required: true
type: string
secrets:
AZURE_PROD_KV_CREDENTIALS:
required: true
jobs:
bump_version:
@@ -22,22 +14,22 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout Branch
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Login to Azure - CI Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
- name: Login to Azure - Prod Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
with:
keyvault: "bitwarden-ci"
keyvault: "bitwarden-prod-kv"
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@111c56156bcc6918c056dbef52164cfa583dc549 # v5.2.0
uses: crazy-max/ghaction-import-gpg@c8bb57c57e8df1be8c73ff3d59deab1dbc00e0d1
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
@@ -48,31 +40,31 @@ jobs:
run: git switch -c version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - Android XML
uses: bitwarden/gh-actions/version-bump@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/Android/Properties/AndroidManifest.xml"
- name: Bump Version - iOS.Autofill
uses: bitwarden/gh-actions/version-bump@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/iOS.Autofill/Info.plist"
- name: Bump Version - iOS.Extension
uses: bitwarden/gh-actions/version-bump@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/iOS.Extension/Info.plist"
- name: Bump Version - iOS.ShareExtension
uses: bitwarden/gh-actions/version-bump@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/iOS.ShareExtension/Info.plist"
- name: Bump Version - iOS
uses: bitwarden/gh-actions/version-bump@34ecb67b2a357795dc893549df0795e7383ff50f
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/iOS/Info.plist"

View File

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

View File

@@ -4,7 +4,7 @@
# Bitwarden Mobile Application
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden" target="_blank"><img alt="Get it on Google Play" src="https://imgur.com/YQzmZi9.png" width="153" height="46"></a> <a href="https://mobileapp.bitwarden.com/fdroid/" target="_blank"><img alt="Get it on 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.

View File

@@ -38,15 +38,3 @@ files:
pt-PT: pt-PT
en-GB: en-GB
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

@@ -78,20 +78,21 @@
<Version>1.9.0</Version>
</PackageReference>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1.1" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.16" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.19" />
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.10.0" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.14" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.17" />
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0.1" />
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.15" />
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1.1" />
<PackageReference Include="Xamarin.Essentials">
<Version>1.7.5</Version>
<Version>1.7.3</Version>
</PackageReference>
<PackageReference Include="Xamarin.Firebase.Messaging">
<Version>123.1.1.1</Version>
<Version>123.0.8</Version>
</PackageReference>
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.8.0" />
<PackageReference Include="Xamarin.Google.Dagger" Version="2.44.2.1" />
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.6.1.1" />
<PackageReference Include="Xamarin.Google.Dagger" Version="2.41.0.2" />
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
<Version>118.0.1.3</Version>
<Version>118.0.1.2</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
@@ -159,7 +160,6 @@
<Compile Include="Constants.cs" />
<Compile Include="Effects\RemoveFontPaddingEffect.cs" />
<Compile Include="Services\WatchDeviceService.cs" />
<Compile Include="Renderers\CustomLabelRenderer.cs" />
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\bwi-font.ttf" />
@@ -233,18 +233,6 @@
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
<AndroidResource Include="Resources\layout\validatable_input_dialog_layout.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\empty_uris_placeholder.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\empty_uris_placeholder_dark.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\splash_screen.xml" />

Binary file not shown.

View File

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

View File

@@ -21,7 +21,6 @@ using Bit.App.Utilities;
using Bit.App.Pages;
using Bit.App.Utilities.AccountManagement;
using Bit.App.Controls;
using Bit.Core.Enums;
#if !FDROID
using Android.Gms.Security;
#endif
@@ -70,8 +69,7 @@ namespace Bit.Droid
var verificationActionsFlowHelper = new VerificationActionsFlowHelper(
ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService"),
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
ServiceContainer.Resolve<ICryptoService>("cryptoService"),
ServiceContainer.Resolve<IUserVerificationService>());
ServiceContainer.Resolve<ICryptoService>("cryptoService"));
ServiceContainer.Register<IVerificationActionsFlowHelper>("verificationActionsFlowHelper", verificationActionsFlowHelper);
var accountsManager = new AccountsManager(
@@ -83,8 +81,7 @@ namespace Bit.Droid
ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"),
ServiceContainer.Resolve<IMessagingService>("messagingService"),
ServiceContainer.Resolve<IWatchDeviceService>(),
ServiceContainer.Resolve<IConditionedAwaiterManager>());
ServiceContainer.Resolve<IWatchDeviceService>());
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
}
#if !FDROID
@@ -149,7 +146,7 @@ namespace Bit.Droid
var storageMediatorService = new StorageMediatorService(mobileStorageService, secureStorageService, preferencesStorage);
var stateService = new StateService(mobileStorageService, secureStorageService, storageMediatorService, messagingService);
var stateMigrationService =
new StateMigrationService(DeviceType.Android, liteDbStorage, preferencesStorage, secureStorageService);
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
var clipboardService = new ClipboardService(stateService);
var deviceActionService = new DeviceActionService(stateService, messagingService);
var fileService = new FileService(stateService, broadcasterService);
@@ -157,9 +154,9 @@ namespace Bit.Droid
messagingService, broadcasterService);
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService,
platformUtilsService, new LazyResolve<IEventService>());
var biometricService = new BiometricService();
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
var biometricService = new BiometricService(stateService, cryptoService);
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService);
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);

View File

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

View File

@@ -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,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">
<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="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>

View File

@@ -2,15 +2,15 @@
using System.Threading.Tasks;
using Android.OS;
using Android.Security.Keystore;
using Bit.App.Services;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Java.Security;
using Javax.Crypto;
namespace Bit.Droid.Services
{
public class BiometricService : BaseBiometricService
public class BiometricService : IBiometricService
{
private const string KeyName = "com.8bit.bitwarden.biometric_integrity";
@@ -23,28 +23,28 @@ namespace Bit.Droid.Services
private readonly KeyStore _keystore;
public BiometricService(IStateService stateService, ICryptoService cryptoService)
: base(stateService, cryptoService)
public BiometricService()
{
_keystore = KeyStore.GetInstance(KeyStoreName);
_keystore.Load(null);
}
public override async Task<bool> SetupBiometricAsync(string bioIntegritySrcKey = null)
public Task<bool> SetupBiometricAsync(string bioIntegrityKey = null)
{
// bioIntegrityKey used in iOS only
if (Build.VERSION.SdkInt >= BuildVersionCodes.M)
{
await CreateKeyAsync(bioIntegritySrcKey);
CreateKey();
}
return true;
return Task.FromResult(true);
}
public override async Task<bool> IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null)
public Task<bool> ValidateIntegrityAsync(string bioIntegrityKey = null)
{
if (Build.VERSION.SdkInt < BuildVersionCodes.M)
{
return true;
return Task.FromResult(true);
}
try
@@ -55,7 +55,7 @@ namespace Bit.Droid.Services
if (key == null || cipher == null)
{
return true;
return Task.FromResult(true);
}
cipher.Init(CipherMode.EncryptMode, key);
@@ -63,32 +63,25 @@ namespace Bit.Droid.Services
catch (KeyPermanentlyInvalidatedException e)
{
// Biometric has changed
await ClearStateAsync(bioIntegritySrcKey);
return false;
return Task.FromResult(false);
}
catch (UnrecoverableKeyException e)
{
// Biometric was disabled and re-enabled
await ClearStateAsync(bioIntegritySrcKey);
return false;
return Task.FromResult(false);
}
catch (InvalidKeyException e)
{
// Fallback for old bitwarden users without a key
LoggerHelper.LogEvenIfCantBeResolved(e);
await CreateKeyAsync(bioIntegritySrcKey);
CreateKey();
}
return true;
return Task.FromResult(true);
}
private async Task CreateKeyAsync(string bioIntegritySrcKey = null)
private void CreateKey()
{
bioIntegritySrcKey ??= Core.Constants.BiometricIntegritySourceKey;
await _stateService.SetSystemBiometricIntegrityState(bioIntegritySrcKey,
await GetStateAsync(bioIntegritySrcKey));
await _stateService.SetAccountBiometricIntegrityValidAsync(bioIntegritySrcKey);
try
{
var keyGen = KeyGenerator.GetInstance(KeyAlgorithm, KeyStoreName);
@@ -108,16 +101,5 @@ namespace Bit.Droid.Services
LoggerHelper.LogEvenIfCantBeResolved(e);
}
}
private async Task<string> GetStateAsync(string bioIntegritySrcKey)
{
return await _stateService.GetSystemBiometricIntegrityState(bioIntegritySrcKey) ??
Guid.NewGuid().ToString();
}
private async Task ClearStateAsync(string bioIntegritySrcKey)
{
await _stateService.SetSystemBiometricIntegrityState(bioIntegritySrcKey, null);
}
}
}

View File

@@ -13,13 +13,12 @@ using Android.Views.InputMethods;
using Android.Widget;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.App.Utilities.Prompts;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Droid.Utilities;
using Plugin.CurrentActivity;
using Xamarin.Forms.Platform.Android;
using static Bit.App.Pages.SettingsPageViewModel;
namespace Bit.Droid.Services
{
@@ -210,7 +209,10 @@ namespace Bit.Droid.Services
}
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)
{
@@ -246,83 +248,6 @@ namespace Bit.Droid.Services
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()
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
@@ -600,29 +525,5 @@ namespace Bit.Droid.Services
// only used by iOS
throw new NotImplementedException();
}
private void SetNumericKeyboardTo(EditText editText)
{
editText.InputType = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned;
#pragma warning disable CS0618 // Type or member is obsolete
editText.KeyListener = DigitsKeyListener.GetInstance(false, false);
#pragma warning restore CS0618 // Type or member is obsolete
}
}
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

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

View File

@@ -1,5 +1,4 @@
using System.Threading.Tasks;
using Bit.App.Utilities.Prompts;
using Bit.Core.Enums;
using Bit.Core.Models;
@@ -19,7 +18,6 @@ namespace Bit.App.Abstractions
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = 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> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);

View File

@@ -9,5 +9,7 @@ namespace Bit.App.Abstractions
Task<bool> ShowPasswordPromptAsync();
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
Task<bool> Enabled();
}
}

View File

@@ -14,11 +14,11 @@
<ItemGroup>
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.88.3" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.6" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.5" />
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.88.2" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.5" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2578" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
<PackageReference Include="MessagePack" Version="2.4.59" />
@@ -145,8 +145,6 @@
<Folder Include="Controls\DateTime\" />
<Folder Include="Controls\IconLabelButton\" />
<Folder Include="Controls\PasswordStrengthProgressBar\" />
<Folder Include="Utilities\Automation\" />
<Folder Include="Utilities\Prompts\" />
</ItemGroup>
<ItemGroup>
@@ -442,7 +440,5 @@
<None Remove="MessagePack" />
<None Remove="MessagePack.MSBuild.Tasks" />
<None Remove="Controls\PasswordStrengthProgressBar\" />
<None Remove="Utilities\Automation\" />
<None Remove="Utilities\Prompts\" />
</ItemGroup>
</Project>

View File

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

View File

@@ -30,15 +30,13 @@
BackgroundColor="{DynamicResource BackgroundColor}"
VerticalOptions="Start"
RowHeight="{Binding AccountListRowHeight, Source={x:Reference _mainOverlay}}"
effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never"
AutomationId="AccountListView">
effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never">
<ListView.ItemTemplate>
<DataTemplate x:DataType="view:AccountView">
<controls:AccountViewCell
Account="{Binding .}"
SelectAccountCommand="{Binding SelectAccountCommand, Source={x:Reference _mainOverlay}}"
LongPressAccountCommand="{Binding LongPressAccountCommand, Source={x:Reference _mainOverlay}}"
AutomationId="AccountViewCell"
/>
</DataTemplate>
</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"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
@@ -60,23 +60,20 @@
Text="{Binding AccountView.Email}"
IsVisible="{Binding IsActive}"
StyleClass="accountlist-title, accountlist-title-platform"
LineBreakMode="TailTruncation"
AutomationId="AccountEmailLabel" />
LineBreakMode="TailTruncation" />
<Label
Grid.Row="0"
Text="{Binding AccountView.Email}"
IsVisible="{Binding IsActive, Converter={StaticResource inverseBool}}"
StyleClass="accountlist-title, accountlist-title-platform"
TextColor="{DynamicResource MutedColor}"
LineBreakMode="TailTruncation"
AutomationId="AccountEmailLabel" />
LineBreakMode="TailTruncation" />
<Label
Grid.Row="1"
IsVisible="{Binding ShowHostname}"
Text="{Binding AccountView.Hostname}"
StyleClass="accountlist-sub, accountlist-sub-platform"
LineBreakMode="TailTruncation"
AutomationId="AccountHostUrlLabel" />
LineBreakMode="TailTruncation" />
<Label
Grid.Row="2"
Text="{u:I18n AccountUnlocked}"
@@ -84,8 +81,7 @@
StyleClass="accountlist-sub, accountlist-sub-platform"
FontAttributes="Italic"
TextTransform="Lowercase"
LineBreakMode="TailTruncation"
AutomationId="AccountStatusLabel" />
LineBreakMode="TailTruncation" />
<Label
Grid.Row="2"
Text="{u:I18n AccountLocked}"
@@ -93,8 +89,7 @@
StyleClass="accountlist-sub, accountlist-sub-platform"
FontAttributes="Italic"
TextTransform="Lowercase"
LineBreakMode="TailTruncation"
AutomationId="AccountStatusLabel" />
LineBreakMode="TailTruncation" />
<Label
Grid.Row="2"
Text="{u:I18n AccountLoggedOut}"
@@ -102,8 +97,7 @@
StyleClass="accountlist-sub, accountlist-sub-platform"
FontAttributes="Italic"
TextTransform="Lowercase"
LineBreakMode="TailTruncation"
AutomationId="AccountStatusLabel" />
LineBreakMode="TailTruncation" />
</Grid>
<controls:IconLabel
@@ -113,8 +107,7 @@
Margin="12,0"
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
AutomationId="InactiveVaultIcon" />
StyleClass="list-icon, list-icon-platform" />
<controls:IconLabel
Grid.Column="2"
Text="{Binding AuthStatusIconActive}"
@@ -123,8 +116,7 @@
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
TextColor="{DynamicResource TextColor}"
AutomationId="ActiveVaultIcon" />
TextColor="{DynamicResource TextColor}"/>
</Grid>
<Grid
@@ -155,8 +147,7 @@
StyleClass="accountlist-title, accountlist-title-platform"
LineBreakMode="TailTruncation"
VerticalOptions="Center"
Grid.Column="1"
AutomationId="AddAccountButton" />
Grid.Column="1" />
</Grid>
</Grid>
</ViewCell>

View File

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

View File

@@ -1,5 +1,4 @@
using System;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
@@ -19,7 +18,7 @@ namespace Bit.App.Controls
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(CipherViewCell));
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
nameof(ButtonCommand), typeof(ICommand), typeof(CipherViewCell));
nameof(ButtonCommand), typeof(Command<CipherView>), typeof(CipherViewCell));
public CipherViewCell()
{
@@ -43,9 +42,9 @@ namespace Bit.App.Controls
set => SetValue(CipherProperty, value);
}
public ICommand ButtonCommand
public Command<CipherView> ButtonCommand
{
get => GetValue(ButtonCommandProperty) as ICommand;
get => GetValue(ButtonCommandProperty) as Command<CipherView>;
set => SetValue(ButtonCommandProperty, value);
}

View File

@@ -31,7 +31,7 @@ namespace Bit.App.Controls
public bool ShowIconImage
{
get => WebsiteIconsEnabled
&& !string.IsNullOrWhiteSpace(Cipher.LaunchUri)
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
&& IconImageSource != null;
}
@@ -41,7 +41,7 @@ namespace Bit.App.Controls
{
if (_iconImageSource == string.Empty) // default value since icon source can return null
{
_iconImageSource = IconImageHelper.GetIconImage(Cipher);
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
}
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,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"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.SendViewCell"
@@ -54,16 +54,14 @@
Grid.Column="0"
Grid.Row="0"
StyleClass="list-title, list-title-platform"
Text="{Binding Send.Name}"
AutomationId="SendNameLabel" />
Text="{Binding Send.Name}" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="0"
Grid.Row="1"
Grid.ColumnSpan="6"
StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Send.DisplayDate}"
AutomationId="SendDateLabel" />
Text="{Binding Send.DisplayDate}" />
<controls:IconLabel
Grid.Column="1"
Grid.Row="0"
@@ -74,8 +72,7 @@
Text="{Binding Source={x:Static core:BitwardenIcons.ExclamationTriangle}}"
IsVisible="{Binding Send.Disabled, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Disabled}"
AutomationId="DisabledSendLabel" />
AutomationProperties.Name="{u:I18n Disabled}" />
<controls:IconLabel
Grid.Column="2"
Grid.Row="0"
@@ -86,8 +83,7 @@
Text="{Binding Source={x:Static core:BitwardenIcons.Key}}"
IsVisible="{Binding Send.HasPassword, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Password}"
AutomationId="PasswordProtectedSendLabel" />
AutomationProperties.Name="{u:I18n Password}" />
<controls:IconLabel
Grid.Column="3"
Grid.Row="0"
@@ -98,8 +94,7 @@
Text="{Binding Source={x:Static core:BitwardenIcons.Ban}}"
IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n MaxAccessCountReached}"
AutomationId="SendMaxAccessCountReachedLabel" />
AutomationProperties.Name="{u:I18n MaxAccessCountReached}" />
<controls:IconLabel
Grid.Column="4"
Grid.Row="0"
@@ -110,8 +105,7 @@
Text="{Binding Source={x:Static core:BitwardenIcons.Clock}}"
IsVisible="{Binding Send.Expired, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Expired}"
AutomationId="ExpiredSendLabel" />
AutomationProperties.Name="{u:I18n Expired}" />
<controls:IconLabel
Grid.Column="5"
Grid.Row="0"
@@ -122,8 +116,7 @@
Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
IsVisible="{Binding Send.PendingDelete, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n PendingDelete}"
AutomationId="SendWithPendingDeletionLabel" />
AutomationProperties.Name="{u:I18n PendingDelete}" />
</Grid>
<controls:MiButton
@@ -136,7 +129,6 @@
VerticalOptions="CenterAndExpand"
HorizontalOptions="EndAndExpand"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}"
AutomationId="SendOptionsButton" />
AutomationProperties.Name="{u:I18n Options}" />
</controls:ExtendedGrid>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,10 @@
using System.Text;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Models.Domain;
@@ -70,13 +73,13 @@ namespace Bit.App.Pages
set => SetProperty(ref _policy, value);
}
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string ShowPasswordIcon => ShowPassword ? "" : "";
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string MasterPassword { get; set; }
public string ConfirmMasterPassword { get; set; }
public string Hint { get; set; }
public virtual async Task InitAsync(bool forceSync = false)
public async Task InitAsync(bool forceSync = false)
{
if (forceSync)
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,27 +27,25 @@ namespace Bit.App.Pages
private readonly IEnvironmentService _environmentService;
private readonly IStateService _stateService;
private readonly IBiometricService _biometricService;
private readonly IUserVerificationService _userVerificationService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly ILogger _logger;
private readonly IWatchDeviceService _watchDeviceService;
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
private readonly IPolicyService _policyService;
private readonly IPasswordGenerationService _passwordGenerationService;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
private readonly ISyncService _syncService;
private string _email;
private string _masterPassword;
private string _pin;
private bool _showPassword;
private PinLockType _pinStatus;
private bool _pinEnabled;
private bool _biometricEnabled;
private bool _pinLock;
private bool _biometricLock;
private bool _biometricIntegrityValid = true;
private bool _biometricButtonVisible;
private bool _hasMasterPassword;
private bool _usingKeyConnector;
private string _biometricButtonText;
private string _loggedInAsText;
private string _lockedVerifyText;
private bool _isPinProtected;
private bool _isPinProtectedWithKey;
public LockPageViewModel()
{
@@ -60,13 +58,9 @@ namespace Bit.App.Pages
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
_policyService = ServiceContainer.Resolve<IPolicyService>();
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
_syncService = ServiceContainer.Resolve<ISyncService>();
PageTitle = AppResources.VerifyMasterPassword;
TogglePasswordCommand = new Command(TogglePassword);
@@ -102,21 +96,21 @@ namespace Bit.App.Pages
});
}
public bool PinEnabled
public bool PinLock
{
get => _pinEnabled;
set => SetProperty(ref _pinEnabled, value);
get => _pinLock;
set => SetProperty(ref _pinLock, value);
}
public bool HasMasterPassword
public bool UsingKeyConnector
{
get => _hasMasterPassword;
get => _usingKeyConnector;
}
public bool BiometricEnabled
public bool BiometricLock
{
get => _biometricEnabled;
set => SetProperty(ref _biometricEnabled, value);
get => _biometricLock;
set => SetProperty(ref _biometricLock, value);
}
public bool BiometricIntegrityValid
@@ -149,8 +143,6 @@ namespace Bit.App.Pages
set => SetProperty(ref _lockedVerifyText, value);
}
public bool CheckPendingAuthRequests { get; set; }
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public Command SubmitCommand { get; }
@@ -166,32 +158,18 @@ namespace Bit.App.Pages
public async Task InitAsync()
{
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
if (pendingRequest != null && CheckPendingAuthRequests)
(_isPinProtected, _isPinProtectedWithKey) = await _vaultTimeoutService.IsPinLockSetAsync();
PinLock = (_isPinProtected && await _stateService.GetPinProtectedKeyAsync() != null) ||
_isPinProtectedWithKey;
BiometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _cryptoService.HasKeyAsync();
// Users with key connector and without biometric or pin has no MP to unlock with
_usingKeyConnector = await _keyConnectorService.GetUsesKeyConnector();
if (_usingKeyConnector && !(BiometricLock || PinLock))
{
await _vaultTimeoutService.LogOutAsync();
return;
}
_pinStatus = await _vaultTimeoutService.GetPinLockTypeAsync();
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
?? await _stateService.GetPinProtectedKeyAsync();
PinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) ||
_pinStatus == PinLockType.Persistent;
BiometricEnabled = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _biometricService.CanUseBiometricsUnlockAsync();
// Users without MP and without biometric or pin has no MP to unlock with
_hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync();
if (await _stateService.IsAuthenticatedAsync()
&& !_hasMasterPassword
&& !BiometricEnabled
&& !PinEnabled)
{
await _vaultTimeoutService.LogOutAsync();
return;
}
_email = await _stateService.GetEmailAsync();
if (string.IsNullOrWhiteSpace(_email))
{
@@ -206,20 +184,28 @@ namespace Bit.App.Pages
}
var webVaultHostname = CoreHelpers.GetHostname(webVault);
LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, webVaultHostname);
if (PinEnabled)
if (PinLock)
{
PageTitle = AppResources.VerifyPIN;
LockedVerifyText = AppResources.VaultLockedPIN;
}
else
{
PageTitle = _hasMasterPassword ? AppResources.VerifyMasterPassword : AppResources.UnlockVault;
LockedVerifyText = _hasMasterPassword ? AppResources.VaultLockedMasterPassword : AppResources.VaultLockedIdentity;
if (_usingKeyConnector)
{
PageTitle = AppResources.UnlockVault;
LockedVerifyText = AppResources.VaultLockedIdentity;
}
else
{
PageTitle = AppResources.VerifyMasterPassword;
LockedVerifyText = AppResources.VaultLockedMasterPassword;
}
}
if (BiometricEnabled)
if (BiometricLock)
{
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
if (!_biometricIntegrityValid)
{
BiometricButtonVisible = false;
@@ -239,14 +225,14 @@ namespace Bit.App.Pages
public async Task SubmitAsync()
{
if (PinEnabled && string.IsNullOrWhiteSpace(Pin))
if (PinLock && string.IsNullOrWhiteSpace(Pin))
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
string.Format(AppResources.ValidationFieldRequired, AppResources.PIN),
AppResources.Ok);
return;
}
if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword))
if (!PinLock && string.IsNullOrWhiteSpace(MasterPassword))
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
@@ -257,54 +243,34 @@ namespace Bit.App.Pages
ShowPassword = false;
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
if (PinEnabled)
if (PinLock)
{
var failed = true;
try
{
EncString userKeyPin = null;
EncString oldPinProtected = null;
if (_pinStatus == PinLockType.Persistent)
if (_isPinProtected)
{
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
}
else if (_pinStatus == PinLockType.Transient)
{
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
}
UserKey userKey;
if (oldPinProtected != null)
{
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
_pinStatus == PinLockType.Transient,
Pin,
_email,
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email,
kdfConfig,
oldPinProtected
);
await _stateService.GetPinProtectedKeyAsync());
var encKey = await _cryptoService.GetEncKeyAsync(key);
var protectedPin = await _stateService.GetProtectedPinAsync();
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
failed = decPin != Pin;
if (!failed)
{
Pin = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await SetKeyAndContinueAsync(key);
}
}
else
{
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
Pin,
_email,
kdfConfig,
userKeyPin
);
}
var protectedPin = await _stateService.GetProtectedPinAsync();
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
failed = decryptedPin != Pin;
if (!failed)
{
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email, kdfConfig);
failed = false;
Pin = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await SetUserKeyAndContinueAsync(userKey);
await SetKeyAndContinueAsync(key);
}
}
catch
@@ -325,31 +291,26 @@ namespace Bit.App.Pages
}
else
{
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig);
var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync();
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdfConfig);
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
var passwordValid = false;
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
if (storedKeyHash != null)
{
// Offline unlock possible
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey);
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, key);
}
else
{
// Online unlock required
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.ServerAuthorization);
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
var request = new PasswordVerificationRequest();
request.MasterPasswordHash = keyHash;
try
{
var response = await _apiService.PostAccountVerifyPasswordAsync(request);
enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
await _apiService.PostAccountVerifyPasswordAsync(request);
passwordValid = true;
var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.LocalAuthorization);
await _cryptoService.SetMasterKeyHashAsync(localKeyHash);
var localKeyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
await _cryptoService.SetKeyHashAsync(localKeyHash);
}
catch (Exception e)
{
@@ -359,22 +320,20 @@ namespace Bit.App.Pages
}
if (passwordValid)
{
if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions))
if (_isPinProtected)
{
// Save the ForcePasswordResetReason to force a password reset after unlock
await _stateService.SetForcePasswordResetReasonAsync(
ForcePasswordResetReason.WeakMasterPasswordOnLogin);
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));
}
MasterPassword = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
await _cryptoService.SetMasterKeyAsync(masterKey);
await SetUserKeyAndContinueAsync(userKey);
await SetKeyAndContinueAsync(key);
// Re-enable biometrics
if (BiometricEnabled & !BiometricIntegrityValid)
if (BiometricLock & !BiometricIntegrityValid)
{
await _biometricService.SetupBiometricAsync();
}
@@ -393,37 +352,6 @@ namespace Bit.App.Pages
}
}
/// <summary>
/// Checks if the master password requires updating to meet the enforced policy requirements
/// </summary>
/// <param name="options"></param>
private async Task<bool> RequirePasswordChangeAsync(MasterPasswordPolicyOptions options = null)
{
// If no policy options are provided, attempt to load them from the policy service
var enforcedOptions = options ?? await _policyService.GetMasterPasswordPolicyOptions();
// No policy to enforce on login/unlock
if (!(enforcedOptions is { EnforceOnLogin: true }))
{
return false;
}
var strength = _passwordGenerationService.PasswordStrength(
MasterPassword, _passwordGenerationService.GetPasswordStrengthUserInput(_email))?.Score;
if (!strength.HasValue)
{
_logger.Error("Unable to evaluate master password strength during unlock");
return false;
}
return !await _policyService.EvaluateMasterPassword(
strength.Value,
MasterPassword,
enforcedOptions
);
}
public async Task LogOutAsync()
{
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation,
@@ -451,43 +379,39 @@ namespace Bit.App.Pages
public void TogglePassword()
{
ShowPassword = !ShowPassword;
var secret = PinEnabled ? Pin : MasterPassword;
var secret = PinLock ? Pin : MasterPassword;
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
}
public async Task PromptBiometricAsync()
{
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
BiometricButtonVisible = BiometricIntegrityValid;
if (!BiometricEnabled || !BiometricIntegrityValid)
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
if (!BiometricLock || !BiometricIntegrityValid)
{
return;
}
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
PinLock ? AppResources.PIN : AppResources.MasterPassword,
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
await _stateService.SetBiometricLockedAsync(!success);
if (success)
{
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
await SetUserKeyAndContinueAsync(userKey);
await DoContinueAsync();
}
}
private async Task SetUserKeyAndContinueAsync(UserKey key)
private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key)
{
var hasKey = await _cryptoService.HasUserKeyAsync();
var hasKey = await _cryptoService.HasKeyAsync();
if (!hasKey)
{
await _cryptoService.SetUserKeyAsync(key);
await _cryptoService.SetKeyAsync(key);
}
await _deviceTrustCryptoService.TrustDeviceIfNeededAsync();
await DoContinueAsync();
}
private async Task DoContinueAsync()
{
_syncService.FullSyncAsync(false).FireAndForget();
await _stateService.SetBiometricLockedAsync(false);
_watchDeviceService.SyncDataToWatchAsync().FireAndForget();
_messagingService.Send("unlocked");

View File

@@ -1,76 +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}"
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}"/>
<Button
x:Name="_approveWithMyOtherDevice"
Text="{u:I18n ApproveWithMyOtherDevice}"
StyleClass="btn-primary"
Command="{Binding ApproveWithMyOtherDeviceCommand}"
IsVisible="{Binding ApproveWithMyOtherDeviceEnabled}"/>
<Button
x:Name="_requestAdminApproval"
Text="{u:I18n RequestAdminApproval}"
StyleClass="box-button-row"
Command="{Binding RequestAdminApprovalCommand}"
IsVisible="{Binding RequestAdminApprovalEnabled}"/>
<Button
x:Name="_approveWithMasterPassword"
Text="{u:I18n ApproveWithMasterPassword}"
StyleClass="box-button-row"
Command="{Binding ApproveWithMasterPasswordCommand}"
IsVisible="{Binding ApproveWithMasterPasswordEnabled}"/>
<Label
Text="{Binding LoggingInAsText}"
StyleClass="text-sm"
Margin="0,40,0,0"
AutomationId="LoggingInAsLabel"
/>
<Label
Text="{u:I18n NotYou}"
StyleClass="text-md"
HorizontalOptions="Start"
TextColor="{DynamicResource HyperlinkColor}"
AutomationId="NotYouLabel">
<Label.GestureRecognizers>
<TapGestureRecognizer 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);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task RequestAdminApprovalAsync()
{
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AdminApproval, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
}

View File

@@ -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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -13,7 +12,6 @@ using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Response;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
@@ -34,9 +32,6 @@ namespace Bit.App.Pages
private IPlatformUtilsService _platformUtilsService;
private IEnvironmentService _environmentService;
private ILogger _logger;
private IDeviceTrustCryptoService _deviceTrustCryptoService;
private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly ICryptoService _cryptoService;
protected override II18nService i18nService => _i18nService;
protected override IEnvironmentService environmentService => _environmentService;
@@ -49,7 +44,6 @@ namespace Bit.App.Pages
private string _email;
private string _requestId;
private string _requestAccessCode;
private AuthRequestType _authRequestType;
// Item1 publicKey, Item2 privateKey
private Tuple<byte[], byte[]> _requestKeyPair;
@@ -63,9 +57,8 @@ namespace Bit.App.Pages
_i18nService = ServiceContainer.Resolve<II18nService>();
_stateService = ServiceContainer.Resolve<IStateService>();
_logger = ServiceContainer.Resolve<ILogger>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>();
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
PageTitle = AppResources.LogInWithAnotherDevice;
CreatePasswordlessLoginCommand = new AsyncCommand(CreatePasswordlessLoginAsync,
onException: ex => HandleException(ex),
@@ -84,86 +77,6 @@ namespace Bit.App.Pages
public ICommand CreatePasswordlessLoginCommand { get; }
public ICommand CloseCommand { get; }
public string HeaderTitle
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.LogInWithDevice;
case AuthRequestType.AdminApproval:
return AppResources.LogInInitiated;
default:
return string.Empty;
};
}
}
public string Title
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.LogInInitiated;
case AuthRequestType.AdminApproval:
return AppResources.AdminApprovalRequested;
default:
return string.Empty;
};
}
}
public string SubTitle
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.ANotificationHasBeenSentToYourDevice;
case AuthRequestType.AdminApproval:
return AppResources.YourRequestHasBeenSentToYourAdmin;
default:
return string.Empty;
};
}
}
public string Description
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice;
case AuthRequestType.AdminApproval:
return AppResources.YouWillBeNotifiedOnceApproved;
default:
return string.Empty;
};
}
}
public string OtherOptions
{
get
{
switch (_authRequestType)
{
case AuthRequestType.AuthenticateAndUnlock:
return AppResources.LogInWithDeviceMustBeSetUpInTheSettingsOfTheBitwardenAppNeedAnotherOption;
case AuthRequestType.AdminApproval:
return AppResources.TroubleLoggingIn;
default:
return string.Empty;
};
}
}
public string FingerprintPhrase
{
get => _fingerprintPhrase;
@@ -176,25 +89,6 @@ namespace Bit.App.Pages
set => SetProperty(ref _email, value);
}
public AuthRequestType AuthRequestType
{
get => _authRequestType;
set
{
SetProperty(ref _authRequestType, value, additionalPropertyNames: new string[]
{
nameof(Title),
nameof(SubTitle),
nameof(Description),
nameof(OtherOptions),
nameof(ResendNotificationVisible)
});
PageTitle = HeaderTitle;
}
}
public bool ResendNotificationVisible => AuthRequestType == AuthRequestType.AuthenticateAndUnlock;
public void StartCheckLoginRequestStatus()
{
try
@@ -225,22 +119,14 @@ namespace Bit.App.Pages
private async Task CheckLoginRequestStatus()
{
if (string.IsNullOrEmpty(_requestId))
if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_requestAccessCode))
{
return;
}
try
{
PasswordlessLoginResponse response = null;
if (await _stateService.IsAuthenticatedAsync())
{
response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId);
}
else
{
response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode);
}
var response = await _authService.GetPasswordlessLoginResponseAsync(_requestId, _requestAccessCode);
if (response.RequestApproved == null || !response.RequestApproved.Value)
{
@@ -252,12 +138,6 @@ namespace Bit.App.Pages
var authResult = await _authService.LogInPasswordlessAsync(Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
if (authResult == null && await _stateService.IsAuthenticatedAsync())
{
await HandleLoginCompleteAsync();
return;
}
if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus))
{
return;
@@ -273,7 +153,8 @@ namespace Bit.App.Pages
}
else
{
await HandleLoginCompleteAsync();
_syncService.FullSyncAsync(true).FireAndForget();
LogInSuccessAction?.Invoke();
}
}
catch (Exception ex)
@@ -283,67 +164,22 @@ namespace Bit.App.Pages
}
}
private async Task HandleLoginCompleteAsync()
{
await _stateService.SetPendingAdminAuthRequestAsync(null);
_syncService.FullSyncAsync(true).FireAndForget();
LogInSuccessAction?.Invoke();
}
private async Task CreatePasswordlessLoginAsync()
{
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
PasswordlessLoginResponse response = null;
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
if (pendingRequest != null && _authRequestType == AuthRequestType.AdminApproval)
var response = await _authService.PasswordlessCreateLoginRequestAsync(_email);
if (response != null)
{
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);
}
FingerprintPhrase = response.FingerprintPhrase;
_requestId = response.Id;
_requestAccessCode = response.RequestAccessCode;
_requestKeyPair = response.RequestKeyPair;
}
if (response == null)
{
response = await _authService.PasswordlessCreateLoginRequestAsync(_email, AuthRequestType);
}
await HandlePasswordlessLoginAsync(response, pendingRequest == null && _authRequestType == AuthRequestType.AdminApproval);
await _deviceActionService.HideLoadingAsync();
}
private async Task HandlePasswordlessLoginAsync(PasswordlessLoginResponse response, bool createPendingAdminRequest)
{
if (response == null)
{
throw new ArgumentNullException(nameof(response));
}
if (createPendingAdminRequest)
{
var pendingAuthRequest = new PendingAdminAuthRequest { Id = response.Id, PrivateKey = response.RequestKeyPair.Item2 };
await _stateService.SetPendingAdminAuthRequestAsync(pendingAuthRequest);
}
FingerprintPhrase = response.FingerprintPhrase;
_requestId = response.Id;
_requestAccessCode = response.RequestAccessCode;
_requestKeyPair = response.RequestKeyPair;
}
private void HandleException(Exception ex)
{
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>

View File

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

View File

@@ -9,7 +9,6 @@ using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
@@ -30,8 +29,6 @@ namespace Bit.App.Pages
private readonly IStateService _stateService;
private readonly ILogger _logger;
private readonly IOrganizationService _organizationService;
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
private readonly ICryptoService _cryptoService;
private string _orgIdentifier;
@@ -48,8 +45,7 @@ namespace Bit.App.Pages
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
_organizationService = ServiceContainer.Resolve<IOrganizationService>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
PageTitle = AppResources.Bitwarden;
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
@@ -65,7 +61,6 @@ namespace Bit.App.Pages
public Action StartTwoFactorAction { get; set; }
public Action StartSetPasswordAction { get; set; }
public Action SsoAuthSuccessAction { get; set; }
public Action StartDeviceApprovalOptionsAction { get; set; }
public Action CloseAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
@@ -126,8 +121,9 @@ namespace Bit.App.Pages
var ssoToken = response.Token;
var passwordOptions = PasswordGenerationOptions.CreateDefault
.WithLength(64);
var passwordOptions = new PasswordGenerationOptions(true);
passwordOptions.Length = 64;
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
@@ -149,6 +145,7 @@ namespace Bit.App.Pages
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
new Uri(REDIRECT_URI));
var code = GetResultCode(authResult, state);
if (!string.IsNullOrEmpty(code))
{
@@ -201,89 +198,28 @@ namespace Bit.App.Pages
try
{
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
await _deviceActionService.HideLoadingAsync();
if (response.TwoFactor)
{
StartTwoFactorAction?.Invoke();
return;
}
if (decryptOptions?.TrustedDeviceOption != null)
{
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (!decryptOptions.HasMasterPassword &&
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
{
StartSetPasswordAction?.Invoke();
}
else if (response.ForcePasswordReset)
{
UpdateTempPasswordAction?.Invoke();
}
else if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
{
if (decryptOptions.TrustedDeviceOption.EncryptedPrivateKey == null && decryptOptions.TrustedDeviceOption.EncryptedUserKey == null)
{
await _deviceTrustCryptoService.RemoveTrustedDeviceAsync();
StartDeviceApprovalOptionsAction?.Invoke();
}
else
{
_syncService.FullSyncAsync(true).FireAndForget();
SsoAuthSuccessAction?.Invoke();
}
}
else if (pendingRequest != null)
{
var authRequest = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
if (authRequest != null && authRequest.RequestApproved != null && authRequest.RequestApproved.Value)
{
var authResult = await _authService.LogInPasswordlessAsync(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
{
await _stateService.SetPendingAdminAuthRequestAsync(null);
StartDeviceApprovalOptionsAction?.Invoke();
}
}
else
{
StartDeviceApprovalOptionsAction?.Invoke();
}
return;
}
// In the standard, non TDE case, a user must set password if they don't
// have one and they aren't using key connector.
// Note: TDE & Key connector are mutually exclusive org config options.
if (response.ResetMasterPassword || (decryptOptions?.RequireSetPassword ?? false))
else if (response.ResetMasterPassword)
{
StartSetPasswordAction?.Invoke();
return;
}
if (response.ForcePasswordReset)
else if (response.ForcePasswordReset)
{
UpdateTempPasswordAction?.Invoke();
return;
}
_syncService.FullSyncAsync(true).FireAndForget();
SsoAuthSuccessAction?.Invoke();
else
{
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
SsoAuthSuccessAction?.Invoke();
}
}
catch (Exception)
catch (Exception e)
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ using Bit.App.Controls;
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -25,23 +24,19 @@ namespace Bit.App.Pages
{
InitializeComponent();
SetActivityIndicator();
_authingWithSso = authingWithSso ?? false;
_appOptions = appOptions;
_orgIdentifier = orgIdentifier;
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_vm = BindingContext as TwoFactorPageViewModel;
_vm.Page = this;
_vm.AuthingWithSso = authingWithSso ?? false;
_vm.StartSetPasswordAction = () =>
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
_vm.TwoFactorAuthSuccessAction = () =>
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessToMainAsync());
_vm.LockAction = () =>
Device.BeginInvokeOnMainThread(TwoFactorAuthSuccessWithSSOLocked);
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync());
_vm.UpdateTempPasswordAction =
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
_vm.StartDeviceApprovalOptionsAction =
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
_vm.CloseAction = async () => await Navigation.PopModalAsync();
DuoWebView = _duoWebView;
if (Device.RuntimePlatform == Device.Android)
@@ -185,25 +180,21 @@ namespace Bit.App.Pages
await Navigation.PushModalAsync(new NavigationPage(page));
}
private async Task StartDeviceApprovalOptionsAsync()
private async Task TwoFactorAuthSuccessAsync()
{
var page = new LoginApproveDevicePage();
await Navigation.PushModalAsync(new NavigationPage(page));
}
private void TwoFactorAuthSuccessWithSSOLocked()
{
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
}
private async Task TwoFactorAuthSuccessToMainAsync()
{
if (AppHelpers.SetAlternateMainPage(_appOptions))
if (_authingWithSso)
{
return;
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
}
else
{
if (AppHelpers.SetAlternateMainPage(_appOptions))
{
return;
}
var previousPage = await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
var previousPage = await AppHelpers.ClearPreviousPage();
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
}
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.Exceptions;
using Bit.Core.Models.Request;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Newtonsoft.Json;
using Xamarin.CommunityToolkit.ObjectModel;
@@ -33,9 +32,8 @@ namespace Bit.App.Pages
private readonly IStateService _stateService;
private readonly II18nService _i18nService;
private readonly IAppIdService _appIdService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ILogger _logger;
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
private TwoFactorProviderType? _selectedProviderType;
private string _totpInstruction;
private string _webVaultUrl = "https://vault.bitwarden.com";
@@ -56,9 +54,7 @@ namespace Bit.App.Pages
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
_logger = ServiceContainer.Resolve<ILogger>();
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
PageTitle = AppResources.TwoStepLogin;
SubmitCommand = new Command(async () => await SubmitAsync());
@@ -73,8 +69,6 @@ namespace Bit.App.Pages
public bool Remember { get; set; }
public bool AuthingWithSso { get; set; }
public string Token { get; set; }
public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo ||
@@ -124,8 +118,6 @@ namespace Bit.App.Pages
public Command SubmitCommand { get; }
public ICommand MoreCommand { get; }
public Action TwoFactorAuthSuccessAction { get; set; }
public Action LockAction { get; set; }
public Action StartDeviceApprovalOptionsAction { get; set; }
public Action StartSetPasswordAction { get; set; }
public Action CloseAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
@@ -323,7 +315,6 @@ namespace Bit.App.Pages
var task = Task.Run(() => _syncService.FullSyncAsync(true));
await _deviceActionService.HideLoadingAsync();
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
_messagingService.Send("listenYubiKeyOTP", false);
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
@@ -335,38 +326,9 @@ namespace Bit.App.Pages
{
UpdateTempPasswordAction?.Invoke();
}
else if (decryptOptions?.TrustedDeviceOption != null)
{
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (!decryptOptions.HasMasterPassword &&
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
{
StartSetPasswordAction?.Invoke();
}
else if (result.ForcePasswordReset)
{
UpdateTempPasswordAction?.Invoke();
}
else if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
{
if (decryptOptions.TrustedDeviceOption.EncryptedPrivateKey == null && decryptOptions.TrustedDeviceOption.EncryptedUserKey == null)
{
await _deviceTrustCryptoService.RemoveTrustedDeviceAsync();
StartDeviceApprovalOptionsAction?.Invoke();
}
else
{
await TwoFactorAuthSuccessAsync();
}
}
else
{
StartDeviceApprovalOptionsAction?.Invoke();
}
}
else
{
await TwoFactorAuthSuccessAsync();
TwoFactorAuthSuccessAction?.Invoke();
}
}
catch (ApiException e)
@@ -460,17 +422,5 @@ namespace Bit.App.Pages
return false;
}
}
public async Task TwoFactorAuthSuccessAsync()
{
if (AuthingWithSso && await _vaultTimeoutService.IsLockedAsync())
{
LockAction?.Invoke();
}
else
{
TwoFactorAuthSuccessAction?.Invoke();
}
}
}
}

View File

@@ -46,7 +46,7 @@
BackgroundColor="Transparent"
BorderColor="{DynamicResource PrimaryColor}">
<Label
Text="{Binding UpdateMasterPasswordWarningText }"
Text="{u:I18n UpdateMasterPasswordWarning}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
@@ -74,40 +74,6 @@
HorizontalTextAlignment="Start" />
</Frame>
</Grid>
<Grid StyleClass="box-row" IsVisible="{Binding RequireCurrentPassword }">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n CurrentMasterPassword}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<controls:MonoEntry
x:Name="_currentMasterPassword"
Text="{Binding CurrentMasterPassword}"
StyleClass="box-value"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
Grid.Row="1"
Grid.Column="0" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />

View File

@@ -1,68 +1,27 @@
using System;
using System.Threading.Tasks;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class UpdateTempPasswordPageViewModel : BaseChangePasswordViewModel
{
private readonly IUserVerificationService _userVerificationService;
private ForcePasswordResetReason _reason = ForcePasswordResetReason.AdminForcePasswordReset;
public UpdateTempPasswordPageViewModel()
{
PageTitle = AppResources.UpdateMasterPassword;
TogglePasswordCommand = new Command(TogglePassword);
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
SubmitCommand = new AsyncCommand(SubmitAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
SubmitCommand = new Command(async () => await SubmitAsync());
}
public AsyncCommand SubmitCommand { get; }
public Command SubmitCommand { get; }
public Command TogglePasswordCommand { get; }
public Command ToggleConfirmPasswordCommand { get; }
public Action UpdateTempPasswordSuccessAction { get; set; }
public Action LogOutAction { get; set; }
public string CurrentMasterPassword { get; set; }
public override async Task InitAsync(bool forceSync = false)
{
await base.InitAsync(forceSync);
var forcePasswordResetReason = await _stateService.GetForcePasswordResetReasonAsync();
if (forcePasswordResetReason.HasValue)
{
_reason = forcePasswordResetReason.Value;
}
}
public bool RequireCurrentPassword
{
get => _reason == ForcePasswordResetReason.WeakMasterPasswordOnLogin;
}
public string UpdateMasterPasswordWarningText
{
get
{
return _reason == ForcePasswordResetReason.WeakMasterPasswordOnLogin
? AppResources.UpdateWeakMasterPasswordWarning
: AppResources.UpdateMasterPasswordWarning;
}
}
public void TogglePassword()
{
@@ -83,46 +42,32 @@ namespace Bit.App.Pages
return;
}
if (RequireCurrentPassword &&
!await _userVerificationService.VerifyUser(CurrentMasterPassword, VerificationType.MasterPassword))
{
return;
}
// Retrieve details for key generation
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
var email = await _stateService.GetEmailAsync();
// Create new master key and hash new password
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, email, kdfConfig);
var masterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey);
// Create new key and hash new password
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig);
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
// Encrypt user key with new master key
var (userKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(masterKey);
// Create new encKey for the User
var newEncKey = await _cryptoService.RemakeEncKeyAsync(key);
// Create request
var request = new UpdateTempPasswordRequest
{
Key = newEncKey.Item2.EncryptedString,
NewMasterPasswordHash = masterPasswordHash,
MasterPasswordHint = Hint
};
// Initiate API action
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.UpdatingPassword);
switch (_reason)
{
case ForcePasswordResetReason.AdminForcePasswordReset:
await UpdateTempPasswordAsync(masterPasswordHash, newProtectedUserKey.EncryptedString);
break;
case ForcePasswordResetReason.WeakMasterPasswordOnLogin:
await UpdatePasswordAsync(masterPasswordHash, newProtectedUserKey.EncryptedString);
break;
default:
throw new ArgumentOutOfRangeException();
}
await _apiService.PutUpdateTempPasswordAsync(request);
await _deviceActionService.HideLoadingAsync();
// Clear the force reset password reason
await _stateService.SetForcePasswordResetReasonAsync(null);
_platformUtilsService.ShowToast(null, null, AppResources.UpdatedMasterPassword);
UpdateTempPasswordSuccessAction?.Invoke();
}
catch (ApiException e)
@@ -140,32 +85,5 @@ namespace Bit.App.Pages
}
}
}
private async Task UpdateTempPasswordAsync(string newMasterPasswordHash, string newEncKey)
{
var request = new UpdateTempPasswordRequest
{
Key = newEncKey,
NewMasterPasswordHash = newMasterPasswordHash,
MasterPasswordHint = Hint
};
await _apiService.PutUpdateTempPasswordAsync(request);
}
private async Task UpdatePasswordAsync(string newMasterPasswordHash, string newEncKey)
{
var currentPasswordHash = await _cryptoService.HashMasterKeyAsync(CurrentMasterPassword, null);
var request = new PasswordRequest
{
MasterPasswordHash = currentPasswordHash,
Key = newEncKey,
NewMasterPasswordHash = newMasterPasswordHash,
MasterPasswordHint = Hint
};
await _apiService.PostPasswordAsync(request);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,27 +66,5 @@ namespace Bit.App.Pages
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"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center"
AutomationId="NoSendDisplayedLabel" />
HorizontalTextAlignment="Center" />
<controls:ExtendedCollectionView
IsVisible="{Binding ShowList}"
ItemsSource="{Binding Sends}"
@@ -68,15 +67,13 @@
SelectionMode="Single"
SelectionChanged="RowSelected"
StyleClass="list, list-platform"
ExtraDataForLogging="Sends Page"
AutomationId="SendCellList">
ExtraDataForLogging="Sends Page">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="views:SendView">
<controls:SendViewCell
Send="{Binding .}"
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}"
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}"
AutomationId="SendCell" />
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</controls:ExtendedCollectionView>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,10 @@ using Bit.App.Pages.Accounts;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Response;
using Bit.Core.Models.View;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
@@ -29,7 +32,7 @@ namespace Bit.App.Pages
private readonly IBiometricService _biometricService;
private readonly IPolicyService _policyService;
private readonly ILocalizeService _localizeService;
private readonly IUserVerificationService _userVerificationService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly IClipboardService _clipboardService;
private readonly ILogger _loggerService;
private readonly IPushNotificationService _pushNotificationService;
@@ -48,8 +51,7 @@ namespace Bit.App.Pages
private bool _reportLoggingEnabled;
private bool _approvePasswordlessLoginRequests;
private bool _shouldConnectToWatch;
private bool _hasMasterPassword;
private readonly static List<KeyValuePair<string, int?>> VaultTimeoutOptions =
private List<KeyValuePair<string, int?>> _vaultTimeouts =
new List<KeyValuePair<string, int?>>
{
new KeyValuePair<string, int?>(AppResources.Immediately, 0),
@@ -63,7 +65,7 @@ namespace Bit.App.Pages
new KeyValuePair<string, int?>(AppResources.Never, null),
new KeyValuePair<string, int?>(AppResources.Custom, CustomVaultTimeoutValue),
};
private readonly static List<KeyValuePair<string, VaultTimeoutAction>> VaultTimeoutActionOptions =
private List<KeyValuePair<string, VaultTimeoutAction>> _vaultTimeoutActions =
new List<KeyValuePair<string, VaultTimeoutAction>>
{
new KeyValuePair<string, VaultTimeoutAction>(AppResources.Lock, VaultTimeoutAction.Lock),
@@ -72,8 +74,6 @@ namespace Bit.App.Pages
private Policy _vaultTimeoutPolicy;
private int? _vaultTimeout;
private List<KeyValuePair<string, int?>> _vaultTimeoutOptions = VaultTimeoutOptions;
private List<KeyValuePair<string, VaultTimeoutAction>> _vaultTimeoutActionOptions = VaultTimeoutActionOptions;
public SettingsPageViewModel()
{
@@ -89,7 +89,7 @@ namespace Bit.App.Pages
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
@@ -101,17 +101,12 @@ namespace Bit.App.Pages
ExecuteSettingItemCommand = new AsyncCommand<SettingsPageListItem>(item => item.ExecuteAsync(), onException: _loggerService.Exception, allowsMultipleExecutions: false);
}
private bool IsVaultTimeoutActionLockAllowed => _hasMasterPassword || _biometric || _pin;
public ObservableRangeCollection<ISettingsPageListItem> GroupedItems { get; set; }
public IAsyncCommand<SettingsPageListItem> ExecuteSettingItemCommand { get; }
public async Task InitAsync()
{
var decryptionOptions = await _stateService.GetAccountDecryptionOptions();
// set has true for backwards compatibility
_hasMasterPassword = decryptionOptions?.HasMasterPassword ?? true;
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
var lastSync = await _syncService.GetLastSyncAsync();
if (lastSync != null)
@@ -122,36 +117,23 @@ namespace Bit.App.Pages
_localizeService.GetLocaleShortTime(lastSync.Value));
}
_vaultTimeoutPolicy = null;
_vaultTimeoutOptions = VaultTimeoutOptions;
_vaultTimeoutActionOptions = VaultTimeoutActionOptions;
_vaultTimeout = await _vaultTimeoutService.GetVaultTimeout();
_vaultTimeoutDisplayValue = _vaultTimeoutOptions.FirstOrDefault(o => o.Value == _vaultTimeout).Key;
_vaultTimeoutDisplayValue ??= _vaultTimeoutOptions.Where(o => o.Value == CustomVaultTimeoutValue).First().Key;
var pinSet = await _vaultTimeoutService.GetPinLockTypeAsync();
_pin = pinSet != PinLockType.Disabled;
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
var timeoutAction = await _vaultTimeoutService.GetVaultTimeoutAction() ?? VaultTimeoutAction.Lock;
if (!IsVaultTimeoutActionLockAllowed && timeoutAction == VaultTimeoutAction.Lock)
{
timeoutAction = VaultTimeoutAction.Logout;
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(_vaultTimeout, VaultTimeoutAction.Logout);
}
_vaultTimeoutActionDisplayValue = _vaultTimeoutActionOptions.FirstOrDefault(o => o.Value == timeoutAction).Key;
if (await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout))
{
// if we have a vault timeout policy, we need to filter the timeout options
_vaultTimeoutPolicy = (await _policyService.GetAll(PolicyType.MaximumVaultTimeout)).First();
var policyMinutes = _vaultTimeoutPolicy.GetInt(Policy.MINUTES_KEY);
_vaultTimeoutOptions = _vaultTimeoutOptions.Where(t =>
t.Value <= policyMinutes &&
var minutes = _policyService.GetPolicyInt(_vaultTimeoutPolicy, "minutes").GetValueOrDefault();
_vaultTimeouts = _vaultTimeouts.Where(t =>
t.Value <= minutes &&
(t.Value > 0 || t.Value == CustomVaultTimeoutValue) &&
t.Value != null).ToList();
}
_vaultTimeout = await _vaultTimeoutService.GetVaultTimeout();
_vaultTimeoutDisplayValue = _vaultTimeouts.FirstOrDefault(o => o.Value == _vaultTimeout).Key;
var action = await _stateService.GetVaultTimeoutActionAsync() ?? VaultTimeoutAction.Lock;
_vaultTimeoutActionDisplayValue = _vaultTimeoutActions.FirstOrDefault(o => o.Value == action).Key;
var pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
_pin = pinSet.Item1 || pinSet.Item2;
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
_screenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
if (_vaultTimeoutDisplayValue == null)
@@ -159,7 +141,8 @@ namespace Bit.App.Pages
_vaultTimeoutDisplayValue = AppResources.Custom;
}
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() && await _userVerificationService.HasMasterPasswordAsync();
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
!await _keyConnectorService.GetUsesKeyConnector();
_reportLoggingEnabled = await _loggerService.IsEnabled();
_approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
_shouldConnectToWatch = await _stateService.GetShouldConnectToWatchAsync();
@@ -283,7 +266,7 @@ namespace Bit.App.Pages
{
var oldTimeout = _vaultTimeout;
var options = _vaultTimeoutOptions.Select(
var options = _vaultTimeouts.Select(
o => o.Key == _vaultTimeoutDisplayValue ? $"✓ {o.Key}" : o.Key).ToArray();
if (promptOptions)
{
@@ -294,7 +277,7 @@ namespace Bit.App.Pages
return;
}
var cleanSelection = selection.Replace("✓ ", string.Empty);
var selectionOption = _vaultTimeoutOptions.FirstOrDefault(o => o.Key == cleanSelection);
var selectionOption = _vaultTimeouts.FirstOrDefault(o => o.Key == cleanSelection);
// Check if the selected Timeout action is "Never" and if it's different from the previous selected value
if (selectionOption.Value == null && selectionOption.Value != oldTimeout)
@@ -312,13 +295,13 @@ namespace Bit.App.Pages
if (_vaultTimeoutPolicy != null)
{
var maximumTimeout = _vaultTimeoutPolicy.GetInt(Policy.MINUTES_KEY);
var maximumTimeout = _policyService.GetPolicyInt(_vaultTimeoutPolicy, "minutes");
if (newTimeout > maximumTimeout)
{
await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutToLarge, AppResources.Warning);
var timeout = await _vaultTimeoutService.GetVaultTimeout();
_vaultTimeoutDisplayValue = _vaultTimeoutOptions.FirstOrDefault(o => o.Value == timeout).Key ??
_vaultTimeoutDisplayValue = _vaultTimeouts.FirstOrDefault(o => o.Value == timeout).Key ??
AppResources.Custom;
return;
}
@@ -333,7 +316,6 @@ namespace Bit.App.Pages
}
if (oldTimeout != newTimeout)
{
await _cryptoService.RefreshKeysAsync();
await Device.InvokeOnMainThreadAsync(BuildList);
}
}
@@ -392,17 +374,8 @@ namespace Bit.App.Pages
public async Task VaultTimeoutActionAsync()
{
if (_vaultTimeoutPolicy != null &&
!string.IsNullOrEmpty(_vaultTimeoutPolicy.GetString(Policy.ACTION_KEY)))
{
// do nothing if we have a policy set
return;
}
var options = IsVaultTimeoutActionLockAllowed
? _vaultTimeoutActionOptions.Select(o => CreateSelectableOption(o.Key, _vaultTimeoutActionDisplayValue == o.Key)).ToArray()
: _vaultTimeoutActionOptions.Where(o => o.Value == VaultTimeoutAction.Logout).Select(v => ToSelectedOption(v.Key)).ToArray();
var options = _vaultTimeoutActions.Select(o =>
o.Key == _vaultTimeoutActionDisplayValue ? $"✓ {o.Key}" : o.Key).ToArray();
var selection = await Page.DisplayActionSheet(AppResources.VaultTimeoutAction,
AppResources.Cancel, null, options);
if (selection == null || selection == AppResources.Cancel)
@@ -420,7 +393,7 @@ namespace Bit.App.Pages
cleanSelection = AppResources.Lock;
}
}
var selectionOption = _vaultTimeoutActionOptions.FirstOrDefault(o => o.Key == cleanSelection);
var selectionOption = _vaultTimeoutActions.FirstOrDefault(o => o.Key == cleanSelection);
var changed = _vaultTimeoutActionDisplayValue != selectionOption.Key;
_vaultTimeoutActionDisplayValue = selectionOption.Key;
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(_vaultTimeout,
@@ -442,7 +415,7 @@ namespace Bit.App.Pages
if (!string.IsNullOrWhiteSpace(pin))
{
var masterPassOnRestart = false;
if (await _userVerificationService.HasMasterPasswordAsync())
if (!await _keyConnectorService.GetUsesKeyConnector())
{
masterPassOnRestart = await _platformUtilsService.ShowDialogAsync(
AppResources.PINRequireMasterPasswordRestart, AppResources.UnlockWithPIN,
@@ -451,20 +424,19 @@ namespace Bit.App.Pages
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
var email = await _stateService.GetEmailAsync();
var pinKey = await _cryptoService.MakePinKeyAsync(pin, email, kdfConfig);
var userKey = await _cryptoService.GetUserKeyAsync();
var protectedPinKey = await _cryptoService.EncryptAsync(userKey.Key, pinKey);
var encPin = await _cryptoService.EncryptAsync(pin);
await _stateService.SetProtectedPinAsync(encPin.EncryptedString);
var pinKey = await _cryptoService.MakePinKeyAysnc(pin, email, kdfConfig);
var key = await _cryptoService.GetKeyAsync();
var pinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
if (masterPassOnRestart)
{
await _stateService.SetPinKeyEncryptedUserKeyEphemeralAsync(protectedPinKey);
var encPin = await _cryptoService.EncryptAsync(pin);
await _stateService.SetProtectedPinAsync(encPin.EncryptedString);
await _stateService.SetPinProtectedKeyAsync(pinProtectedKey);
}
else
{
await _stateService.SetPinKeyEncryptedUserKeyAsync(protectedPinKey);
await _stateService.SetPinProtectedAsync(pinProtectedKey.EncryptedString);
}
}
else
@@ -474,8 +446,8 @@ namespace Bit.App.Pages
}
if (!_pin)
{
await _cryptoService.ClearPinProtectedKeyAsync();
await _vaultTimeoutService.ClearAsync();
await UpdateVaultTimeoutActionIfNeededAsync();
}
BuildList();
}
@@ -504,10 +476,9 @@ namespace Bit.App.Pages
else
{
await _stateService.SetBiometricUnlockAsync(null);
await UpdateVaultTimeoutActionIfNeededAsync();
}
await _stateService.SetBiometricLockedAsync(false);
await _cryptoService.RefreshKeysAsync();
await _cryptoService.ToggleKeyAsync();
BuildList();
}
@@ -626,36 +597,14 @@ namespace Bit.App.Pages
}
if (_vaultTimeoutPolicy != null)
{
var policyMinutes = _vaultTimeoutPolicy.GetInt(Policy.MINUTES_KEY);
var policyAction = _vaultTimeoutPolicy.GetString(Policy.ACTION_KEY);
if (policyMinutes.HasValue || !string.IsNullOrWhiteSpace(policyAction))
var maximumTimeout = _policyService.GetPolicyInt(_vaultTimeoutPolicy, "minutes").GetValueOrDefault();
securityItems.Insert(0, new SettingsPageListItem
{
string policyAlert;
if (policyMinutes.HasValue && string.IsNullOrWhiteSpace(policyAction))
{
policyAlert = string.Format(AppResources.VaultTimeoutPolicyInEffect,
Math.Floor((float)policyMinutes / 60),
policyMinutes % 60);
}
else if (!policyMinutes.HasValue && !string.IsNullOrWhiteSpace(policyAction))
{
policyAlert = string.Format(AppResources.VaultTimeoutActionPolicyInEffect,
policyAction == Policy.ACTION_LOCK ? AppResources.Lock : AppResources.LogOut);
}
else
{
policyAlert = string.Format(AppResources.VaultTimeoutPolicyWithActionInEffect,
Math.Floor((float)policyMinutes / 60),
policyMinutes % 60,
policyAction == Policy.ACTION_LOCK ? AppResources.Lock : AppResources.LogOut);
}
securityItems.Insert(0, new SettingsPageListItem
{
Name = policyAlert,
UseFrame = true,
});
}
Name = string.Format(AppResources.VaultTimeoutPolicyInEffect,
Math.Floor((float)maximumTimeout / 60),
maximumTimeout % 60),
UseFrame = true,
});
}
if (Device.RuntimePlatform == Device.Android)
{
@@ -843,19 +792,17 @@ namespace Bit.App.Pages
private VaultTimeoutAction GetVaultTimeoutActionFromKey(string key)
{
return _vaultTimeoutActionOptions.FirstOrDefault(o => o.Key == key).Value;
return _vaultTimeoutActions.FirstOrDefault(o => o.Key == key).Value;
}
private int? GetVaultTimeoutFromKey(string key)
{
return _vaultTimeoutOptions.FirstOrDefault(o => o.Key == key).Value;
return _vaultTimeouts.FirstOrDefault(o => o.Key == key).Value;
}
private string CreateSelectableOption(string option, bool selected) => selected ? ToSelectedOption(option) : option;
private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option;
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == ToSelectedOption(compareTo);
private string ToSelectedOption(string option) => $"✓ {option}";
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == $"✓ {compareTo}";
public async Task SetScreenCaptureAllowedAsync()
{
@@ -887,17 +834,5 @@ namespace Bit.App.Pages
await _watchDeviceService.SetShouldConnectToWatchAsync(_shouldConnectToWatch);
BuildList();
}
private async Task UpdateVaultTimeoutActionIfNeededAsync()
{
if (IsVaultTimeoutActionLockAllowed)
{
return;
}
_vaultTimeoutActionDisplayValue = _vaultTimeoutActionOptions.First(o => o.Value == VaultTimeoutAction.Logout).Key;
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(_vaultTimeout, VaultTimeoutAction.Logout);
_deviceActionService.Toast(AppResources.VaultTimeoutActionChangedToLogOut);
}
}
}

View File

@@ -3,7 +3,6 @@ using System.Threading.Tasks;
using Bit.App.Effects;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Models.Data;
using Bit.Core.Utilities;
@@ -16,7 +15,6 @@ namespace Bit.App.Pages
private readonly IBroadcasterService _broadcasterService;
private readonly IMessagingService _messagingService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly IStateService _stateService;
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private NavigationPage _groupingsPage;
@@ -28,7 +26,6 @@ namespace Bit.App.Pages
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_stateService = ServiceContainer.Resolve<IStateService>();
_groupingsPage = new NavigationPage(new GroupingsPage(true, previousPage: previousPage))
{
@@ -94,17 +91,10 @@ namespace Bit.App.Pages
}
});
await UpdateVaultButtonTitleAsync();
if (await _keyConnectorService.UserNeedsMigrationAsync())
if (await _keyConnectorService.UserNeedsMigration())
{
_messagingService.Send("convertAccountToKeyConnector");
}
var forcePasswordResetReason = await _stateService.GetForcePasswordResetReasonAsync();
if (forcePasswordResetReason.HasValue)
{
_messagingService.Send(Constants.ForceUpdatePassword);
}
}
protected override void OnDisappearing()

View File

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

View File

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

View File

@@ -37,8 +37,6 @@ namespace Bit.App.Pages
set => SetProperty(ref _cipher, value, additionalPropertyNames: AdditionalPropertiesToRaiseOnCipherChanged);
}
public string CreationDate => string.Format(AppResources.CreatedX, Cipher.CreationDate.ToShortDateString());
public AsyncCommand CheckPasswordCommand { get; }
protected async Task CheckPasswordAsync()

View File

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

View File

@@ -21,7 +21,7 @@ namespace Bit.App.Pages
private readonly IDeviceActionService _deviceActionService;
private readonly IAutofillHandler _autofillHandler;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IUserVerificationService _userVerificationService;
private readonly IKeyConnectorService _keyConnectorService;
private CipherAddEditPageViewModel _vm;
private bool _fromAutofill;
@@ -43,7 +43,7 @@ namespace Bit.App.Pages
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_appOptions = appOptions;
_fromAutofill = fromAutofill;
@@ -175,8 +175,8 @@ namespace Bit.App.Pages
RequestFocus(_nameEntry);
}
});
_passwordPrompt.IsVisible = await _userVerificationService.HasMasterPasswordAsync();
// Hide password reprompt option if using key connector
_passwordPrompt.IsVisible = !await _keyConnectorService.GetUsesKeyConnector();
}
protected override void OnDisappearing()

View File

@@ -88,6 +88,7 @@ namespace Bit.App.Pages
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
_accountsManager = ServiceContainer.Resolve<IAccountsManager>();
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
ToggleCardNumberCommand = new Command(ToggleCardNumber);
@@ -120,7 +121,6 @@ namespace Bit.App.Pages
new KeyValuePair<string, string>("JCB", "JCB"),
new KeyValuePair<string, string>("Maestro", "Maestro"),
new KeyValuePair<string, string>("UnionPay", "UnionPay"),
new KeyValuePair<string, string>("RuPay", "RuPay"),
new KeyValuePair<string, string>(AppResources.Other, "Other")
};
CardExpMonthOptions = new List<KeyValuePair<string, string>>
@@ -296,7 +296,6 @@ namespace Bit.App.Pages
public bool IsIdentity => Cipher?.Type == CipherType.Identity;
public bool IsCard => Cipher?.Type == CipherType.Card;
public bool IsSecureNote => Cipher?.Type == CipherType.SecureNote;
public bool IsFido2Key => Cipher?.Type == CipherType.Fido2Key;
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
public bool ShowAttachments => Cipher.HasAttachments;
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
@@ -309,7 +308,6 @@ namespace Bit.App.Pages
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public bool HasTotpValue => IsLogin && !string.IsNullOrEmpty(Cipher?.Login?.Totp);
public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTotp}";
public bool ShowPasskeyInfo => Cipher?.Login?.Fido2Key != null && !CloneMode;
public void Init()
{
@@ -368,11 +366,6 @@ namespace Bit.App.Pages
{
Cipher.OrganizationId = OrganizationId;
}
if (Cipher.Type == CipherType.Login)
{
// passkeys can't be cloned
Cipher.Login.Fido2Key = null;
}
}
if (appOptions?.OtpData != null && Cipher.Type == CipherType.Login)
{

View File

@@ -45,20 +45,20 @@
x:Name="_attachmentsItem" x:Key="attachmentsItem" />
<ToolbarItem Text="{u:I18n Delete}" Clicked="Delete_Clicked" Order="Secondary" IsDestructive="True"
x:Name="_deleteItem" x:Key="deleteItem" />
<ToolbarItem Text="{u:I18n Clone}" Command="{Binding CloneCommand}" Order="Secondary"
<ToolbarItem Text="{u:I18n Clone}" Clicked="Clone_Clicked" Order="Secondary"
x:Name="_cloneItem" x:Key="cloneItem" />
<DataTemplate x:Key="TextCustomFieldDataTemplate">
<il:TextCustomFieldItemLayout AutomationId="TextCustomFieldItem" />
<il:TextCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="BooleanCustomFieldDataTemplate">
<il:BooleanCustomFieldItemLayout AutomationId="BooleanCustomFieldItem" />
<il:BooleanCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="HiddenCustomFieldDataTemplate">
<il:HiddenCustomFieldItemLayout AutomationId="HiddenCustomFieldItem" />
<il:HiddenCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="LinkedCustomFieldDataTemplate">
<il:LinkedCustomFieldItemLayout AutomationId="LinkedCustomFieldItem" />
<il:LinkedCustomFieldItemLayout />
</DataTemplate>
<dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector"
@@ -69,26 +69,23 @@
<ScrollView x:Key="scrollView" x:Name="_scrollView">
<StackLayout Spacing="20" x:Name="_mainLayout">
<StackLayout StyleClass="box" AutomationId="ItemInformationSection">
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n ItemInformation, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row" AutomationId="ItemRow">
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n Name}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Name, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0">
<Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
@@ -101,14 +98,12 @@
Text="{u:I18n Username}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
AutomationId="ItemName" />
Grid.Column="0" />
<Label
Text="{Binding Cipher.Login.Username, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
AutomationId="ItemValue" />
Grid.Column="0" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -118,14 +113,12 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyUsername}"
AutomationId="CopyValueButton" />
AutomationProperties.Name="{u:I18n CopyUsername}" />
</Grid>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}" />
<Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
@@ -140,23 +133,20 @@
Text="{u:I18n Password}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
AutomationId="ItemName" />
Grid.Column="0" />
<controls:MonoLabel
Text="{Binding Cipher.Login.MaskedPassword, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
AutomationId="ItemValue" />
IsVisible="{Binding ShowPassword, Converter={StaticResource inverseBool}}" />
<controls:MonoLabel
Text="{Binding ColoredPassword, Mode=OneWay}"
StyleClass="box-value, text-html"
Grid.Row="1"
Grid.Column="0"
LineBreakMode="CharacterWrap"
IsVisible="{Binding ShowPassword}"
AutomationId="ItemValue" />
IsVisible="{Binding ShowPassword}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
@@ -166,8 +156,7 @@
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CheckPassword}"
IsVisible="{Binding Cipher.ViewPassword}"
AutomationId="CheckPasswordButton" />
IsVisible="{Binding Cipher.ViewPassword}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
@@ -178,8 +167,7 @@
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
IsVisible="{Binding Cipher.ViewPassword}"
AutomationId="ShowValueButton" />
IsVisible="{Binding Cipher.ViewPassword}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -190,24 +178,11 @@
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyPassword}"
IsVisible="{Binding Cipher.ViewPassword}"
AutomationId="CopyValueButton" />
IsVisible="{Binding Cipher.ViewPassword}" />
</Grid>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}" />
<Label
Text="{u:I18n Passkey}"
StyleClass="box-label"
Margin="0,10,0,0"
IsVisible="{Binding Cipher.Login.Fido2Key, Converter={StaticResource notNull}}"/>
<Entry
Text="{u:I18n AvailableForTwoStepLogin}"
IsEnabled="False"
StyleClass="box-value,text-muted"
IsVisible="{Binding Cipher.Login.Fido2Key, Converter={StaticResource notNull}}" />
<Grid StyleClass="box-row"
IsVisible="{Binding ShowTotp}"
AutomationId="ItemRow">
<Grid StyleClass="box-row" IsVisible="{Binding ShowTotp}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
@@ -222,8 +197,7 @@
Text="{u:I18n VerificationCodeTotp}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
AutomationId="ItemName" />
Grid.Column="0" />
<controls:MonoLabel
Text="{Binding TotpCodeFormatted, Mode=OneWay}"
IsVisible="{Binding ShowUpgradePremiumTotpText, Converter={StaticResource inverseBool}}"
@@ -231,16 +205,14 @@
Grid.Row="1"
Grid.Column="0"
VerticalTextAlignment="Start"
VerticalOptions="Start"
AutomationId="ItemValue" />
VerticalOptions="Start" />
<controls:CircularProgressbarView
Progress="{Binding TotpProgress}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
AutomationId="LoginTotpProgressBar" />
VerticalOptions="FillAndExpand" />
<Label
Text="{Binding TotpSec, Mode=OneWay}"
Style="{DynamicResource textTotp}"
@@ -262,8 +234,7 @@
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyTotp}"
AutomationId="CopyValueButton" />
AutomationProperties.Name="{u:I18n CopyTotp}" />
<Label
Text="{u:I18n PremiumSubscriptionRequired}"
StyleClass="box-footer-label"
@@ -271,29 +242,24 @@
Margin="0,5,0,2"
Grid.Column="0"
Grid.Row="1"
HorizontalOptions="FillAndExpand"
AutomationId="ShowUpgradePremiumTotpLabel" />
HorizontalOptions="FillAndExpand" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowTotp}" />
</StackLayout>
<StackLayout IsVisible="{Binding IsCard}" Spacing="0" Padding="0">
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n CardholderName}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Card.CardholderName, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}" />
<Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
@@ -307,22 +273,19 @@
Text="{u:I18n Number}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
AutomationId="ItemName" />
Grid.Column="0" />
<controls:MonoLabel
Text="{Binding Cipher.Card.MaskedNumber, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}"
AutomationId="ItemValue" />
IsVisible="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}" />
<controls:MonoLabel
Text="{Binding Cipher.Card.Number, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowCardNumber}"
AutomationId="ItemValue" />
IsVisible="{Binding ShowCardNumber}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowCardNumberIcon}"
@@ -331,8 +294,7 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationId="ShowValueButton" />
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -342,42 +304,34 @@
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyNumber}"
AutomationId="CopyValueButton" />
AutomationProperties.Name="{u:I18n CopyNumber}" />
</Grid>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Brand}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Card.Brand, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Expiration}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Card.Expiration, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}" />
<Grid StyleClass="box-row"
IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
@@ -391,22 +345,19 @@
Text="{u:I18n SecurityCode}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
AutomationId="ItemName" />
Grid.Column="0" />
<controls:MonoLabel
Text="{Binding Cipher.Card.MaskedCode, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowCardCode, Converter={StaticResource inverseBool}}"
AutomationId="ItemValue" />
IsVisible="{Binding ShowCardCode, Converter={StaticResource inverseBool}}" />
<controls:MonoLabel
Text="{Binding Cipher.Card.Code, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding ShowCardCode}"
AutomationId="ItemValue" />
IsVisible="{Binding ShowCardCode}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowCardCodeIcon}"
@@ -415,8 +366,7 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationId="ShowValueButton" />
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -426,228 +376,138 @@
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopySecurityCode}"
AutomationId="CopyValueButton" />
AutomationProperties.Name="{u:I18n CopySecurityCode}" />
</Grid>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}" />
</StackLayout>
<StackLayout IsVisible="{Binding IsIdentity}" Spacing="0" Padding="0">
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n IdentityName}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.FullName, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Username}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.Username, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Company}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.Company, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n SSN}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.SSN, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n PassportNumber}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.PassportNumber, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n LicenseNumber}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.LicenseNumber, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow">
IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Email}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.Email, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row"
IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}"
AutomationId="ItemRow" >
IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}">
<Label
Text="{u:I18n Phone}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.Phone, Mode=OneWay}"
StyleClass="box-value"
AutomationId="ItemValue" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator"
IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}" />
<StackLayout StyleClass="box-row" IsVisible="{Binding ShowIdentityAddress}"
AutomationId="ItemRow">
<StackLayout StyleClass="box-row" IsVisible="{Binding ShowIdentityAddress}">
<Label
Text="{u:I18n Address}"
StyleClass="box-label"
AutomationId="ItemName" />
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Identity.Address1, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Address1, Converter={StaticResource stringHasValue}}"
StyleClass="box-value"
AutomationId="IdentityAddressOneLabel" />
StyleClass="box-value" />
<Label
Text="{Binding Cipher.Identity.Address2, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Address2, Converter={StaticResource stringHasValue}}"
StyleClass="box-value"
AutomationId="IdentityAddressTwoLabel" />
StyleClass="box-value" />
<Label
Text="{Binding Cipher.Identity.Address3, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Address3, Converter={StaticResource stringHasValue}}"
StyleClass="box-value"
AutomationId="IdentityAddressThreeLabel" />
StyleClass="box-value" />
<Label
Text="{Binding Cipher.Identity.FullAddressPart2, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.FullAddressPart2, Converter={StaticResource stringHasValue}}"
StyleClass="box-value"
AutomationId="IdentityFullAddressPartTwoLabel" />
StyleClass="box-value" />
<Label
Text="{Binding Cipher.Identity.Country, Mode=OneWay}"
IsVisible="{Binding Cipher.Identity.Country, Converter={StaticResource stringHasValue}}"
StyleClass="box-value"
AutomationId="IdentityCountryLabel" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowIdentityAddress}" />
</StackLayout>
<StackLayout
IsVisible="{Binding IsFido2Key}"
Spacing="0"
Padding="0"
Margin="0,10,0,0">
<Label
Text="{u:I18n Username}"
StyleClass="box-label" />
<Label
Text="{Binding Cipher.Fido2Key.UserName, Mode=OneWay}"
StyleClass="box-value" />
<BoxView StyleClass="box-row-separator" Margin="0,10,0,0" />
<Label
Text="{u:I18n Passkey}"
StyleClass="box-label"
Margin="0,10,0,0" />
<Label
Text="{Binding CreationDate, Mode=OneWay}"
StyleClass="box-value" />
<BoxView StyleClass="box-row-separator" Margin="0,10,0,0" />
<Grid
StyleClass="box-row"
RowDefinitions="Auto,*,Auto"
ColumnDefinitions="*,Auto,Auto">
<Label
Text="{u:I18n Application}"
StyleClass="box-label" />
<Label
Grid.Row="1"
Text="{Binding Cipher.Fido2Key.LaunchUri, Mode=OneWay}"
StyleClass="box-value" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.ShareSquare}}"
Command="{Binding LaunchUriCommand}"
CommandParameter="{Binding Cipher.Fido2Key}"
Grid.Column="1"
Grid.RowSpan="2"
VerticalOptions="End"
IsVisible="{Binding Cipher.Fido2Key.CanLaunch, Mode=OneWay}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Launch}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
CommandParameter="Fido2KeyApplication"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyApplication}" />
<BoxView
StyleClass="box-row-separator"
Margin="0,3,0,0"
Grid.Row="2"
Grid.ColumnSpan="3" />
</Grid>
</StackLayout>
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowUris}">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n URIs, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Cipher.Login.Uris}" AutomationId="CipherUriContainer">
<controls:RepeaterView ItemsSource="{Binding Cipher.Login.Uris}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="views:LoginUriView">
<StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row" AutomationId="UriRow">
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
@@ -673,8 +533,7 @@
Text="{Binding HostOrUri, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
AutomationId="UriValue" />
Grid.Column="0" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.ShareSquare}}"
@@ -685,8 +544,7 @@
Grid.RowSpan="2"
IsVisible="{Binding CanLaunch, Mode=OneWay}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Launch}"
AutomationId="LaunchUriButton" />
AutomationProperties.Name="{u:I18n Launch}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
@@ -696,8 +554,7 @@
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Copy}"
AutomationId="CopyUriButton" />
AutomationProperties.Name="{u:I18n Copy}" />
</Grid>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
@@ -711,15 +568,14 @@
<Label Text="{u:I18n Notes, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row" AutomationId="NotesRow">
<StackLayout StyleClass="box-row">
<controls:SelectableLabel
Text="{Binding Cipher.Notes, Mode=OneWay}"
StyleClass="box-value"
AutomationId="CipherNotesLabel" />
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding Cipher.HasFields}" AutomationId="CustomFieldsContainer">
<StackLayout StyleClass="box" IsVisible="{Binding Cipher.HasFields}">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n CustomFields, Header=True}"
StyleClass="box-header, box-header-platform" />
@@ -734,23 +590,21 @@
<Label Text="{u:I18n Attachments, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Cipher.Attachments}" AutomationId="CipherAttachmentsContainer">
<controls:RepeaterView ItemsSource="{Binding Cipher.Attachments}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="views:AttachmentView">
<StackLayout Spacing="0" Padding="0">
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10" AutomationId="CipherAttachment">
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10">
<Label
Text="{Binding FileName, Mode=OneWay}"
StyleClass="box-value"
VerticalTextAlignment="Center"
HorizontalOptions="StartAndExpand"
AutomationId="CipherAttachmentFileNameLabel" />
HorizontalOptions="StartAndExpand" />
<Label
Text="{Binding SizeName, Mode=OneWay}"
StyleClass="box-sub-label"
HorizontalTextAlignment="End"
VerticalTextAlignment="Center"
AutomationId="CipherAttachmentFileSizeLabel" />
VerticalTextAlignment="Center" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Download}}"
@@ -758,8 +612,7 @@
CommandParameter="{Binding .}"
VerticalOptions="Center"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Download}"
AutomationId="CipherAttachmentDownloadButton" />
AutomationProperties.Name="{u:I18n Download}" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
@@ -769,20 +622,17 @@
</StackLayout>
<StackLayout StyleClass="box-bottom">
<Label FormattedText="{Binding UpdatedText}"
StyleClass="box-footer-label"
AutomationId="CipherUpdatedDateLabel" />
StyleClass="box-footer-label" />
<Label FormattedText="{Binding PasswordUpdatedText}"
StyleClass="box-footer-label"
IsVisible="{Binding Cipher.PasswordRevisionDisplayDate, Converter={StaticResource notNull}}"
AutomationId="CipherUpdatedPasswordDateLabel">
IsVisible="{Binding Cipher.PasswordRevisionDisplayDate, Converter={StaticResource notNull}}">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="PasswordHistory_Tapped" />
</Label.GestureRecognizers>
</Label>
<Label FormattedText="{Binding PasswordHistoryText}"
StyleClass="box-footer-label"
IsVisible="{Binding Cipher.HasPasswordHistory}"
AutomationId="CipherPasswordHistoryLabel">
IsVisible="{Binding Cipher.HasPasswordHistory}">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="PasswordHistory_Tapped" />
</Label.GestureRecognizers>
@@ -812,7 +662,6 @@
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n EditItem}"
AutomationId="CipherEditButton"
IsVisible="{Binding CanEdit}">
<Button.Effects>
<effects:FabShadowEffect />

View File

@@ -204,7 +204,20 @@ namespace Bit.App.Pages
}
}
private async void More_Clicked(object sender, EventArgs e)
private async void Clone_Clicked(object sender, System.EventArgs e)
{
if (DoOnce())
{
if (!await _vm.PromptPasswordAsync())
{
return;
}
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
private async void More_Clicked(object sender, System.EventArgs e)
{
if (!DoOnce())
{
@@ -214,11 +227,7 @@ namespace Bit.App.Pages
var options = new List<string> { AppResources.Attachments };
if (_vm.Cipher.OrganizationId == null)
{
if (_vm.CanClone)
{
options.Add(AppResources.Clone);
}
options.Add(AppResources.Clone);
options.Add(AppResources.MoveToOrganization);
}
else
@@ -258,7 +267,8 @@ namespace Bit.App.Pages
}
else if (selection == AppResources.Clone)
{
_vm.CloneCommand.Execute(null);
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
@@ -292,13 +302,13 @@ namespace Bit.App.Pages
{
ToolbarItems.Remove(_collectionsItem);
}
if (_vm.CanClone && !ToolbarItems.Contains(_cloneItem))
if (!ToolbarItems.Contains(_cloneItem))
{
ToolbarItems.Insert(1, _cloneItem);
}
if (!ToolbarItems.Contains(_shareItem))
{
ToolbarItems.Insert(_vm.CanClone ? 2 : 1, _shareItem);
ToolbarItems.Insert(2, _shareItem);
}
}
else

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