mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f634ff3f0 | ||
|
|
3e2005e5ed | ||
|
|
382eee2ed3 | ||
|
|
b0f1dd00ee | ||
|
|
5961a001ab | ||
|
|
9026dd10e5 | ||
|
|
355261679d | ||
|
|
7f14ec9b5d | ||
|
|
0c72626916 | ||
|
|
f21fae7fea | ||
|
|
6d4792bc24 | ||
|
|
dbadf8c56f | ||
|
|
4d0f9d1c03 | ||
|
|
68759fc608 | ||
|
|
47be3d6aef | ||
|
|
7ec5c8ccfd | ||
|
|
819aabb330 | ||
|
|
9c7ff853d7 | ||
|
|
e30f9903d1 | ||
|
|
249406e3a8 | ||
|
|
8cae840c68 | ||
|
|
e274c04107 | ||
|
|
7043be67dd | ||
|
|
afb8c515d6 | ||
|
|
bfcfd367dd | ||
|
|
a23454bc53 | ||
|
|
6f7100ae4f | ||
|
|
01ac20e6e4 | ||
|
|
8474f536ff | ||
|
|
f426c0e370 | ||
|
|
420dc09fd1 | ||
|
|
6d4793d592 | ||
|
|
eea7c6b7d7 | ||
|
|
ec93a61275 | ||
|
|
c34d1da6e6 | ||
|
|
c4e64e082b | ||
|
|
5aaff1ea20 | ||
|
|
0271a4db4c | ||
|
|
375718f945 | ||
|
|
9eda015371 | ||
|
|
ea81acb3bf | ||
|
|
174549e5bc | ||
|
|
87b1d18872 | ||
|
|
ae9ba810ff | ||
|
|
dd52ff0dcc | ||
|
|
c678c17ebc | ||
|
|
cd9e49b13b | ||
|
|
6d7970f767 | ||
|
|
9adc4d3080 | ||
|
|
1f20f70d13 | ||
|
|
a25da68437 | ||
|
|
fdc0313d10 | ||
|
|
f31c87b52e | ||
|
|
1e79e1182f | ||
|
|
11947ce99a | ||
|
|
4abb472998 | ||
|
|
1d541e5b8e | ||
|
|
175b9936b6 | ||
|
|
72e67bd6f2 | ||
|
|
216c6abcf6 | ||
|
|
1014563c75 | ||
|
|
3506269811 | ||
|
|
31487a31bb | ||
|
|
1407aa5655 | ||
|
|
16f59e2698 | ||
|
|
d876b54f45 | ||
|
|
6644e3b449 | ||
|
|
8d98d1d5bd | ||
|
|
3e9711f8f2 | ||
|
|
3af37f01d3 | ||
|
|
43d2d386b1 | ||
|
|
bc5c11b47f | ||
|
|
52843b4181 | ||
|
|
98705e443f | ||
|
|
1332ef7b43 | ||
|
|
04e30c2146 | ||
|
|
f604da13a1 | ||
|
|
dcf9acb51c | ||
|
|
3b087c50ae | ||
|
|
1c13ed9895 | ||
|
|
eeb634e698 | ||
|
|
8bc2df6c8a | ||
|
|
7cd40d4d89 | ||
|
|
bebf23785d | ||
|
|
e78833cbcb | ||
|
|
b7ff636862 | ||
|
|
0288a6659c | ||
|
|
c7fd113f26 | ||
|
|
79241731e7 | ||
|
|
74e9914f5b | ||
|
|
65307f6eab | ||
|
|
e9f83aee90 | ||
|
|
fdaf743868 | ||
|
|
9d6b938ba9 | ||
|
|
1c8328f62d | ||
|
|
f24b82f345 | ||
|
|
37f1a7087e | ||
|
|
6bb654e630 | ||
|
|
fc260f8159 | ||
|
|
bf463926a3 | ||
|
|
c1673a1bbf | ||
|
|
7b44395e1a | ||
|
|
0f3529aab8 | ||
|
|
a72779997c | ||
|
|
49da536c7a | ||
|
|
c985c0a62b | ||
|
|
0f417b8434 | ||
|
|
4f0238122b | ||
|
|
52ff634f00 | ||
|
|
e820537fce | ||
|
|
7130d8a18c | ||
|
|
659d34dfc2 | ||
|
|
6a5c999628 | ||
|
|
3bcb44ea71 | ||
|
|
b108b4e71d | ||
|
|
a72f267558 | ||
|
|
cc75cebdb8 | ||
|
|
3a0510d6b4 | ||
|
|
0c4b88e562 | ||
|
|
ac3b0c2bad | ||
|
|
1823efa0e5 | ||
|
|
e5ce1760a6 | ||
|
|
e77a971519 | ||
|
|
d7715c90f0 | ||
|
|
8fe9bd7347 | ||
|
|
11d3d71c32 | ||
|
|
0462f4db63 | ||
|
|
120f1d6859 | ||
|
|
99ceb8dbc1 | ||
|
|
d7d044f717 | ||
|
|
53d892a0ba | ||
|
|
80e38f8669 | ||
|
|
3e76f6b054 | ||
|
|
55a3b76f45 | ||
|
|
bd9b767339 | ||
|
|
276a93c497 | ||
|
|
c6bdb67981 | ||
|
|
a6bb089633 | ||
|
|
606b00142f | ||
|
|
151ecf83e7 | ||
|
|
ccd71202de |
29
.github/CODEOWNERS
vendored
Normal file
29
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Please sort lines alphabetically, this will ensure we don't accidentally add duplicates.
|
||||
#
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# The following owners will be the default owners for everything in the repo.
|
||||
# Unless a later match takes precedence
|
||||
# @bitwarden/tech-leads
|
||||
|
||||
## Auth team files ##
|
||||
|
||||
## Platform team files ##
|
||||
appIcons @bitwarden/team-platform-dev
|
||||
build.cake @bitwarden/team-platform-dev
|
||||
|
||||
## Vault team files ##
|
||||
src/watchOS @bitwarden/team-vault-dev
|
||||
|
||||
## Tools team files ##
|
||||
src/Core/Services/EmailForwarders @bitwarden/team-tools-dev
|
||||
|
||||
|
||||
## Crowdin Sync files ##
|
||||
src/App/Resources @bitwarden/tech-leads
|
||||
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization @bitwarden/tech-leads
|
||||
|
||||
## Locales ##
|
||||
src/App/Resources/AppResources.Designer.cs
|
||||
src/App/Resources/AppResources.resx
|
||||
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization/en.lproj
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
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!
|
||||
@@ -9,9 +12,6 @@ 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
Normal file
19
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
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
57
.github/renovate.json
vendored
@@ -1,22 +1,37 @@
|
||||
{
|
||||
"$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
|
||||
}
|
||||
]
|
||||
}
|
||||
"$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"]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
10
.github/workflows/automatic-issue-responses.yml
vendored
10
.github/workflows/automatic-issue-responses.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
# Feature request
|
||||
- if: github.event.label.name == 'feature-request'
|
||||
name: Feature request
|
||||
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
|
||||
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@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
|
||||
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@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
|
||||
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@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
|
||||
with:
|
||||
comment: |
|
||||
We’ve 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@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
|
||||
with:
|
||||
comment: |
|
||||
As we haven’t heard from you about this problem in some time, this issue will now be closed.
|
||||
|
||||
107
.github/workflows/build.yml
vendored
107
.github/workflows/build.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- 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@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
submodules: 'true'
|
||||
|
||||
@@ -67,12 +67,17 @@ jobs:
|
||||
variant: ["prod", "qa"]
|
||||
steps:
|
||||
- name: Setup NuGet
|
||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
||||
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@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||
uses: microsoft/setup-msbuild@1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c # v1.3.1
|
||||
|
||||
- name: Setup Windows builder
|
||||
run: choco install checksum --no-progress
|
||||
@@ -105,7 +110,7 @@ jobs:
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Decrypt secrets
|
||||
@@ -157,7 +162,7 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226
|
||||
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
|
||||
if: always()
|
||||
with:
|
||||
name: Test Results
|
||||
@@ -191,7 +196,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 }}";
|
||||
}
|
||||
@@ -232,7 +237,7 @@ jobs:
|
||||
shell: pwsh
|
||||
- name: Upload Prod .aab artifact
|
||||
if: ${{ matrix.variant == 'prod' }}
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: ./com.x8bit.bitwarden.aab
|
||||
@@ -240,7 +245,7 @@ jobs:
|
||||
|
||||
- name: Upload Prod .apk artifact
|
||||
if: ${{ matrix.variant == 'prod' }}
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: ./com.x8bit.bitwarden.apk
|
||||
@@ -248,7 +253,7 @@ jobs:
|
||||
|
||||
- name: Upload Other .apk artifact
|
||||
if: ${{ matrix.variant != 'prod' }}
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
@@ -268,7 +273,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk sha file for prod
|
||||
if: ${{ matrix.variant == 'prod' }}
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
|
||||
with:
|
||||
name: bw-android-apk-sha256.txt
|
||||
path: ./bw-android-apk-sha256.txt
|
||||
@@ -276,7 +281,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk sha file for other
|
||||
if: ${{ matrix.variant != 'prod' }}
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
|
||||
with:
|
||||
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
@@ -303,12 +308,12 @@ jobs:
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- name: Setup NuGet
|
||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
||||
with:
|
||||
nuget-version: 5.9.0
|
||||
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||
uses: microsoft/setup-msbuild@1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c # v1.3.1
|
||||
|
||||
- name: Setup Windows builder
|
||||
run: choco install checksum --no-progress
|
||||
@@ -342,7 +347,7 @@ jobs:
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: Decrypt secrets
|
||||
env:
|
||||
@@ -477,7 +482,7 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: ./com.x8bit.bitwarden-fdroid.apk
|
||||
@@ -489,7 +494,7 @@ jobs:
|
||||
-t sha256 | Out-File -Encoding ASCII ./bw-fdroid-apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid sha file
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
|
||||
with:
|
||||
name: bw-fdroid-apk-sha256.txt
|
||||
path: ./bw-fdroid-apk-sha256.txt
|
||||
@@ -502,7 +507,7 @@ jobs:
|
||||
needs: setup
|
||||
steps:
|
||||
- name: Setup NuGet
|
||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
||||
with:
|
||||
nuget-version: 5.9.0
|
||||
|
||||
@@ -515,19 +520,19 @@ jobs:
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
submodules: 'true'
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
env:
|
||||
KEYVAULT: bitwarden-prod-kv
|
||||
KEYVAULT: bitwarden-ci
|
||||
SECRETS: |
|
||||
appcenter-ios-token
|
||||
run: |
|
||||
@@ -662,6 +667,22 @@ jobs:
|
||||
$configuration = "AppStore";
|
||||
$platform = "iPhone";
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Archive $configuration Configuration for $platform Platform"
|
||||
Write-Output "########################################"
|
||||
msbuild "$($env:GITHUB_WORKSPACE + "/src/iOS/iOS.csproj")" "/p:Platform=$platform" `
|
||||
"/p:Configuration=$configuration" "/p:ArchiveOnBuild=true" "/t:`"Build`""
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Done"
|
||||
Write-Output "########################################"
|
||||
shell: pwsh
|
||||
|
||||
- name: Archive Build for Mobile Automation
|
||||
run: |
|
||||
$configuration = "Release";
|
||||
$platform = "iPhoneSimulator";
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Archive $configuration Configuration for $platform Platform"
|
||||
Write-Output "########################################"
|
||||
@@ -684,6 +705,15 @@ jobs:
|
||||
-exportOptionsPlist $EXPORT_OPTIONS_PATH
|
||||
shell: bash
|
||||
|
||||
- name: Export .app for Automation CI
|
||||
run: |
|
||||
ARCHIVE_PATH="./src/iOS/bin/iPhoneSimulator/Release/BitwardeniOS.app"
|
||||
EXPORT_PATH="./bitwarden-export"
|
||||
|
||||
zip -r -q BitwardeniOS.app.zip $ARCHIVE_PATH
|
||||
mv BitwardeniOS.app.zip $EXPORT_PATH
|
||||
shell: bash
|
||||
|
||||
- name: Copy all dSYMs files to upload
|
||||
run: |
|
||||
ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs"
|
||||
@@ -698,7 +728,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Upload App Store .ipa & dSYMs artifacts
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
|
||||
with:
|
||||
name: Bitwarden iOS
|
||||
path: |
|
||||
@@ -706,6 +736,13 @@ jobs:
|
||||
./bitwarden-export/dSYMs/*.*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .app file for Automation CI
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v3.0.0
|
||||
with:
|
||||
name: BitwardeniOS.app.zip
|
||||
path: ./bitwarden-export/BitwardeniOS.app.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install AppCenter CLI
|
||||
if: |
|
||||
(github.ref == 'refs/heads/master'
|
||||
@@ -771,17 +808,17 @@ jobs:
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
env:
|
||||
KEYVAULT: bitwarden-prod-kv
|
||||
KEYVAULT: bitwarden-ci
|
||||
SECRETS: |
|
||||
crowdin-api-token
|
||||
run: |
|
||||
@@ -793,7 +830,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Upload Sources
|
||||
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
|
||||
uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
@@ -839,17 +876,17 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
|
||||
if: failure()
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
if: failure()
|
||||
env:
|
||||
KEYVAULT: bitwarden-prod-kv
|
||||
KEYVAULT: bitwarden-ci
|
||||
SECRETS: |
|
||||
devops-alerts-slack-webhook-url
|
||||
run: |
|
||||
@@ -861,7 +898,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Notify Slack on failure
|
||||
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
||||
uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0
|
||||
if: failure()
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||
|
||||
16
.github/workflows/crowdin-pull.yml
vendored
16
.github/workflows/crowdin-pull.yml
vendored
@@ -15,22 +15,22 @@ jobs:
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
|
||||
uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0
|
||||
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 }}
|
||||
|
||||
2
.github/workflows/enforce-labels.yml
vendored
2
.github/workflows/enforce-labels.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Enforce Label
|
||||
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
|
||||
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
|
||||
with:
|
||||
BANNED_LABELS: "hold,needs-qa"
|
||||
BANNED_LABELS_DESCRIPTION: "PRs with the hold or needs-qa labels cannot be merged"
|
||||
|
||||
17
.github/workflows/pr-labeler.yml
vendored
Normal file
17
.github/workflows/pr-labeler.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
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@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0
|
||||
with:
|
||||
sync-labels: true
|
||||
30
.github/workflows/release.yml
vendored
30
.github/workflows/release.yml
vendored
@@ -38,11 +38,11 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@8f055ef543c7433c967a1b9b04a0f230923233bb
|
||||
uses: bitwarden/gh-actions/release-version-check@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
|
||||
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@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48
|
||||
uses: chrnorm/deployment-action@d42cde7132fcec920de534fffc3be83794335c00 # v2.0.5
|
||||
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@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||
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@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -87,14 +87,14 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
|
||||
uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0
|
||||
with:
|
||||
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
||||
./com.x8bit.bitwarden.apk/com.x8bit.bitwarden.apk,
|
||||
./com.x8bit.bitwarden-fdroid.apk/com.x8bit.bitwarden-fdroid.apk,
|
||||
./Bitwarden iOS.zip,
|
||||
./bw-android-apk-sha256.txt,
|
||||
./bw-fdroid-apk-sha256.txt"
|
||||
./bw-android-apk-sha256.txt/bw-android-apk-sha256.txt,
|
||||
./bw-fdroid-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@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
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@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'failure'
|
||||
@@ -126,11 +126,11 @@ jobs:
|
||||
if: inputs.fdroid_publish
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Download F-Droid .apk artifact
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||
with:
|
||||
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@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -147,9 +147,9 @@ jobs:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2.5.1
|
||||
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
|
||||
with:
|
||||
node-version: '10.x'
|
||||
node-version: '16.x'
|
||||
|
||||
- name: Set up F-Droid server
|
||||
run: |
|
||||
|
||||
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: 'Run stale action'
|
||||
uses: actions/stale@3cc123766321e9f15a6676375c154ccffb12a358 # v5.0.0
|
||||
uses: actions/stale@f7176fd3007623b69d27091f9b9d4ab7995f0a06 # v5.2.1
|
||||
with:
|
||||
stale-issue-label: 'needs-reply'
|
||||
stale-pr-label: 'needs-changes'
|
||||
|
||||
36
.github/workflows/version-auto-bump.yml
vendored
36
.github/workflows/version-auto-bump.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
version_number: ${{ steps.version.outputs.new-version }}
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Calculate bumped version
|
||||
id: version
|
||||
@@ -32,32 +32,8 @@ jobs:
|
||||
echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT
|
||||
|
||||
trigger_version_bump:
|
||||
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
|
||||
name: Bump version to ${{ needs.setup.outputs.version_number }}
|
||||
needs: setup
|
||||
uses: ./.github/workflows/version-bump.yml
|
||||
with:
|
||||
version_number: ${{ needs.setup.outputs.version_number }}
|
||||
|
||||
29
.github/workflows/version-bump.yml
vendored
29
.github/workflows/version-bump.yml
vendored
@@ -7,6 +7,11 @@ on:
|
||||
version_number:
|
||||
description: "New Version"
|
||||
required: true
|
||||
workflow_call:
|
||||
inputs:
|
||||
version_number:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
@@ -14,22 +19,22 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@c8bb57c57e8df1be8c73ff3d59deab1dbc00e0d1
|
||||
uses: crazy-max/ghaction-import-gpg@d6f3f49f3345e29369fe57596a3ca8f94c4d2ca7 # v5.4.0
|
||||
with:
|
||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||
@@ -40,31 +45,31 @@ jobs:
|
||||
run: git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Bump Version - Android XML
|
||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||
uses: bitwarden/gh-actions/version-bump@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./src/Android/Properties/AndroidManifest.xml"
|
||||
|
||||
- name: Bump Version - iOS.Autofill
|
||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||
uses: bitwarden/gh-actions/version-bump@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./src/iOS.Autofill/Info.plist"
|
||||
|
||||
- name: Bump Version - iOS.Extension
|
||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||
uses: bitwarden/gh-actions/version-bump@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./src/iOS.Extension/Info.plist"
|
||||
|
||||
- name: Bump Version - iOS.ShareExtension
|
||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||
uses: bitwarden/gh-actions/version-bump@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./src/iOS.ShareExtension/Info.plist"
|
||||
|
||||
- name: Bump Version - iOS
|
||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||
uses: bitwarden/gh-actions/version-bump@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
|
||||
with:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./src/iOS/Info.plist"
|
||||
|
||||
2
.github/workflows/workflow-linter.yml
vendored
2
.github/workflows/workflow-linter.yml
vendored
@@ -8,4 +8,4 @@ on:
|
||||
|
||||
jobs:
|
||||
call-workflow:
|
||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@master
|
||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@4a7ddc1b38ca5cb4e3e43578f4df5cabe4f55a67
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# Bitwarden Mobile Application
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden" target="_blank"><img alt="Get it on Google Play" src="https://imgur.com/YQzmZi9.png" width="153" height="46"></a> <a href="https://mobileapp.bitwarden.com/fdroid/" target="_blank"><img alt="Get it on Google Play" src="https://i.imgur.com/HDicnzz.png" width="154" height="46"></a> <a href="https://itunes.apple.com/us/app/bitwarden-free-password-manager/id1137397744?mt=8" target="_blank"><img src="https://imgur.com/GdGqPMY.png" width="135" height="40"></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden" target="_blank"><img alt="Get it on Google Play" src="https://imgur.com/YQzmZi9.png" width="153" height="46"></a> <a href="https://mobileapp.bitwarden.com/fdroid/" target="_blank"><img alt="Get it on F-Droid" src="https://i.imgur.com/HDicnzz.png" width="154" height="46"></a> <a href="https://itunes.apple.com/us/app/bitwarden-free-password-manager/id1137397744?mt=8" target="_blank"><img src="https://imgur.com/GdGqPMY.png" width="135" height="40"></a>
|
||||
|
||||
The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin iOS, and Xamarin Forms.
|
||||
|
||||
|
||||
12
crowdin.yml
12
crowdin.yml
@@ -38,3 +38,15 @@ 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
|
||||
|
||||
@@ -77,22 +77,21 @@
|
||||
<PackageReference Include="Portable.BouncyCastle">
|
||||
<Version>1.9.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1.1" />
|
||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.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.AndroidX.AppCompat" Version="1.6.1.3" />
|
||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
|
||||
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.21" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.10.1.2" />
|
||||
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.4.0.2" />
|
||||
<PackageReference Include="Xamarin.Essentials">
|
||||
<Version>1.7.3</Version>
|
||||
<Version>1.8.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||
<Version>123.0.8</Version>
|
||||
<Version>123.1.2.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.6.1.1" />
|
||||
<PackageReference Include="Xamarin.Google.Dagger" Version="2.41.0.2" />
|
||||
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.9.0.2" />
|
||||
<PackageReference Include="Xamarin.Google.Dagger" Version="2.46.1.2" />
|
||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
||||
<Version>118.0.1.2</Version>
|
||||
<Version>118.0.1.5</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -160,6 +159,7 @@
|
||||
<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,6 +233,18 @@
|
||||
<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.
@@ -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,15 +54,14 @@ namespace Bit.Droid.Autofill
|
||||
if (HintToFieldsMap.ContainsKey(View.AutofillHintPassword))
|
||||
{
|
||||
_passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]);
|
||||
return _passwordFields;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
_passwordFields = Fields.Where(f => FieldIsPassword(f)).ToList();
|
||||
if (!_passwordFields.Any())
|
||||
{
|
||||
_passwordFields = Fields.Where(f => FieldIsPassword(f)).ToList();
|
||||
if (!_passwordFields.Any())
|
||||
{
|
||||
_passwordFields = Fields.Where(f => FieldHasPasswordTerms(f)).ToList();
|
||||
}
|
||||
_passwordFields = Fields.Where(f => FieldHasPasswordTerms(f)).ToList();
|
||||
}
|
||||
return _passwordFields;
|
||||
}
|
||||
@@ -87,24 +86,26 @@ namespace Bit.Droid.Autofill
|
||||
{
|
||||
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]);
|
||||
}
|
||||
if (_usernameFields.Any())
|
||||
{
|
||||
return _usernameFields;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var passwordField in PasswordFields)
|
||||
{
|
||||
var usernameField = Fields.TakeWhile(f => f.AutofillId != passwordField.AutofillId)
|
||||
.LastOrDefault();
|
||||
if (usernameField != null)
|
||||
{
|
||||
_usernameFields.Add(usernameField);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_usernameFields.Any())
|
||||
foreach (var passwordField in PasswordFields)
|
||||
{
|
||||
var usernameField = Fields.TakeWhile(f => f.AutofillId != passwordField.AutofillId)
|
||||
.LastOrDefault();
|
||||
if (usernameField != null)
|
||||
{
|
||||
_usernameFields = Fields.Where(f => FieldIsUsername(f)).ToList();
|
||||
_usernameFields.Add(usernameField);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_usernameFields.Any())
|
||||
{
|
||||
_usernameFields = Fields.Where(f => FieldIsUsername(f)).ToList();
|
||||
}
|
||||
return _usernameFields;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ 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
|
||||
@@ -67,9 +68,9 @@ namespace Bit.Droid
|
||||
ServiceContainer.Register<IDeleteAccountActionFlowExecutioner>("deleteAccountActionFlowExecutioner", deleteAccountActionFlowExecutioner);
|
||||
|
||||
var verificationActionsFlowHelper = new VerificationActionsFlowHelper(
|
||||
ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService"),
|
||||
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
|
||||
ServiceContainer.Resolve<ICryptoService>("cryptoService"));
|
||||
ServiceContainer.Resolve<ICryptoService>("cryptoService"),
|
||||
ServiceContainer.Resolve<IUserVerificationService>());
|
||||
ServiceContainer.Register<IVerificationActionsFlowHelper>("verificationActionsFlowHelper", verificationActionsFlowHelper);
|
||||
|
||||
var accountsManager = new AccountsManager(
|
||||
@@ -81,7 +82,8 @@ namespace Bit.Droid
|
||||
ServiceContainer.Resolve<IAuthService>("authService"),
|
||||
ServiceContainer.Resolve<ILogger>("logger"),
|
||||
ServiceContainer.Resolve<IMessagingService>("messagingService"),
|
||||
ServiceContainer.Resolve<IWatchDeviceService>());
|
||||
ServiceContainer.Resolve<IWatchDeviceService>(),
|
||||
ServiceContainer.Resolve<IConditionedAwaiterManager>());
|
||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||
}
|
||||
#if !FDROID
|
||||
@@ -146,7 +148,7 @@ namespace Bit.Droid
|
||||
var storageMediatorService = new StorageMediatorService(mobileStorageService, secureStorageService, preferencesStorage);
|
||||
var stateService = new StateService(mobileStorageService, secureStorageService, storageMediatorService, messagingService);
|
||||
var stateMigrationService =
|
||||
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
||||
new StateMigrationService(DeviceType.Android, liteDbStorage, preferencesStorage, secureStorageService);
|
||||
var clipboardService = new ClipboardService(stateService);
|
||||
var deviceActionService = new DeviceActionService(stateService, messagingService);
|
||||
var fileService = new FileService(stateService, broadcasterService);
|
||||
@@ -154,10 +156,10 @@ 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 passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService);
|
||||
var biometricService = new BiometricService(stateService, cryptoService);
|
||||
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
|
||||
|
||||
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
||||
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
|
||||
|
||||
@@ -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.2.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.9.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
|
||||
43
src/Android/Renderers/CustomLabelRenderer.cs
Normal file
43
src/Android/Renderers/CustomLabelRenderer.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.ComponentModel;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Droid.Renderers;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
|
||||
[assembly: ExportRenderer(typeof(CustomLabel), typeof(CustomLabelRenderer))]
|
||||
namespace Bit.Droid.Renderers
|
||||
{
|
||||
public class CustomLabelRenderer : LabelRenderer
|
||||
{
|
||||
public CustomLabelRenderer(Context context)
|
||||
: base(context)
|
||||
{ }
|
||||
|
||||
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
|
||||
{
|
||||
base.OnElementChanged(e);
|
||||
|
||||
if (Control != null && e.NewElement is CustomLabel label)
|
||||
{
|
||||
if (label.FontWeight.HasValue && Build.VERSION.SdkInt >= BuildVersionCodes.P)
|
||||
{
|
||||
Control.Typeface = Android.Graphics.Typeface.Create(null, label.FontWeight.Value, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
var label = sender as CustomLabel;
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(CustomLabel.AutomationId):
|
||||
Control.ContentDescription = label.AutomationId;
|
||||
break;
|
||||
}
|
||||
base.OnElementPropertyChanged(sender, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Android/Resources/drawable/empty_uris_placeholder.xml
Normal file
35
src/Android/Resources/drawable/empty_uris_placeholder.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="129"
|
||||
android:viewportHeight="124"
|
||||
android:width="129dp"
|
||||
android:height="124dp">
|
||||
<path
|
||||
android:pathData="M126.8227 61.9441A59.6843 59.6843 0 0 1 7.4541 61.9441A59.6843 59.6843 0 0 1 126.8227 61.9441Z"
|
||||
android:fillColor="#F0F0F0"
|
||||
android:strokeColor="#89929F"
|
||||
android:strokeWidth="3" />
|
||||
<path
|
||||
android:pathData="M21.6167 100.851C52.597 103.31 79.6937 80.3264 82.1391 49.5156C83.6205 30.8497 76.0789 14.8844 62.7275 3.63385"
|
||||
android:strokeColor="#89929F"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M14.5633 34.2845C12.2035 66.7711 38.5225 96.3429 72.6666 98.8232C74.2596 98.9389 78.629 98.9975 80.1951 99C84.6245 98.8232 97.8063 96.593 106.813 91.8485C113.439 88.3581 119.745 84.6984 124.644 79.1121"
|
||||
android:strokeColor="#89929F"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M124.502 48.5051C106.554 24.3817 68.8237 21.6709 41.4178 42.0617C24.8146 54.4149 14.7327 72.4183 13.9255 90.1427"
|
||||
android:strokeColor="#89929F"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M83.4034 28.3934A5 5 0 0 1 73.4034 28.3934A5 5 0 0 1 83.4034 28.3934Z"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M24.7698 66.5518A5 5 0 0 1 14.7698 66.5518A5 5 0 0 1 24.7698 66.5518Z"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M57.344 94.4726A5 5 0 0 1 47.344 94.4726A5 5 0 0 1 57.344 94.4726Z"
|
||||
android:fillColor="#89929F" />
|
||||
</vector>
|
||||
@@ -0,0 +1,35 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="129"
|
||||
android:viewportHeight="124"
|
||||
android:width="129dp"
|
||||
android:height="124dp">
|
||||
<path
|
||||
android:pathData="M126.8227 61.9441A59.6843 59.6843 0 0 1 7.4541 61.9441A59.6843 59.6843 0 0 1 126.8227 61.9441Z"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#A3A3A3"
|
||||
android:strokeWidth="3" />
|
||||
<path
|
||||
android:pathData="M21.6167 100.851C52.597 103.31 79.6937 80.3264 82.1391 49.5156C83.6205 30.8497 76.0789 14.8844 62.7275 3.63385"
|
||||
android:strokeColor="#A3A3A3"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M14.5633 34.2845C12.2035 66.7711 38.5225 96.3429 72.6666 98.8232C74.2596 98.9389 78.629 98.9975 80.1951 99C84.6245 98.8232 97.8063 96.593 106.813 91.8485C113.439 88.3581 119.745 84.6984 124.644 79.1121"
|
||||
android:strokeColor="#A3A3A3"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M124.502 48.5051C106.554 24.3817 68.8237 21.6709 41.4178 42.0617C24.8146 54.4149 14.7327 72.4183 13.9255 90.1427"
|
||||
android:strokeColor="#A3A3A3"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M83.4034 28.3934A5 5 0 0 1 73.4034 28.3934A5 5 0 0 1 83.4034 28.3934Z"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M24.7698 66.5518A5 5 0 0 1 14.7698 66.5518A5 5 0 0 1 24.7698 66.5518Z"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M57.344 94.4726A5 5 0 0 1 47.344 94.4726A5 5 0 0 1 57.344 94.4726Z"
|
||||
android:fillColor="#A3A3A3" />
|
||||
</vector>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="30dp"
|
||||
android:paddingRight="30dp">
|
||||
<TextView
|
||||
android:id="@+id/lblHeader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/dialog_header_text_size"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="-3dp"
|
||||
android:labelFor="@+id/txtValue"/>
|
||||
<EditText
|
||||
android:id="@id/txtValue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/dialog_input_text_size"/>
|
||||
<TextView
|
||||
android:id="@+id/lblValueSubinfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/dialog_sub_value_info_text_size"/>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
<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>
|
||||
|
||||
@@ -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 : IBiometricService
|
||||
public class BiometricService : BaseBiometricService
|
||||
{
|
||||
private const string KeyName = "com.8bit.bitwarden.biometric_integrity";
|
||||
|
||||
@@ -23,28 +23,28 @@ namespace Bit.Droid.Services
|
||||
|
||||
private readonly KeyStore _keystore;
|
||||
|
||||
public BiometricService()
|
||||
public BiometricService(IStateService stateService, ICryptoService cryptoService)
|
||||
: base(stateService, cryptoService)
|
||||
{
|
||||
_keystore = KeyStore.GetInstance(KeyStoreName);
|
||||
_keystore.Load(null);
|
||||
}
|
||||
|
||||
public Task<bool> SetupBiometricAsync(string bioIntegrityKey = null)
|
||||
public override async Task<bool> SetupBiometricAsync(string bioIntegritySrcKey = null)
|
||||
{
|
||||
// bioIntegrityKey used in iOS only
|
||||
if (Build.VERSION.SdkInt >= BuildVersionCodes.M)
|
||||
{
|
||||
CreateKey();
|
||||
await CreateKeyAsync(bioIntegritySrcKey);
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task<bool> ValidateIntegrityAsync(string bioIntegrityKey = null)
|
||||
public override async Task<bool> IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null)
|
||||
{
|
||||
if (Build.VERSION.SdkInt < BuildVersionCodes.M)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
@@ -55,7 +55,7 @@ namespace Bit.Droid.Services
|
||||
|
||||
if (key == null || cipher == null)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
cipher.Init(CipherMode.EncryptMode, key);
|
||||
@@ -63,25 +63,32 @@ namespace Bit.Droid.Services
|
||||
catch (KeyPermanentlyInvalidatedException e)
|
||||
{
|
||||
// Biometric has changed
|
||||
return Task.FromResult(false);
|
||||
await ClearStateAsync(bioIntegritySrcKey);
|
||||
return false;
|
||||
}
|
||||
catch (UnrecoverableKeyException e)
|
||||
{
|
||||
// Biometric was disabled and re-enabled
|
||||
return Task.FromResult(false);
|
||||
await ClearStateAsync(bioIntegritySrcKey);
|
||||
return false;
|
||||
}
|
||||
catch (InvalidKeyException e)
|
||||
{
|
||||
// Fallback for old bitwarden users without a key
|
||||
LoggerHelper.LogEvenIfCantBeResolved(e);
|
||||
CreateKey();
|
||||
await CreateKeyAsync(bioIntegritySrcKey);
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CreateKey()
|
||||
private async Task CreateKeyAsync(string bioIntegritySrcKey = null)
|
||||
{
|
||||
bioIntegritySrcKey ??= Core.Constants.BiometricIntegritySourceKey;
|
||||
await _stateService.SetSystemBiometricIntegrityState(bioIntegritySrcKey,
|
||||
await GetStateAsync(bioIntegritySrcKey));
|
||||
await _stateService.SetAccountBiometricIntegrityValidAsync(bioIntegritySrcKey);
|
||||
|
||||
try
|
||||
{
|
||||
var keyGen = KeyGenerator.GetInstance(KeyAlgorithm, KeyStoreName);
|
||||
@@ -101,5 +108,16 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,13 @@ 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 static Bit.App.Pages.SettingsPageViewModel;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
|
||||
namespace Bit.Droid.Services
|
||||
{
|
||||
@@ -209,10 +210,7 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
if (numericKeyboard)
|
||||
{
|
||||
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
|
||||
SetNumericKeyboardTo(input);
|
||||
}
|
||||
if (password)
|
||||
{
|
||||
@@ -248,6 +246,83 @@ 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;
|
||||
@@ -525,5 +600,29 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Java.Lang;
|
||||
|
||||
namespace Bit.Droid.Utilities
|
||||
{
|
||||
@@ -13,7 +14,12 @@ namespace Bit.Droid.Utilities
|
||||
// Note: getting the bundle like this will cause to call unparcel() internally
|
||||
var b = intent?.Extras?.GetBundle("trashstringwhichhasnousebuttocheckunparcel");
|
||||
}
|
||||
catch (BadParcelableException)
|
||||
catch (Exception ex) when
|
||||
(
|
||||
ex is BadParcelableException ||
|
||||
ex is ClassNotFoundException ||
|
||||
ex is RuntimeException
|
||||
)
|
||||
{
|
||||
intent.ReplaceExtras((Bundle)null);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities.Prompts;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
|
||||
@@ -18,6 +19,7 @@ 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);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
@@ -6,10 +7,8 @@ namespace Bit.App.Abstractions
|
||||
{
|
||||
string[] ProtectedFields { get; }
|
||||
|
||||
Task<bool> ShowPasswordPromptAsync();
|
||||
Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
|
||||
|
||||
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
|
||||
|
||||
Task<bool> Enabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.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="SkiaSharp.Views.Forms" Version="2.88.3" />
|
||||
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.6" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.8.0" />
|
||||
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2612" />
|
||||
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
|
||||
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
|
||||
<PackageReference Include="MessagePack" Version="2.4.59" />
|
||||
@@ -145,6 +145,8 @@
|
||||
<Folder Include="Controls\DateTime\" />
|
||||
<Folder Include="Controls\IconLabelButton\" />
|
||||
<Folder Include="Controls\PasswordStrengthProgressBar\" />
|
||||
<Folder Include="Utilities\Automation\" />
|
||||
<Folder Include="Utilities\Prompts\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -440,5 +442,7 @@
|
||||
<None Remove="MessagePack" />
|
||||
<None Remove="MessagePack.MSBuild.Tasks" />
|
||||
<None Remove="Controls\PasswordStrengthProgressBar\" />
|
||||
<None Remove="Utilities\Automation\" />
|
||||
<None Remove="Utilities\Prompts\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -38,6 +38,7 @@ 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;
|
||||
@@ -61,6 +62,7 @@ namespace Bit.App
|
||||
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||
_configService = ServiceContainer.Resolve<IConfigService>();
|
||||
|
||||
_accountsManager.Init(() => Options, this);
|
||||
|
||||
@@ -161,6 +163,18 @@ 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)
|
||||
@@ -217,7 +231,7 @@ namespace Bit.App
|
||||
Id = loginRequestData.Id,
|
||||
IpAddress = loginRequestData.RequestIpAddress,
|
||||
Email = await _stateService.GetEmailAsync(),
|
||||
FingerprintPhrase = loginRequestData.RequestFingerprint,
|
||||
FingerprintPhrase = loginRequestData.FingerprintPhrase,
|
||||
RequestDate = loginRequestData.CreationDate,
|
||||
DeviceType = loginRequestData.RequestDeviceType,
|
||||
Origin = loginRequestData.Origin
|
||||
@@ -285,6 +299,8 @@ namespace Bit.App
|
||||
// Reset delay on every start
|
||||
_vaultTimeoutService.DelayLockAndLogoutMs = null;
|
||||
}
|
||||
|
||||
await _configService.GetAsync();
|
||||
_messagingService.Send("startEventTimer");
|
||||
}
|
||||
|
||||
|
||||
@@ -30,13 +30,15 @@
|
||||
BackgroundColor="{DynamicResource BackgroundColor}"
|
||||
VerticalOptions="Start"
|
||||
RowHeight="{Binding AccountListRowHeight, Source={x:Reference _mainOverlay}}"
|
||||
effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never">
|
||||
effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never"
|
||||
AutomationId="AccountListView">
|
||||
<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>
|
||||
|
||||
@@ -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,20 +60,23 @@
|
||||
Text="{Binding AccountView.Email}"
|
||||
IsVisible="{Binding IsActive}"
|
||||
StyleClass="accountlist-title, accountlist-title-platform"
|
||||
LineBreakMode="TailTruncation" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountEmailLabel" />
|
||||
<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" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountEmailLabel" />
|
||||
<Label
|
||||
Grid.Row="1"
|
||||
IsVisible="{Binding ShowHostname}"
|
||||
Text="{Binding AccountView.Hostname}"
|
||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||
LineBreakMode="TailTruncation" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountHostUrlLabel" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Text="{u:I18n AccountUnlocked}"
|
||||
@@ -81,7 +84,8 @@
|
||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||
FontAttributes="Italic"
|
||||
TextTransform="Lowercase"
|
||||
LineBreakMode="TailTruncation" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountStatusLabel" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Text="{u:I18n AccountLocked}"
|
||||
@@ -89,7 +93,8 @@
|
||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||
FontAttributes="Italic"
|
||||
TextTransform="Lowercase"
|
||||
LineBreakMode="TailTruncation" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountStatusLabel" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Text="{u:I18n AccountLoggedOut}"
|
||||
@@ -97,7 +102,8 @@
|
||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||
FontAttributes="Italic"
|
||||
TextTransform="Lowercase"
|
||||
LineBreakMode="TailTruncation" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountStatusLabel" />
|
||||
</Grid>
|
||||
|
||||
<controls:IconLabel
|
||||
@@ -107,7 +113,8 @@
|
||||
Margin="12,0"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform" />
|
||||
StyleClass="list-icon, list-icon-platform"
|
||||
AutomationId="InactiveVaultIcon" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="2"
|
||||
Text="{Binding AuthStatusIconActive}"
|
||||
@@ -116,7 +123,8 @@
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform"
|
||||
TextColor="{DynamicResource TextColor}"/>
|
||||
TextColor="{DynamicResource TextColor}"
|
||||
AutomationId="ActiveVaultIcon" />
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
@@ -147,7 +155,8 @@
|
||||
StyleClass="accountlist-title, accountlist-title-platform"
|
||||
LineBreakMode="TailTruncation"
|
||||
VerticalOptions="Center"
|
||||
Grid.Column="1" />
|
||||
Grid.Column="1"
|
||||
AutomationId="AddAccountButton" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ViewCell>
|
||||
@@ -9,7 +9,8 @@
|
||||
StyleClass="list-row, list-row-platform"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0"
|
||||
x:DataType="controls:CipherViewCellViewModel">
|
||||
x:DataType="controls:CipherViewCellViewModel"
|
||||
AutomationId="CipherCell">
|
||||
|
||||
<Grid.Resources>
|
||||
<u:IconGlyphConverter x:Key="iconGlyphConverter"/>
|
||||
@@ -36,7 +37,8 @@
|
||||
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
|
||||
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
|
||||
ShouldUpdateFontSizeDynamicallyForAccesibility="True"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="CipherTypeIcon" />
|
||||
|
||||
<ff:CachedImage
|
||||
x:Name="_iconImage"
|
||||
@@ -52,7 +54,8 @@
|
||||
Aspect="AspectFit"
|
||||
IsVisible="{Binding ShowIconImage}"
|
||||
Source="{Binding IconImageSource, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="CipherWebsiteIcon" />
|
||||
|
||||
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center" Padding="0, 7">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -71,7 +74,8 @@
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Cipher.Name}" />
|
||||
Text="{Binding Cipher.Name}"
|
||||
AutomationId="CipherNameLabel" />
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
@@ -80,7 +84,8 @@
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Cipher.SubTitle}"
|
||||
IsVisible="{Binding Source={RelativeSource Self}, Path=Text,
|
||||
Converter={StaticResource stringHasValueConverter}}"/>
|
||||
Converter={StaticResource stringHasValueConverter}}"
|
||||
AutomationId="CipherSubTitleLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
@@ -91,7 +96,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Collection}}"
|
||||
IsVisible="{Binding Cipher.Shared, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Shared}" />
|
||||
AutomationProperties.Name="{u:I18n Shared}"
|
||||
AutomationId="CipherInCollectionIcon" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="2"
|
||||
Grid.Row="0"
|
||||
@@ -102,7 +108,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Paperclip}}"
|
||||
IsVisible="{Binding Cipher.HasAttachments, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Attachments}" />
|
||||
AutomationProperties.Name="{u:I18n Attachments}"
|
||||
AutomationId="CipherWithAttachmentsIcon" />
|
||||
</Grid>
|
||||
|
||||
<controls:MiButton
|
||||
@@ -114,6 +121,7 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="EndAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
AutomationId="CipherOptionsButton" />
|
||||
|
||||
</controls:ExtendedGrid>
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -18,7 +19,7 @@ namespace Bit.App.Controls
|
||||
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(CipherViewCell));
|
||||
|
||||
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
|
||||
nameof(ButtonCommand), typeof(Command<CipherView>), typeof(CipherViewCell));
|
||||
nameof(ButtonCommand), typeof(ICommand), typeof(CipherViewCell));
|
||||
|
||||
public CipherViewCell()
|
||||
{
|
||||
@@ -42,9 +43,9 @@ namespace Bit.App.Controls
|
||||
set => SetValue(CipherProperty, value);
|
||||
}
|
||||
|
||||
public Command<CipherView> ButtonCommand
|
||||
public ICommand ButtonCommand
|
||||
{
|
||||
get => GetValue(ButtonCommandProperty) as Command<CipherView>;
|
||||
get => GetValue(ButtonCommandProperty) as ICommand;
|
||||
set => SetValue(ButtonCommandProperty, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Bit.App.Controls
|
||||
public bool ShowIconImage
|
||||
{
|
||||
get => WebsiteIconsEnabled
|
||||
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
|
||||
&& !string.IsNullOrWhiteSpace(Cipher.LaunchUri)
|
||||
&& IconImageSource != null;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Bit.App.Controls
|
||||
{
|
||||
if (_iconImageSource == string.Empty) // default value since icon source can return null
|
||||
{
|
||||
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
|
||||
_iconImageSource = IconImageHelper.GetIconImage(Cipher);
|
||||
}
|
||||
return _iconImageSource;
|
||||
}
|
||||
|
||||
13
src/App/Controls/CustomLabel.cs
Normal file
13
src/App/Controls/CustomLabel.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class CustomLabel : Label
|
||||
{
|
||||
public CustomLabel()
|
||||
{
|
||||
}
|
||||
|
||||
public int? FontWeight { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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,14 +54,16 @@
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Send.Name}" />
|
||||
Text="{Binding Send.Name}"
|
||||
AutomationId="SendNameLabel" />
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="6"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Send.DisplayDate}" />
|
||||
Text="{Binding Send.DisplayDate}"
|
||||
AutomationId="SendDateLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
@@ -72,7 +74,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ExclamationTriangle}}"
|
||||
IsVisible="{Binding Send.Disabled, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Disabled}" />
|
||||
AutomationProperties.Name="{u:I18n Disabled}"
|
||||
AutomationId="DisabledSendLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="2"
|
||||
Grid.Row="0"
|
||||
@@ -83,7 +86,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Key}}"
|
||||
IsVisible="{Binding Send.HasPassword, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Password}" />
|
||||
AutomationProperties.Name="{u:I18n Password}"
|
||||
AutomationId="PasswordProtectedSendLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="3"
|
||||
Grid.Row="0"
|
||||
@@ -94,7 +98,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Ban}}"
|
||||
IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n MaxAccessCountReached}" />
|
||||
AutomationProperties.Name="{u:I18n MaxAccessCountReached}"
|
||||
AutomationId="SendMaxAccessCountReachedLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="4"
|
||||
Grid.Row="0"
|
||||
@@ -105,7 +110,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clock}}"
|
||||
IsVisible="{Binding Send.Expired, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Expired}" />
|
||||
AutomationProperties.Name="{u:I18n Expired}"
|
||||
AutomationId="ExpiredSendLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="5"
|
||||
Grid.Row="0"
|
||||
@@ -116,7 +122,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||
IsVisible="{Binding Send.PendingDelete, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n PendingDelete}" />
|
||||
AutomationProperties.Name="{u:I18n PendingDelete}"
|
||||
AutomationId="SendWithPendingDeletionLabel" />
|
||||
</Grid>
|
||||
|
||||
<controls:MiButton
|
||||
@@ -129,6 +136,7 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="EndAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
AutomationId="SendOptionsButton" />
|
||||
|
||||
</controls:ExtendedGrid>
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="BooleanCustomFieldNameLabel" />
|
||||
<Label
|
||||
Text="{Binding Field.Name, Mode=OneWay}"
|
||||
IsVisible="{Binding IsEditing}"
|
||||
@@ -49,13 +50,15 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="0, 5, 0, 0"
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="BooleanCustomFieldValueLabel" />
|
||||
<Switch
|
||||
IsToggled="{Binding BooleanValue}"
|
||||
IsVisible="{Binding IsEditing}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationId="BooleanCustomFieldValueToggle" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
Text="{Binding Field.Name, Mode=OneWay}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="HiddenCustomFieldNameLabel" />
|
||||
<StackLayout
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
@@ -39,7 +40,8 @@
|
||||
<controls:MonoLabel
|
||||
Text="{Binding ValueText, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
IsVisible="{Binding ShowHiddenValue}" />
|
||||
IsVisible="{Binding ShowHiddenValue}"
|
||||
AutomationId="HiddenCustomFieldValueLabel" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding Field.MaskedValue, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
@@ -54,7 +56,10 @@
|
||||
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding ShowViewHidden}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False">
|
||||
IsTextPredictionEnabled="False"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{Binding Field.Name}"
|
||||
AutomationId="HiddenCustomFieldValueEntry">
|
||||
<Entry.Keyboard>
|
||||
<Keyboard x:FactoryMethod="Create">
|
||||
<x:Arguments>
|
||||
@@ -72,7 +77,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationId="HiddenCustomFieldShowValueButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
|
||||
@@ -29,13 +29,15 @@
|
||||
Text="{Binding Field.Name, Mode=OneWay}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="LinkedCustomFieldNameLabel" />
|
||||
<controls:IconLabel
|
||||
Text="{Binding ValueText, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="LinkedCustomFieldValueLabel" />
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding IsEditing}">
|
||||
@@ -44,7 +46,8 @@
|
||||
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
|
||||
ItemDisplayBinding="{Binding Key}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="LinkedCustomFieldValuePicker" />
|
||||
</StackLayout>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
@@ -55,7 +58,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
AutomationId="LinkedCustomFieldOptionsButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
</StackLayout>
|
||||
|
||||
@@ -29,19 +29,24 @@
|
||||
Text="{Binding Field.Name, Mode=OneWay}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="TextCustomFieldNameLabel" />
|
||||
<Label
|
||||
Text="{Binding ValueText, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="TextCustomFieldValueLabel" />
|
||||
<Entry
|
||||
Text="{Binding Field.Value}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding IsEditing}" />
|
||||
IsVisible="{Binding IsEditing}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{Binding Field.Name}"
|
||||
AutomationId="TextCustomFieldValueEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
@@ -51,7 +56,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Copy}" />
|
||||
AutomationProperties.Name="{u:I18n Copy}"
|
||||
AutomationId="TextCustomFieldCopyValue" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||
@@ -61,7 +67,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
AutomationId="TextCustomFieldOptionsButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
</StackLayout>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text;
|
||||
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;
|
||||
@@ -73,13 +70,13 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _policy, value);
|
||||
}
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string MasterPassword { get; set; }
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
|
||||
public async Task InitAsync(bool forceSync = false)
|
||||
public virtual async Task InitAsync(bool forceSync = false)
|
||||
{
|
||||
if (forceSync)
|
||||
{
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
Placeholder="ex. https://bitwarden.company.com"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="ServerUrlEntry"/>
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n SelfHostedEnvironmentFooter}"
|
||||
@@ -53,7 +54,8 @@
|
||||
x:Name="_webVaultEntry"
|
||||
Text="{Binding WebVaultUrl}"
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="WebVaultUrlEntry"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
@@ -63,7 +65,8 @@
|
||||
x:Name="_apiEntry"
|
||||
Text="{Binding ApiUrl}"
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ApiUrlEntry"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
@@ -73,7 +76,8 @@
|
||||
x:Name="_identityEntry"
|
||||
Text="{Binding IdentityUrl}"
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="IdentityUrlEntry"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
@@ -85,7 +89,8 @@
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="IconsUrlEntry"/>
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n CustomEnvironmentFooter}"
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
|
||||
@@ -18,7 +19,8 @@ namespace Bit.App.Pages
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
|
||||
PageTitle = AppResources.Settings;
|
||||
BaseUrl = _environmentService.BaseUrl;
|
||||
BaseUrl = _environmentService.BaseUrl == EnvironmentUrlData.DefaultEU.Base || EnvironmentUrlData.DefaultUS.Base == _environmentService.BaseUrl ?
|
||||
string.Empty : _environmentService.BaseUrl;
|
||||
WebVaultUrl = _environmentService.WebVaultUrl;
|
||||
ApiUrl = _environmentService.ApiUrl;
|
||||
IdentityUrl = _environmentService.IdentityUrl;
|
||||
|
||||
@@ -23,12 +23,9 @@
|
||||
Priority="-1"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Account}" />
|
||||
AutomationProperties.Name="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
|
||||
<ToolbarItem
|
||||
Icon="cog_environment.png" Clicked="Environment_Clicked" Order="Primary"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
@@ -53,7 +50,9 @@
|
||||
Keyboard="Email"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding ContinueCommand}">
|
||||
ReturnCommand="{Binding ContinueCommand}"
|
||||
AutomationId="EmailAddressEntry"
|
||||
>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Disabled">
|
||||
@@ -66,7 +65,28 @@
|
||||
</Entry>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Margin="0, 16, 0 ,0">
|
||||
Margin="0, 6, 0 ,0">
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer
|
||||
Command="{Binding ShowEnvironmentPickerCommand}" />
|
||||
</StackLayout.GestureRecognizers>
|
||||
<Label
|
||||
Text="{Binding RegionText}"
|
||||
FontSize="13"
|
||||
TextColor="{DynamicResource MutedColor}"
|
||||
VerticalOptions="Center"
|
||||
VerticalTextAlignment="Center"/>
|
||||
<controls:IconLabel
|
||||
Text="{Binding SelectedEnvironmentName}"
|
||||
FontSize="13"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
VerticalOptions="Center"
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="RegionSelectorDropdown"/>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Margin="0, 20, 0 ,0">
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer
|
||||
Command="{Binding RememberEmailCommand}" />
|
||||
@@ -76,21 +96,27 @@
|
||||
StyleClass="text-sm"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="Center"
|
||||
VerticalTextAlignment="Center"/>
|
||||
VerticalTextAlignment="Center"
|
||||
/>
|
||||
<Switch
|
||||
Scale="0.8"
|
||||
IsToggled="{Binding RememberEmail}"
|
||||
VerticalOptions="Center"/>
|
||||
VerticalOptions="Center"
|
||||
AutomationId="RememberMeSwitch"
|
||||
/>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n Continue}"
|
||||
StyleClass="btn-primary"
|
||||
IsEnabled="{Binding CanContinue}"
|
||||
Command="{Binding ContinueCommand}" />
|
||||
Command="{Binding ContinueCommand}"
|
||||
AutomationId="ContinueButton"
|
||||
/>
|
||||
|
||||
<Label FormattedText="{Binding CreateAccountText}"
|
||||
Margin="0, 10"
|
||||
StyleClass="box-footer-label">
|
||||
StyleClass="box-footer-label"
|
||||
AutomationId="CreateAccountLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding CreateAccountCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
@@ -116,5 +142,4 @@
|
||||
MainPage="{Binding Source={x:Reference _page}}"
|
||||
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
|
||||
</AbsoluteLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -15,6 +15,8 @@ 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");
|
||||
@@ -70,6 +72,14 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
});
|
||||
try
|
||||
{
|
||||
await _vm.UpdateEnvironment();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value?.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnBackButtonPressed()
|
||||
@@ -128,14 +138,6 @@ 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();
|
||||
|
||||
@@ -4,29 +4,33 @@ 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.Services;
|
||||
using Bit.Core.Models.Data;
|
||||
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 bool _isEmailEnabled;
|
||||
private bool _canLogin;
|
||||
private IPlatformUtilsService _platformUtilsService;
|
||||
private ILogger _logger;
|
||||
private IEnvironmentService _environmentService;
|
||||
private IAccountsManager _accountManager;
|
||||
private string _selectedEnvironmentName;
|
||||
private bool _displayEuEnvironment;
|
||||
|
||||
public HomeViewModel()
|
||||
{
|
||||
@@ -36,6 +40,7 @@ namespace Bit.App.Pages
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||
_accountManager = ServiceContainer.Resolve<IAccountsManager>();
|
||||
_configService = ServiceContainer.Resolve<IConfigService>();
|
||||
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
|
||||
@@ -49,6 +54,8 @@ 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();
|
||||
}
|
||||
|
||||
@@ -71,6 +78,13 @@ 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
|
||||
@@ -101,11 +115,13 @@ 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()
|
||||
@@ -144,5 +160,59 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
Priority="-1"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Account}" />
|
||||
AutomationProperties.Name="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
@@ -45,7 +46,7 @@
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding PinLock}"
|
||||
IsVisible="{Binding PinEnabled}"
|
||||
Padding="0, 10, 0, 0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -71,7 +72,8 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="PinEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -81,12 +83,13 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="PinVisibilityToggle" />
|
||||
</Grid>
|
||||
<Grid
|
||||
x:Name="_passwordGrid"
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding PinLock, Converter={StaticResource inverseBool}}"
|
||||
IsVisible="{Binding PinEnabled, Converter={StaticResource inverseBool}}"
|
||||
Padding="0, 10, 0, 0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -111,7 +114,8 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="MasterPasswordEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -121,7 +125,9 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="PasswordVisibilityToggle"
|
||||
/>
|
||||
</Grid>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
@@ -137,7 +143,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 0">
|
||||
<Label
|
||||
Text="{u:I18n BiometricInvalidated}"
|
||||
Text="{u:I18n AccountBiometricInvalidated}"
|
||||
StyleClass="box-footer-label,text-danger,text-bold"
|
||||
IsVisible="{Binding BiometricIntegrityValid, Converter={StaticResource inverseBool}}" />
|
||||
<Button Text="{Binding BiometricButtonText}" Clicked="Biometric_Clicked"
|
||||
@@ -147,7 +153,8 @@
|
||||
x:Name="_unlockButton"
|
||||
Text="{u:I18n Unlock}"
|
||||
StyleClass="btn-primary"
|
||||
Clicked="Unlock_Clicked" />
|
||||
Clicked="Unlock_Clicked"
|
||||
AutomationId="UnlockVaultButton" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -20,13 +20,14 @@ namespace Bit.App.Pages
|
||||
private bool _promptedAfterResume;
|
||||
private bool _appeared;
|
||||
|
||||
public LockPage(AppOptions appOptions = null, bool autoPromptBiometric = true)
|
||||
public LockPage(AppOptions appOptions = null, bool autoPromptBiometric = true, bool checkPendingAuthRequests = true)
|
||||
{
|
||||
_appOptions = appOptions;
|
||||
_autoPromptBiometric = autoPromptBiometric;
|
||||
InitializeComponent();
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
||||
_vm = BindingContext as LockPageViewModel;
|
||||
_vm.CheckPendingAuthRequests = checkPendingAuthRequests;
|
||||
_vm.Page = this;
|
||||
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
||||
|
||||
@@ -44,7 +45,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_vm?.PinLock ?? false)
|
||||
if (_vm?.PinEnabled ?? false)
|
||||
{
|
||||
return _pin;
|
||||
}
|
||||
@@ -54,7 +55,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task PromptBiometricAfterResumeAsync()
|
||||
{
|
||||
if (_vm.BiometricLock)
|
||||
if (_vm.BiometricEnabled)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
if (!_promptedAfterResume)
|
||||
@@ -91,13 +92,13 @@ namespace Bit.App.Pages
|
||||
|
||||
_vm.FocusSecretEntry += PerformFocusSecretEntry;
|
||||
|
||||
if (!_vm.BiometricLock)
|
||||
if (!_vm.BiometricEnabled)
|
||||
{
|
||||
RequestFocus(SecretEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_vm.UsingKeyConnector && !_vm.PinLock)
|
||||
if (!_vm.HasMasterPassword && !_vm.PinEnabled)
|
||||
{
|
||||
_passwordGrid.IsVisible = false;
|
||||
_unlockButton.IsVisible = false;
|
||||
|
||||
@@ -27,25 +27,27 @@ namespace Bit.App.Pages
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IBiometricService _biometricService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IWatchDeviceService _watchDeviceService;
|
||||
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
|
||||
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly ISyncService _syncService;
|
||||
private string _email;
|
||||
private string _masterPassword;
|
||||
private string _pin;
|
||||
private bool _showPassword;
|
||||
private bool _pinLock;
|
||||
private bool _biometricLock;
|
||||
private PinLockType _pinStatus;
|
||||
private bool _pinEnabled;
|
||||
private bool _biometricEnabled;
|
||||
private bool _biometricIntegrityValid = true;
|
||||
private bool _biometricButtonVisible;
|
||||
private bool _usingKeyConnector;
|
||||
private bool _hasMasterPassword;
|
||||
private string _biometricButtonText;
|
||||
private string _loggedInAsText;
|
||||
private string _lockedVerifyText;
|
||||
private bool _isPinProtected;
|
||||
private bool _isPinProtectedWithKey;
|
||||
|
||||
public LockPageViewModel()
|
||||
{
|
||||
@@ -58,9 +60,13 @@ namespace Bit.App.Pages
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>();
|
||||
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||
|
||||
PageTitle = AppResources.VerifyMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@@ -96,21 +102,21 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
|
||||
public bool PinLock
|
||||
public bool PinEnabled
|
||||
{
|
||||
get => _pinLock;
|
||||
set => SetProperty(ref _pinLock, value);
|
||||
get => _pinEnabled;
|
||||
set => SetProperty(ref _pinEnabled, value);
|
||||
}
|
||||
|
||||
public bool UsingKeyConnector
|
||||
public bool HasMasterPassword
|
||||
{
|
||||
get => _usingKeyConnector;
|
||||
get => _hasMasterPassword;
|
||||
}
|
||||
|
||||
public bool BiometricLock
|
||||
public bool BiometricEnabled
|
||||
{
|
||||
get => _biometricLock;
|
||||
set => SetProperty(ref _biometricLock, value);
|
||||
get => _biometricEnabled;
|
||||
set => SetProperty(ref _biometricEnabled, value);
|
||||
}
|
||||
|
||||
public bool BiometricIntegrityValid
|
||||
@@ -143,6 +149,8 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _lockedVerifyText, value);
|
||||
}
|
||||
|
||||
public bool CheckPendingAuthRequests { get; set; }
|
||||
|
||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
@@ -158,18 +166,32 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
(_isPinProtected, _isPinProtectedWithKey) = await _vaultTimeoutService.IsPinLockSetAsync();
|
||||
PinLock = (_isPinProtected && await _stateService.GetPinProtectedKeyAsync() != null) ||
|
||||
_isPinProtectedWithKey;
|
||||
BiometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _cryptoService.HasKeyAsync();
|
||||
|
||||
// Users with key connector and without biometric or pin has no MP to unlock with
|
||||
_usingKeyConnector = await _keyConnectorService.GetUsesKeyConnector();
|
||||
if (_usingKeyConnector && !(BiometricLock || PinLock))
|
||||
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||
if (pendingRequest != null && CheckPendingAuthRequests)
|
||||
{
|
||||
await _vaultTimeoutService.LogOutAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_pinStatus = await _vaultTimeoutService.GetPinLockTypeAsync();
|
||||
|
||||
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
|
||||
?? await _stateService.GetPinProtectedKeyAsync();
|
||||
PinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) ||
|
||||
_pinStatus == PinLockType.Persistent;
|
||||
BiometricEnabled = await _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))
|
||||
{
|
||||
@@ -184,28 +206,20 @@ namespace Bit.App.Pages
|
||||
}
|
||||
var webVaultHostname = CoreHelpers.GetHostname(webVault);
|
||||
LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, webVaultHostname);
|
||||
if (PinLock)
|
||||
if (PinEnabled)
|
||||
{
|
||||
PageTitle = AppResources.VerifyPIN;
|
||||
LockedVerifyText = AppResources.VaultLockedPIN;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_usingKeyConnector)
|
||||
{
|
||||
PageTitle = AppResources.UnlockVault;
|
||||
LockedVerifyText = AppResources.VaultLockedIdentity;
|
||||
}
|
||||
else
|
||||
{
|
||||
PageTitle = AppResources.VerifyMasterPassword;
|
||||
LockedVerifyText = AppResources.VaultLockedMasterPassword;
|
||||
}
|
||||
PageTitle = _hasMasterPassword ? AppResources.VerifyMasterPassword : AppResources.UnlockVault;
|
||||
LockedVerifyText = _hasMasterPassword ? AppResources.VaultLockedMasterPassword : AppResources.VaultLockedIdentity;
|
||||
}
|
||||
|
||||
if (BiometricLock)
|
||||
if (BiometricEnabled)
|
||||
{
|
||||
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
|
||||
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
||||
if (!_biometricIntegrityValid)
|
||||
{
|
||||
BiometricButtonVisible = false;
|
||||
@@ -225,14 +239,14 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
if (PinLock && string.IsNullOrWhiteSpace(Pin))
|
||||
if (PinEnabled && string.IsNullOrWhiteSpace(Pin))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.PIN),
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (!PinLock && string.IsNullOrWhiteSpace(MasterPassword))
|
||||
if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
@@ -243,34 +257,54 @@ namespace Bit.App.Pages
|
||||
ShowPassword = false;
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
|
||||
if (PinLock)
|
||||
if (PinEnabled)
|
||||
{
|
||||
var failed = true;
|
||||
try
|
||||
{
|
||||
if (_isPinProtected)
|
||||
EncString userKeyPin = null;
|
||||
EncString oldPinProtected = null;
|
||||
if (_pinStatus == PinLockType.Persistent)
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email,
|
||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
|
||||
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
|
||||
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
|
||||
}
|
||||
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,
|
||||
kdfConfig,
|
||||
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);
|
||||
}
|
||||
oldPinProtected
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email, kdfConfig);
|
||||
failed = false;
|
||||
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)
|
||||
{
|
||||
Pin = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -291,26 +325,31 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdfConfig);
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig);
|
||||
var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync();
|
||||
var passwordValid = false;
|
||||
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
|
||||
|
||||
if (storedKeyHash != null)
|
||||
{
|
||||
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, key);
|
||||
// Offline unlock possible
|
||||
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Online unlock required
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
|
||||
var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.ServerAuthorization);
|
||||
var request = new PasswordVerificationRequest();
|
||||
request.MasterPasswordHash = keyHash;
|
||||
|
||||
try
|
||||
{
|
||||
await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
var response = await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
|
||||
passwordValid = true;
|
||||
var localKeyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
|
||||
await _cryptoService.SetKeyHashAsync(localKeyHash);
|
||||
var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.LocalAuthorization);
|
||||
await _cryptoService.SetMasterKeyHashAsync(localKeyHash);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -320,20 +359,22 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (passwordValid)
|
||||
{
|
||||
if (_isPinProtected)
|
||||
if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions))
|
||||
{
|
||||
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));
|
||||
// Save the ForcePasswordResetReason to force a password reset after unlock
|
||||
await _stateService.SetForcePasswordResetReasonAsync(
|
||||
ForcePasswordResetReason.WeakMasterPasswordOnLogin);
|
||||
}
|
||||
|
||||
MasterPassword = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
|
||||
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
||||
await _cryptoService.SetMasterKeyAsync(masterKey);
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
|
||||
// Re-enable biometrics
|
||||
if (BiometricLock & !BiometricIntegrityValid)
|
||||
if (BiometricEnabled & !BiometricIntegrityValid)
|
||||
{
|
||||
await _biometricService.SetupBiometricAsync();
|
||||
}
|
||||
@@ -352,6 +393,37 @@ 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,
|
||||
@@ -379,39 +451,43 @@ namespace Bit.App.Pages
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
var secret = PinLock ? Pin : MasterPassword;
|
||||
var secret = PinEnabled ? Pin : MasterPassword;
|
||||
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
|
||||
}
|
||||
|
||||
public async Task PromptBiometricAsync()
|
||||
{
|
||||
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
|
||||
if (!BiometricLock || !BiometricIntegrityValid)
|
||||
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
||||
BiometricButtonVisible = BiometricIntegrityValid;
|
||||
if (!BiometricEnabled || !BiometricIntegrityValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
PinLock ? AppResources.PIN : AppResources.MasterPassword,
|
||||
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
|
||||
await _stateService.SetBiometricLockedAsync(!success);
|
||||
if (success)
|
||||
{
|
||||
await DoContinueAsync();
|
||||
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key)
|
||||
private async Task SetUserKeyAndContinueAsync(UserKey key)
|
||||
{
|
||||
var hasKey = await _cryptoService.HasKeyAsync();
|
||||
var hasKey = await _cryptoService.HasUserKeyAsync();
|
||||
if (!hasKey)
|
||||
{
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
await _cryptoService.SetUserKeyAsync(key);
|
||||
}
|
||||
await _deviceTrustCryptoService.TrustDeviceIfNeededAsync();
|
||||
await DoContinueAsync();
|
||||
}
|
||||
|
||||
private async Task DoContinueAsync()
|
||||
{
|
||||
_syncService.FullSyncAsync(false).FireAndForget();
|
||||
await _stateService.SetBiometricLockedAsync(false);
|
||||
_watchDeviceService.SyncDataToWatchAsync().FireAndForget();
|
||||
_messagingService.Send("unlocked");
|
||||
|
||||
76
src/App/Pages/Accounts/LoginApproveDevicePage.xaml
Normal file
76
src/App/Pages/Accounts/LoginApproveDevicePage.xaml
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.LoginApproveDevicePage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginApproveDeviceViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginApproveDeviceViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<StackLayout Padding="10, 10">
|
||||
<StackLayout Padding="5, 10" Orientation="Horizontal">
|
||||
<StackLayout HorizontalOptions="FillAndExpand">
|
||||
<Label
|
||||
StyleClass="text-md"
|
||||
Text="{u:I18n RememberThisDevice}"/>
|
||||
<Label
|
||||
StyleClass="box-sub-label"
|
||||
Text="{u:I18n TurnOffUsingPublicDevice}"/>
|
||||
</StackLayout>
|
||||
<Switch
|
||||
Scale="0.8"
|
||||
IsToggled="{Binding RememberThisDevice}"
|
||||
VerticalOptions="Center"/>
|
||||
</StackLayout>
|
||||
<StackLayout Margin="0, 20, 0, 0">
|
||||
<Button
|
||||
x:Name="_continue"
|
||||
Text="{u:I18n Continue}"
|
||||
StyleClass="btn-primary"
|
||||
Command="{Binding ContinueCommand}"
|
||||
IsVisible="{Binding 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>
|
||||
|
||||
|
||||
64
src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs
Normal file
64
src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginApproveDevicePage : BaseContentPage
|
||||
{
|
||||
|
||||
private readonly LoginApproveDeviceViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
public LoginApproveDevicePage(AppOptions appOptions = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginApproveDeviceViewModel;
|
||||
_vm.LogInWithMasterPasswordAction = () => StartLogInWithMasterPasswordAsync().FireAndForget();
|
||||
_vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget();
|
||||
_vm.RequestAdminApprovalAction = () => RequestAdminApprovalAsync().FireAndForget();
|
||||
_vm.ContinueToVaultAction = () => ContinueToVaultAsync().FireAndForget();
|
||||
_vm.Page = this;
|
||||
_appOptions = appOptions;
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
_vm.InitAsync();
|
||||
}
|
||||
|
||||
private async Task ContinueToVaultAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
|
||||
private async Task StartLogInWithMasterPasswordAsync()
|
||||
{
|
||||
var page = new LockPage(_appOptions, checkPendingAuthRequests: false);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartLoginWithDeviceAsync()
|
||||
{
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions, true);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task RequestAdminApprovalAsync()
|
||||
{
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AdminApproval, _appOptions, true);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
150
src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs
Normal file
150
src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities.AccountManagement;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginApproveDeviceViewModel : BaseViewModel
|
||||
{
|
||||
private bool _rememberThisDevice;
|
||||
private bool _approveWithMyOtherDeviceEnabled;
|
||||
private bool _requestAdminApprovalEnabled;
|
||||
private bool _approveWithMasterPasswordEnabled;
|
||||
private string _email;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IApiService _apiService;
|
||||
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
|
||||
public ICommand ApproveWithMyOtherDeviceCommand { get; }
|
||||
public ICommand RequestAdminApprovalCommand { get; }
|
||||
public ICommand ApproveWithMasterPasswordCommand { get; }
|
||||
public ICommand ContinueCommand { get; }
|
||||
public ICommand LogoutCommand { get; }
|
||||
|
||||
public Action LogInWithMasterPasswordAction { get; set; }
|
||||
public Action LogInWithDeviceAction { get; set; }
|
||||
public Action RequestAdminApprovalAction { get; set; }
|
||||
public Action ContinueToVaultAction { get; set; }
|
||||
|
||||
public LoginApproveDeviceViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_apiService = ServiceContainer.Resolve<IApiService>();
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_authService = ServiceContainer.Resolve<IAuthService>();
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>();
|
||||
|
||||
PageTitle = AppResources.LogInInitiated;
|
||||
RememberThisDevice = true;
|
||||
|
||||
ApproveWithMyOtherDeviceCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithDeviceAction),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
RequestAdminApprovalCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(RequestAdminApprovalAction),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
ApproveWithMasterPasswordCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithMasterPasswordAction),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
ContinueCommand = new AsyncCommand(CreateNewSsoUserAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
LogoutCommand = new Command(() => _messagingService.Send(AccountsManagerMessageCommands.LOGOUT));
|
||||
}
|
||||
|
||||
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
|
||||
|
||||
public bool RememberThisDevice
|
||||
{
|
||||
get => _rememberThisDevice;
|
||||
set => SetProperty(ref _rememberThisDevice, value);
|
||||
}
|
||||
|
||||
public bool ApproveWithMyOtherDeviceEnabled
|
||||
{
|
||||
get => _approveWithMyOtherDeviceEnabled;
|
||||
set => SetProperty(ref _approveWithMyOtherDeviceEnabled, value);
|
||||
}
|
||||
|
||||
public bool RequestAdminApprovalEnabled
|
||||
{
|
||||
get => _requestAdminApprovalEnabled;
|
||||
set => SetProperty(ref _requestAdminApprovalEnabled, value,
|
||||
additionalPropertyNames: new[] { nameof(IsNewUser) });
|
||||
}
|
||||
|
||||
public bool ApproveWithMasterPasswordEnabled
|
||||
{
|
||||
get => _approveWithMasterPasswordEnabled;
|
||||
set => SetProperty(ref _approveWithMasterPasswordEnabled, value,
|
||||
additionalPropertyNames: new[] { nameof(IsNewUser) });
|
||||
}
|
||||
|
||||
public bool IsNewUser => !RequestAdminApprovalEnabled && !ApproveWithMasterPasswordEnabled;
|
||||
|
||||
public string Email
|
||||
{
|
||||
get => _email;
|
||||
set => SetProperty(ref _email, value, additionalPropertyNames:
|
||||
new string[] {
|
||||
nameof(LoggingInAsText)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Email = await _stateService.GetActiveUserEmailAsync();
|
||||
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
RequestAdminApprovalEnabled = decryptOptions?.TrustedDeviceOption?.HasAdminApproval ?? false;
|
||||
ApproveWithMasterPasswordEnabled = decryptOptions?.HasMasterPassword ?? false;
|
||||
ApproveWithMyOtherDeviceEnabled = decryptOptions?.TrustedDeviceOption?.HasLoginApprovingDevice ?? false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateNewSsoUserAsync()
|
||||
{
|
||||
await _authService.CreateNewSsoUserAsync(await _stateService.GetRememberedOrgIdentifierAsync());
|
||||
if (RememberThisDevice)
|
||||
{
|
||||
await _deviceTrustCryptoService.TrustDeviceAsync();
|
||||
}
|
||||
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
await Device.InvokeOnMainThreadAsync(ContinueToVaultAction);
|
||||
}
|
||||
|
||||
private async Task SetDeviceTrustAndInvokeAsync(Action action)
|
||||
{
|
||||
await _deviceTrustCryptoService.SetShouldTrustDeviceAsync(RememberThisDevice);
|
||||
await Device.InvokeOnMainThreadAsync(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,23 +9,23 @@
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
Title="{Binding PageTitle}"
|
||||
AutomationId="PageTitleLabel">
|
||||
|
||||
<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}" />
|
||||
AutomationProperties.Name="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
@@ -34,7 +34,8 @@
|
||||
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
||||
x:Name="_moreItem" x:Key="moreItem"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
AutomationId="OptionsButton" />
|
||||
<ToolbarItem Text="{u:I18n GetPasswordHint}"
|
||||
x:Key="getPasswordHint"
|
||||
x:Name="_getPasswordHint"
|
||||
@@ -75,7 +76,9 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding LogInCommand}" />
|
||||
ReturnCommand="{Binding LogInCommand}"
|
||||
AutomationId="MasterPasswordEntry"
|
||||
/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -84,6 +87,7 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationId="PasswordVisibilityToggle"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||
<Label
|
||||
@@ -93,7 +97,9 @@
|
||||
Padding="0,5,0,0"
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2">
|
||||
Grid.ColumnSpan="2"
|
||||
AutomationId="GetMasterPasswordHintLabel"
|
||||
>
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="Hint_Clicked" />
|
||||
</Label.GestureRecognizers>
|
||||
@@ -104,19 +110,24 @@
|
||||
<Button x:Name="_loginWithMasterPassword"
|
||||
Text="{u:I18n LogInWithMasterPassword}"
|
||||
StyleClass="btn-primary"
|
||||
Clicked="LogIn_Clicked" />
|
||||
Clicked="LogIn_Clicked"
|
||||
AutomationId="LogInWithMasterPasswordButton"
|
||||
/>
|
||||
<controls:IconLabelButton
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
|
||||
Label="{u:I18n LogInWithAnotherDevice}"
|
||||
ButtonCommand="{Binding LogInWithDeviceCommand}"
|
||||
IsVisible="{Binding IsKnownDevice}"/>
|
||||
IsVisible="{Binding IsKnownDevice}"
|
||||
AutomationId="LogInWithAnotherDeviceButton"
|
||||
/>
|
||||
<controls:IconLabelButton
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Suitcase}}"
|
||||
Label="{u:I18n LogInSso}">
|
||||
Label="{u:I18n LogInSso}"
|
||||
AutomationId="LogInWithSsoButton">
|
||||
<controls:IconLabelButton.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="LogInSSO_Clicked" />
|
||||
</controls:IconLabelButton.GestureRecognizers>
|
||||
@@ -124,12 +135,15 @@
|
||||
<Label
|
||||
Text="{Binding LoggingInAsText}"
|
||||
StyleClass="text-sm"
|
||||
Margin="0,40,0,0"/>
|
||||
Margin="0,40,0,0"
|
||||
AutomationId="LoggingInAsLabel"
|
||||
/>
|
||||
<Label
|
||||
Text="{u:I18n NotYou}"
|
||||
StyleClass="text-md"
|
||||
HorizontalOptions="Start"
|
||||
TextColor="{DynamicResource HyperlinkColor}">
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationId="NotYouLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="Cancel_Clicked" />
|
||||
</Label.GestureRecognizers>
|
||||
|
||||
@@ -4,6 +4,7 @@ using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
@@ -135,7 +136,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task StartLoginWithDeviceAsync()
|
||||
{
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, _appOptions);
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace Bit.App.Pages
|
||||
private string _masterPassword;
|
||||
private bool _isEmailEnabled;
|
||||
private bool _isKnownDevice;
|
||||
private bool _isExecutingLogin;
|
||||
private string _environmentHostName;
|
||||
|
||||
public LoginPageViewModel()
|
||||
{
|
||||
@@ -114,6 +116,16 @@ 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; }
|
||||
@@ -121,7 +133,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.LoggingInAsX, Email);
|
||||
public string LoggingInAsText => string.Format(AppResources.LoggingInAsXOnY, Email, EnvironmentDomainName);
|
||||
public bool IsIosExtension { get; set; }
|
||||
public bool CanRemoveAccount { get; set; }
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
@@ -149,15 +161,22 @@ namespace Bit.App.Pages
|
||||
{
|
||||
Email = await _stateService.GetRememberedEmailAsync();
|
||||
}
|
||||
var deviceIdentifier = await _appIdService.GetAppIdAsync();
|
||||
IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, deviceIdentifier);
|
||||
CanRemoveAccount = await _stateService.GetActiveUserEmailAsync() != Email;
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
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)
|
||||
{
|
||||
_logger.Exception(apiEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogInAsync(bool showLoading = true, bool checkForExistingAccount = false)
|
||||
@@ -192,6 +211,7 @@ namespace Bit.App.Pages
|
||||
ShowPassword = false;
|
||||
try
|
||||
{
|
||||
_isExecutingLogin = true;
|
||||
if (checkForExistingAccount)
|
||||
{
|
||||
var userId = await _stateService.GetUserIdAsync(Email);
|
||||
@@ -253,14 +273,21 @@ namespace Bit.App.Pages
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecutingLogin = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetPasswordField()
|
||||
{
|
||||
try
|
||||
{
|
||||
MasterPassword = string.Empty;
|
||||
ShowPassword = false;
|
||||
if (!_isExecutingLogin)
|
||||
{
|
||||
MasterPassword = string.Empty;
|
||||
ShowPassword = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
<Label
|
||||
Text="{Binding LogInAttemptByLabel}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,24"/>
|
||||
Margin="0,0,0,24"
|
||||
AutomationId="LogInAttemptByLabel" />
|
||||
<Label
|
||||
Text="{u:I18n FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
@@ -41,7 +42,8 @@
|
||||
FormattedText="{Binding LoginRequest.FingerprintPhrase}"
|
||||
FontSize="Medium"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"
|
||||
Margin="0,0,0,27"/>
|
||||
Margin="0,0,0,27"
|
||||
AutomationId="FingerprintValueLabel" />
|
||||
<Label
|
||||
Text="{u:I18n DeviceType}"
|
||||
FontSize="Small"
|
||||
@@ -49,7 +51,8 @@
|
||||
<Label
|
||||
Text="{Binding LoginRequest.DeviceType}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,21"/>
|
||||
Margin="0,0,0,21"
|
||||
AutomationId="DeviceTypeValueLabel" />
|
||||
<Label
|
||||
Text="{u:I18n IpAddress}"
|
||||
IsVisible="{Binding ShowIpAddress}"
|
||||
@@ -59,7 +62,8 @@
|
||||
Text="{Binding LoginRequest.IpAddress}"
|
||||
IsVisible="{Binding ShowIpAddress}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,21"/>
|
||||
Margin="0,0,0,21"
|
||||
AutomationId="IpAddressValueLabel" />
|
||||
<Label
|
||||
Text="{u:I18n Time}"
|
||||
FontSize="Small"
|
||||
@@ -67,7 +71,8 @@
|
||||
<Label
|
||||
Text="{Binding TimeOfRequestText}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,57"/>
|
||||
Margin="0,0,0,57"
|
||||
AutomationId="TimeOfRequestValueLabel" />
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -75,11 +80,13 @@
|
||||
Text="{u:I18n ConfirmLogIn}"
|
||||
Command="{Binding AcceptRequestCommand}"
|
||||
Margin="0,0,0,17"
|
||||
StyleClass="btn-primary"/>
|
||||
StyleClass="btn-primary"
|
||||
AutomationId="ConfirmLoginButton" />
|
||||
<Button
|
||||
Text="{u:I18n DenyLogIn}"
|
||||
Command="{Binding RejectRequestCommand}"
|
||||
StyleClass="btn-secundary"/>
|
||||
StyleClass="btn-secundary"
|
||||
AutomationId="DenyLoginButton" />
|
||||
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -21,16 +21,17 @@
|
||||
<StackLayout
|
||||
Padding="7, 0, 7, 20">
|
||||
<Label
|
||||
Text="{u:I18n LogInInitiated}"
|
||||
Text="{Binding Title}"
|
||||
FontSize="Title"
|
||||
FontAttributes="Bold"
|
||||
Margin="0,14,0,21"/>
|
||||
Margin="0,14,0,21"
|
||||
AutomationId="LogInInitiatedLabel" />
|
||||
<Label
|
||||
Text="{u:I18n ANotificationHasBeenSentToYourDevice}"
|
||||
Text="{Binding SubTitle}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,10"/>
|
||||
<Label
|
||||
Text="{u:I18n PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice}"
|
||||
Text="{Binding Description}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,24"/>
|
||||
<Label
|
||||
@@ -39,38 +40,39 @@
|
||||
FontAttributes="Bold"/>
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding FingerprintPhrase}"
|
||||
FontSize="Medium"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"/>
|
||||
FontSize="Small"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"
|
||||
AutomationId="FingerprintPhraseValue" />
|
||||
<Label
|
||||
Text="{u:I18n ResendNotification}"
|
||||
StyleClass="text-md"
|
||||
IsVisible="{Binding ResendNotificationVisible}"
|
||||
StyleClass="text-sm"
|
||||
FontAttributes="Bold"
|
||||
HorizontalOptions="Start"
|
||||
Margin="0,40,0,0"
|
||||
TextColor="{DynamicResource HyperlinkColor}">
|
||||
Margin="0,24,0,0"
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationId="ResendNotificationButton">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding CreatePasswordlessLoginCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Margin="0,30,0,0">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</ScrollView>
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -12,13 +13,15 @@ namespace Bit.App.Pages
|
||||
private LoginPasswordlessRequestViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
public LoginPasswordlessRequestPage(string email, AppOptions appOptions = null)
|
||||
public LoginPasswordlessRequestPage(string email, AuthRequestType authRequestType, AppOptions appOptions = null, bool authingWithSso = false)
|
||||
{
|
||||
InitializeComponent();
|
||||
_appOptions = appOptions;
|
||||
_vm = BindingContext as LoginPasswordlessRequestViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.Email = email;
|
||||
_vm.AuthRequestType = authRequestType;
|
||||
_vm.AuthingWithSso = authingWithSso;
|
||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -11,7 +12,9 @@ using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
@@ -32,6 +35,9 @@ namespace Bit.App.Pages
|
||||
private IPlatformUtilsService _platformUtilsService;
|
||||
private IEnvironmentService _environmentService;
|
||||
private ILogger _logger;
|
||||
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
|
||||
protected override II18nService i18nService => _i18nService;
|
||||
protected override IEnvironmentService environmentService => _environmentService;
|
||||
@@ -44,6 +50,7 @@ namespace Bit.App.Pages
|
||||
private string _email;
|
||||
private string _requestId;
|
||||
private string _requestAccessCode;
|
||||
private AuthRequestType _authRequestType;
|
||||
// Item1 publicKey, Item2 privateKey
|
||||
private Tuple<byte[], byte[]> _requestKeyPair;
|
||||
|
||||
@@ -57,8 +64,9 @@ namespace Bit.App.Pages
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>();
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
|
||||
PageTitle = AppResources.LogInWithAnotherDevice;
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>();
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
||||
|
||||
CreatePasswordlessLoginCommand = new AsyncCommand(CreatePasswordlessLoginAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
@@ -73,10 +81,91 @@ namespace Bit.App.Pages
|
||||
public Action LogInSuccessAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public bool AuthingWithSso { get; set; }
|
||||
|
||||
public ICommand CreatePasswordlessLoginCommand { get; }
|
||||
public ICommand CloseCommand { get; }
|
||||
|
||||
public string HeaderTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.LogInWithDevice;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.LogInInitiated;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string Title
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.LogInInitiated;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.AdminApprovalRequested;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string SubTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.ANotificationHasBeenSentToYourDevice;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.YourRequestHasBeenSentToYourAdmin;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.YouWillBeNotifiedOnceApproved;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string OtherOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.LogInWithDeviceMustBeSetUpInTheSettingsOfTheBitwardenAppNeedAnotherOption;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.TroubleLoggingIn;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string FingerprintPhrase
|
||||
{
|
||||
get => _fingerprintPhrase;
|
||||
@@ -89,6 +178,25 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _email, value);
|
||||
}
|
||||
|
||||
public AuthRequestType AuthRequestType
|
||||
{
|
||||
get => _authRequestType;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _authRequestType, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(Title),
|
||||
nameof(SubTitle),
|
||||
nameof(Description),
|
||||
nameof(OtherOptions),
|
||||
nameof(ResendNotificationVisible)
|
||||
});
|
||||
PageTitle = HeaderTitle;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ResendNotificationVisible => AuthRequestType == AuthRequestType.AuthenticateAndUnlock;
|
||||
|
||||
public void StartCheckLoginRequestStatus()
|
||||
{
|
||||
try
|
||||
@@ -119,25 +227,39 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task CheckLoginRequestStatus()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_requestAccessCode))
|
||||
if (string.IsNullOrEmpty(_requestId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _authService.GetPasswordlessLoginResponseAsync(_requestId, _requestAccessCode);
|
||||
PasswordlessLoginResponse response = null;
|
||||
if (AuthingWithSso)
|
||||
{
|
||||
response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode);
|
||||
}
|
||||
|
||||
if (response.RequestApproved == null || !response.RequestApproved.Value)
|
||||
if (response?.RequestApproved != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopCheckLoginRequestStatus();
|
||||
|
||||
var authResult = await _authService.LogInPasswordlessAsync(Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
|
||||
var authResult = await _authService.LogInPasswordlessAsync(AuthingWithSso, Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
|
||||
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
await HandleLoginCompleteAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus))
|
||||
{
|
||||
return;
|
||||
@@ -153,10 +275,13 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
LogInSuccessAction?.Invoke();
|
||||
await HandleLoginCompleteAsync();
|
||||
}
|
||||
}
|
||||
catch (ApiException ex) when (ex.Error?.StatusCode == System.Net.HttpStatusCode.BadRequest)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StartCheckLoginRequestStatus();
|
||||
@@ -164,30 +289,65 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleLoginCompleteAsync()
|
||||
{
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
LogInSuccessAction?.Invoke();
|
||||
}
|
||||
|
||||
private async Task CreatePasswordlessLoginAsync()
|
||||
{
|
||||
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
|
||||
|
||||
var response = await _authService.PasswordlessCreateLoginRequestAsync(_email);
|
||||
if (response != null)
|
||||
PasswordlessLoginResponse response = null;
|
||||
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||
if (pendingRequest != null && _authRequestType == AuthRequestType.AdminApproval)
|
||||
{
|
||||
FingerprintPhrase = response.RequestFingerprint;
|
||||
_requestId = response.Id;
|
||||
_requestAccessCode = response.RequestAccessCode;
|
||||
_requestKeyPair = response.RequestKeyPair;
|
||||
response = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
|
||||
if (response == null || (response.IsAnswered && !response.RequestApproved.Value))
|
||||
{
|
||||
// handle pending auth request not valid remove it from state
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
pendingRequest = null;
|
||||
response = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Derive pubKey from privKey in state to avoid MITM attacks
|
||||
// Also generate FingerprintPhrase locally for the same reason
|
||||
var derivedPublicKey = await _cryptoFunctionService.RsaExtractPublicKeyAsync(pendingRequest.PrivateKey);
|
||||
response.FingerprintPhrase = string.Join("-", await _cryptoService.GetFingerprintAsync(Email, derivedPublicKey));
|
||||
response.RequestKeyPair = new Tuple<byte[], byte[]>(derivedPublicKey, pendingRequest.PrivateKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
response = await _authService.PasswordlessCreateLoginRequestAsync(_email, AuthRequestType);
|
||||
}
|
||||
|
||||
await HandlePasswordlessLoginAsync(response, pendingRequest == null && _authRequestType == AuthRequestType.AdminApproval);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
|
||||
private void HandleException(Exception ex)
|
||||
private async Task HandlePasswordlessLoginAsync(PasswordlessLoginResponse response, bool createPendingAdminRequest)
|
||||
{
|
||||
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
|
||||
if (response == null)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
|
||||
}).FireAndForget();
|
||||
_logger.Exception(ex);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace Bit.App.Pages
|
||||
_vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync());
|
||||
_vm.UpdateTempPasswordAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
_vm.StartDeviceApprovalOptionsAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
@@ -106,10 +108,17 @@ namespace Bit.App.Pages
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartDeviceApprovalOptionsAsync()
|
||||
{
|
||||
var page = new LoginApproveDevicePage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task SsoAuthSuccessAsync()
|
||||
{
|
||||
RestoreAppOptionsFromCopy();
|
||||
await AppHelpers.ClearPreviousPage();
|
||||
|
||||
if (await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
||||
|
||||
@@ -9,6 +9,7 @@ using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
@@ -29,8 +30,11 @@ namespace Bit.App.Pages
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
|
||||
private string _orgIdentifier;
|
||||
private bool _useEphemeralWebBrowserSession;
|
||||
|
||||
public LoginSsoPageViewModel()
|
||||
{
|
||||
@@ -45,7 +49,8 @@ namespace Bit.App.Pages
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_organizationService = ServiceContainer.Resolve<IOrganizationService>();
|
||||
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
||||
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
|
||||
@@ -61,6 +66,7 @@ namespace Bit.App.Pages
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action SsoAuthSuccessAction { get; set; }
|
||||
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
|
||||
@@ -109,7 +115,7 @@ namespace Bit.App.Pages
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
||||
|
||||
var response = await _apiService.PreValidateSso(OrgIdentifier);
|
||||
var response = await _apiService.PreValidateSsoAsync(OrgIdentifier);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(response?.Token))
|
||||
{
|
||||
@@ -121,9 +127,8 @@ namespace Bit.App.Pages
|
||||
|
||||
var ssoToken = response.Token;
|
||||
|
||||
|
||||
var passwordOptions = new PasswordGenerationOptions(true);
|
||||
passwordOptions.Length = 64;
|
||||
var passwordOptions = PasswordGenerationOptions.CreateDefault
|
||||
.WithLength(64);
|
||||
|
||||
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
||||
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
|
||||
@@ -141,10 +146,12 @@ namespace Bit.App.Pages
|
||||
"ssoToken=" + Uri.EscapeDataString(ssoToken);
|
||||
|
||||
WebAuthenticatorResult authResult = null;
|
||||
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
|
||||
new Uri(REDIRECT_URI));
|
||||
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions()
|
||||
{
|
||||
CallbackUrl = new Uri(REDIRECT_URI),
|
||||
Url = new Uri(url),
|
||||
PrefersEphemeralWebBrowserSession = _useEphemeralWebBrowserSession,
|
||||
});
|
||||
|
||||
var code = GetResultCode(authResult, state);
|
||||
if (!string.IsNullOrEmpty(code))
|
||||
@@ -169,6 +176,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
// user canceled
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
// Workaroung for cached expired sso token PM-3551
|
||||
_useEphemeralWebBrowserSession = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -198,28 +207,93 @@ namespace Bit.App.Pages
|
||||
try
|
||||
{
|
||||
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
|
||||
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (response.TwoFactor)
|
||||
{
|
||||
StartTwoFactorAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
else if (response.ResetMasterPassword)
|
||||
|
||||
// Trusted device option is sent regardless if this is a trusted device or not
|
||||
// If it is trusted, it will have the necessary keys
|
||||
if (decryptOptions?.TrustedDeviceOption != null)
|
||||
{
|
||||
if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
|
||||
{
|
||||
// If we have a device key but no keys on server, we need to remove the device key
|
||||
if (decryptOptions.TrustedDeviceOption.EncryptedPrivateKey == null && decryptOptions.TrustedDeviceOption.EncryptedUserKey == null)
|
||||
{
|
||||
await _deviceTrustCryptoService.RemoveTrustedDeviceAsync();
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||
if (!decryptOptions.HasMasterPassword &&
|
||||
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
// Update temp password only if the device is trusted and therefore has a decrypted User Key set
|
||||
if (response.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
// Device is trusted and has keys, so we can decrypt
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for pending Admin Auth requests before navigating to device approval options
|
||||
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||
if (pendingRequest != null)
|
||||
{
|
||||
var authRequest = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
|
||||
if (authRequest?.RequestApproved == true)
|
||||
{
|
||||
var authResult = await _authService.LogInPasswordlessAsync(true, await _stateService.GetActiveUserEmailAsync(), authRequest.RequestAccessCode, pendingRequest.Id, pendingRequest.PrivateKey, authRequest.Key, authRequest.MasterPasswordHash);
|
||||
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
await Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(
|
||||
() => _platformUtilsService.ShowToast("info", null, AppResources.LoginApproved));
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In the standard, non TDE case, a user must set password if they don't
|
||||
// have one and they aren't using key connector.
|
||||
// Note: TDE & Key connector are mutually exclusive org config options.
|
||||
if (response.ResetMasterPassword || (decryptOptions?.RequireSetPassword == true))
|
||||
{
|
||||
// TODO: We need to look into how to handle this when Org removes TDE
|
||||
// Will we have the User Key by now to set a new password?
|
||||
StartSetPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
else if (response.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
}
|
||||
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
x:Name="_email"
|
||||
Text="{Binding Email}"
|
||||
Keyboard="Email"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="EmailAddressEntry"/>
|
||||
</StackLayout>
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -59,7 +60,8 @@
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="MasterPasswordEntry"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -69,7 +71,8 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="PasswordVisibilityToggle"/>
|
||||
</Grid>
|
||||
<Label
|
||||
StyleClass="box-sub-label"
|
||||
@@ -109,7 +112,8 @@
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="ConfirmMasterPasswordEntry"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -118,6 +122,7 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationId="ConfirmPasswordVisibilityToggle"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</Grid>
|
||||
@@ -130,7 +135,8 @@
|
||||
Text="{Binding Hint}"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="MasterPasswordHintLabel" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHintDescription}"
|
||||
@@ -142,7 +148,8 @@
|
||||
IsToggled="{Binding CheckExposedMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="Start"
|
||||
Margin="0, 0, 10, 0"/>
|
||||
Margin="0, 0, 10, 0"
|
||||
AutomationId="CheckExposedMasterPasswordToggle"/>
|
||||
<Label
|
||||
Text="{u:I18n CheckKnownDataBreachesForThisPassword}"
|
||||
StyleClass="box-footer-label"
|
||||
@@ -154,7 +161,8 @@
|
||||
IsToggled="{Binding AcceptPolicies}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="Start"
|
||||
Margin="0, 0, 10, 0"/>
|
||||
Margin="0, 0, 10, 0"
|
||||
AutomationId="AcceptPoliciesToggle"/>
|
||||
<Label StyleClass="box-footer-label"
|
||||
HorizontalOptions="Fill">
|
||||
<Label.FormattedText>
|
||||
|
||||
@@ -177,25 +177,28 @@ namespace Bit.App.Pages
|
||||
Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
|
||||
Email = Email.Trim().ToLower();
|
||||
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, Email, kdfConfig);
|
||||
var encKey = await _cryptoService.MakeEncKeyAsync(key);
|
||||
var hashedPassword = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1);
|
||||
var newMasterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, Email, kdfConfig);
|
||||
var (newUserKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(
|
||||
newMasterKey,
|
||||
await _cryptoService.MakeUserKeyAsync()
|
||||
);
|
||||
var hashedPassword = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey);
|
||||
var (newPublicKey, newProtectedPrivateKey) = await _cryptoService.MakeKeyPairAsync(newUserKey);
|
||||
var request = new RegisterRequest
|
||||
{
|
||||
Email = Email,
|
||||
Name = Name,
|
||||
MasterPasswordHash = hashedPassword,
|
||||
MasterPasswordHint = Hint,
|
||||
Key = encKey.Item2.EncryptedString,
|
||||
Key = newProtectedUserKey.EncryptedString,
|
||||
Kdf = kdfConfig.Type,
|
||||
KdfIterations = kdfConfig.Iterations,
|
||||
KdfMemory = kdfConfig.Memory,
|
||||
KdfParallelism = kdfConfig.Parallelism,
|
||||
Keys = new KeysRequest
|
||||
{
|
||||
PublicKey = keys.Item1,
|
||||
EncryptedPrivateKey = keys.Item2.EncryptedString
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newProtectedPrivateKey.EncryptedString
|
||||
},
|
||||
CaptchaResponse = _captchaToken,
|
||||
};
|
||||
|
||||
@@ -30,14 +30,14 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task Init()
|
||||
{
|
||||
Organization = await _keyConnectorService.GetManagingOrganization();
|
||||
Organization = await _keyConnectorService.GetManagingOrganizationAsync();
|
||||
}
|
||||
|
||||
public async Task MigrateAccount()
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
|
||||
await _keyConnectorService.MigrateUser();
|
||||
await _keyConnectorService.MigrateUserAsync();
|
||||
await _syncService.FullSyncAsync(true);
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
@@ -47,7 +47,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
|
||||
await _apiService.PostLeaveOrganization(Organization.Id);
|
||||
await _apiService.PostLeaveOrganizationAsync(Organization.Id);
|
||||
await _syncService.FullSyncAsync(true);
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
<Label
|
||||
Text="{u:I18n ResetPasswordAutoEnrollInviteWarning}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Start" />
|
||||
HorizontalTextAlignment="Start"
|
||||
AutomationId="ResetPasswordAutoEnrollInviteWarningLabel" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||
@@ -73,7 +74,8 @@
|
||||
<Label
|
||||
Text="{Binding PolicySummary}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Start" />
|
||||
HorizontalTextAlignment="Start"
|
||||
AutomationId="PolicyInEffectLabel" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row">
|
||||
@@ -98,7 +100,8 @@
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="MasterPasswordField" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -108,7 +111,8 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="ToggleMasterPasswordVisibilityButton" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordDescription}"
|
||||
@@ -137,7 +141,8 @@
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="RetypePasswordField" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -147,7 +152,8 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="ToggleRetypePasswordVisibilityButton" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
@@ -158,7 +164,8 @@
|
||||
Text="{Binding Hint}"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="MasterPasswordHintLabel" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHintDescription}"
|
||||
|
||||
@@ -165,26 +165,18 @@ namespace Bit.App.Pages
|
||||
|
||||
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig);
|
||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
|
||||
var localMasterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
|
||||
var newMasterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, email, kdfConfig);
|
||||
var masterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey, HashPurpose.ServerAuthorization);
|
||||
var localMasterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey, HashPurpose.LocalAuthorization);
|
||||
|
||||
Tuple<SymmetricCryptoKey, EncString> encKey;
|
||||
var existingEncKey = await _cryptoService.GetEncKeyAsync();
|
||||
if (existingEncKey == null)
|
||||
{
|
||||
encKey = await _cryptoService.MakeEncKeyAsync(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
encKey = await _cryptoService.RemakeEncKeyAsync(key);
|
||||
}
|
||||
var (newUserKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(newMasterKey,
|
||||
await _cryptoService.GetUserKeyAsync() ?? await _cryptoService.MakeUserKeyAsync());
|
||||
|
||||
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1);
|
||||
var (newPublicKey, newProtectedPrivateKey) = await _cryptoService.MakeKeyPairAsync(newUserKey);
|
||||
var request = new SetPasswordRequest
|
||||
{
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
Key = encKey.Item2.EncryptedString,
|
||||
Key = newProtectedUserKey.EncryptedString,
|
||||
MasterPasswordHint = Hint,
|
||||
Kdf = kdfConfig.Type.GetValueOrDefault(KdfType.PBKDF2_SHA256),
|
||||
KdfIterations = kdfConfig.Iterations.GetValueOrDefault(Constants.Pbkdf2Iterations),
|
||||
@@ -193,8 +185,8 @@ namespace Bit.App.Pages
|
||||
OrgIdentifier = OrgIdentifier,
|
||||
Keys = new KeysRequest
|
||||
{
|
||||
PublicKey = keys.Item1,
|
||||
EncryptedPrivateKey = keys.Item2.EncryptedString
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newProtectedPrivateKey.EncryptedString
|
||||
}
|
||||
};
|
||||
|
||||
@@ -204,19 +196,20 @@ namespace Bit.App.Pages
|
||||
// Set Password and relevant information
|
||||
await _apiService.SetPasswordAsync(request);
|
||||
await _stateService.SetKdfConfigurationAsync(kdfConfig);
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
await _cryptoService.SetKeyHashAsync(localMasterPasswordHash);
|
||||
await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString);
|
||||
await _cryptoService.SetEncPrivateKeyAsync(keys.Item2.EncryptedString);
|
||||
await _cryptoService.SetUserKeyAsync(newUserKey);
|
||||
await _cryptoService.SetMasterKeyAsync(newMasterKey);
|
||||
await _cryptoService.SetMasterKeyHashAsync(localMasterPasswordHash);
|
||||
await _cryptoService.SetMasterKeyEncryptedUserKeyAsync(newProtectedUserKey.EncryptedString);
|
||||
await _cryptoService.SetUserPrivateKeyAsync(newProtectedPrivateKey.EncryptedString);
|
||||
|
||||
if (ResetPasswordAutoEnroll)
|
||||
{
|
||||
// Grab Organization Keys
|
||||
var response = await _apiService.GetOrganizationKeysAsync(OrgId);
|
||||
var publicKey = CoreHelpers.Base64UrlDecode(response.PublicKey);
|
||||
// Grab user's Encryption Key and encrypt with Org Public Key
|
||||
var userEncKey = await _cryptoService.GetEncKeyAsync();
|
||||
var encryptedKey = await _cryptoService.RsaEncryptAsync(userEncKey.Key, publicKey);
|
||||
// Grab User Key and encrypt with Org Public Key
|
||||
var userKey = await _cryptoService.GetUserKeyAsync();
|
||||
var encryptedKey = await _cryptoService.RsaEncryptAsync(userKey.Key, publicKey);
|
||||
// Request
|
||||
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using Bit.App.Controls;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -17,26 +18,29 @@ namespace Bit.App.Pages
|
||||
|
||||
private TwoFactorPageViewModel _vm;
|
||||
private bool _inited;
|
||||
private bool _authingWithSso;
|
||||
private string _orgIdentifier;
|
||||
|
||||
public TwoFactorPage(bool? authingWithSso = false, AppOptions appOptions = null, string orgIdentifier = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
SetActivityIndicator();
|
||||
_authingWithSso = authingWithSso ?? false;
|
||||
_appOptions = appOptions;
|
||||
_orgIdentifier = orgIdentifier;
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_vm = BindingContext as TwoFactorPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.AuthingWithSso = authingWithSso ?? false;
|
||||
_vm.StartSetPasswordAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
_vm.TwoFactorAuthSuccessAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync());
|
||||
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessToMainAsync());
|
||||
_vm.LockAction = () =>
|
||||
Device.BeginInvokeOnMainThread(TwoFactorAuthSuccessWithSSOLocked);
|
||||
_vm.UpdateTempPasswordAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
_vm.StartDeviceApprovalOptionsAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
|
||||
_vm.CloseAction = async () => await Navigation.PopModalAsync();
|
||||
DuoWebView = _duoWebView;
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
@@ -180,21 +184,25 @@ namespace Bit.App.Pages
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task TwoFactorAuthSuccessAsync()
|
||||
private async Task StartDeviceApprovalOptionsAsync()
|
||||
{
|
||||
if (_authingWithSso)
|
||||
var page = new LoginApproveDevicePage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private void TwoFactorAuthSuccessWithSSOLocked()
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
||||
}
|
||||
|
||||
private async Task TwoFactorAuthSuccessToMainAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
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);
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
|
||||
private void Token_TextChanged(object sender, TextChangedEventArgs e)
|
||||
|
||||
@@ -11,6 +11,7 @@ using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Newtonsoft.Json;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
@@ -32,12 +33,12 @@ namespace Bit.App.Pages
|
||||
private readonly IStateService _stateService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IAppIdService _appIdService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private TwoFactorProviderType? _selectedProviderType;
|
||||
private string _totpInstruction;
|
||||
private string _webVaultUrl = "https://vault.bitwarden.com";
|
||||
private bool _authingWithSso = false;
|
||||
private bool _enableContinue = false;
|
||||
private bool _showContinue = true;
|
||||
|
||||
@@ -54,7 +55,9 @@ namespace Bit.App.Pages
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
|
||||
PageTitle = AppResources.TwoStepLogin;
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
@@ -69,6 +72,8 @@ namespace Bit.App.Pages
|
||||
|
||||
public bool Remember { get; set; }
|
||||
|
||||
public bool AuthingWithSso { get; set; }
|
||||
|
||||
public string Token { get; set; }
|
||||
|
||||
public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo ||
|
||||
@@ -118,6 +123,8 @@ namespace Bit.App.Pages
|
||||
public Command SubmitCommand { get; }
|
||||
public ICommand MoreCommand { get; }
|
||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||
public Action LockAction { get; set; }
|
||||
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
@@ -136,8 +143,6 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
_authingWithSso = _authService.AuthingWithSso();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl))
|
||||
{
|
||||
_webVaultUrl = _environmentService.BaseUrl;
|
||||
@@ -315,21 +320,84 @@ namespace Bit.App.Pages
|
||||
|
||||
var task = Task.Run(() => _syncService.FullSyncAsync(true));
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
|
||||
|
||||
if (_authingWithSso && result.ResetMasterPassword)
|
||||
if (decryptOptions?.TrustedDeviceOption != null)
|
||||
{
|
||||
if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
|
||||
{
|
||||
// If we have a device key but no keys on server, we need to remove the device key
|
||||
if (decryptOptions.TrustedDeviceOption.EncryptedPrivateKey == null && decryptOptions.TrustedDeviceOption.EncryptedUserKey == null)
|
||||
{
|
||||
await _deviceTrustCryptoService.RemoveTrustedDeviceAsync();
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||
if (!decryptOptions.HasMasterPassword &&
|
||||
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
// Update temp password only if the device is trusted and therefore has a decrypted User Key set
|
||||
if (result.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
// Device is trusted and has keys, so we can decrypt
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
await TwoFactorAuthSuccessAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for pending Admin Auth requests before navigating to device approval options
|
||||
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||
if (pendingRequest != null)
|
||||
{
|
||||
var authRequest = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
|
||||
if (authRequest?.RequestApproved == true)
|
||||
{
|
||||
var authResult = await _authService.LogInPasswordlessAsync(true, await _stateService.GetActiveUserEmailAsync(), authRequest.RequestAccessCode, pendingRequest.Id, pendingRequest.PrivateKey, authRequest.Key, authRequest.MasterPasswordHash);
|
||||
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
await Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(
|
||||
() => _platformUtilsService.ShowToast("info", null, AppResources.LoginApproved));
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
await TwoFactorAuthSuccessAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In the standard, non TDE case, a user must set password if they don't
|
||||
// have one and they aren't using key connector.
|
||||
// Note: TDE & Key connector are mutually exclusive org config options.
|
||||
if (result.ResetMasterPassword || (decryptOptions?.RequireSetPassword ?? false))
|
||||
{
|
||||
// TODO: We need to look into how to handle this when Org removes TDE
|
||||
// Will we have the User Key by now to set a new password?
|
||||
StartSetPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
else if (result.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
TwoFactorAuthSuccessAction?.Invoke();
|
||||
}
|
||||
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
await TwoFactorAuthSuccessAsync();
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
@@ -398,7 +466,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
Email = _authService.Email,
|
||||
MasterPasswordHash = _authService.MasterPasswordHash,
|
||||
DeviceIdentifier = await _appIdService.GetAppIdAsync()
|
||||
DeviceIdentifier = await _appIdService.GetAppIdAsync(),
|
||||
SsoEmail2FaSessionToken = _authService.SsoEmail2FaSessionToken
|
||||
};
|
||||
await _apiService.PostTwoFactorEmailAsync(request);
|
||||
if (showLoading)
|
||||
@@ -422,5 +491,17 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TwoFactorAuthSuccessAsync()
|
||||
{
|
||||
if (AuthingWithSso && await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
LockAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
TwoFactorAuthSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
@@ -46,9 +46,10 @@
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{u:I18n UpdateMasterPasswordWarning}"
|
||||
Text="{Binding UpdateMasterPasswordWarningText }"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="UpdatePasswordWarningLabel" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||
@@ -71,9 +72,46 @@
|
||||
<Label
|
||||
Text="{Binding PolicySummary}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Start" />
|
||||
HorizontalTextAlignment="Start"
|
||||
AutomationId="PolicySummaryLabel" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row" IsVisible="{Binding RequireCurrentPassword }">
|
||||
<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"
|
||||
AutomationId="MasterPasswordField" />
|
||||
<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}"
|
||||
AutomationId="ToggleMasterPasswordVisibilityButton" />
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -96,7 +134,8 @@
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="NewPasswordField" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -106,7 +145,8 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="NewPasswordVisibilityButton" />
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
@@ -132,7 +172,8 @@
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="RetypePasswordField" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -142,7 +183,8 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="ToggleRetypePasswordVisibilityButton" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
@@ -153,7 +195,8 @@
|
||||
Text="{Binding Hint}"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="MasterPasswordHintLabel" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHintDescription}"
|
||||
|
||||
@@ -1,27 +1,68 @@
|
||||
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 Command(async () => await SubmitAsync());
|
||||
SubmitCommand = new AsyncCommand(SubmitAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
}
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public AsyncCommand 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()
|
||||
{
|
||||
@@ -42,32 +83,46 @@ 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 key and hash new password
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig);
|
||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
// Create new master key and hash new password
|
||||
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, email, kdfConfig);
|
||||
var masterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey);
|
||||
|
||||
// Create new encKey for the User
|
||||
var newEncKey = await _cryptoService.RemakeEncKeyAsync(key);
|
||||
|
||||
// Create request
|
||||
var request = new UpdateTempPasswordRequest
|
||||
{
|
||||
Key = newEncKey.Item2.EncryptedString,
|
||||
NewMasterPasswordHash = masterPasswordHash,
|
||||
MasterPasswordHint = Hint
|
||||
};
|
||||
// Encrypt user key with new master key
|
||||
var (userKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(masterKey);
|
||||
|
||||
// Initiate API action
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.UpdatingPassword);
|
||||
await _apiService.PutUpdateTempPasswordAsync(request);
|
||||
|
||||
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 _deviceActionService.HideLoadingAsync();
|
||||
|
||||
// Clear the force reset password reason
|
||||
await _stateService.SetForcePasswordResetReasonAsync(null);
|
||||
|
||||
_platformUtilsService.ShowToast(null, null, AppResources.UpdatedMasterPassword);
|
||||
|
||||
UpdateTempPasswordSuccessAction?.Invoke();
|
||||
}
|
||||
catch (ApiException e)
|
||||
@@ -85,5 +140,32 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,12 @@ namespace Bit.App.Pages
|
||||
private async Task SaveActivityAsync()
|
||||
{
|
||||
SetServices();
|
||||
if (await _stateService.GetActiveUserIdAsync() == null)
|
||||
{
|
||||
// Fresh install and/or all users logged out won't have an active user, skip saving last active time
|
||||
return;
|
||||
}
|
||||
|
||||
await _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
Clicked="Clear_Clicked"
|
||||
Order="Secondary"
|
||||
x:Name="_clearItem"
|
||||
x:Key="clearItem" />
|
||||
x:Key="clearItem"
|
||||
AutomationId="ClearPasswordList" />
|
||||
<ToolbarItem Icon="more_vert.png"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
@@ -43,7 +44,8 @@
|
||||
Margin="20, 0"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center"></Label>
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoPasswordsDisplayedLabel"></Label>
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding History}"
|
||||
@@ -56,7 +58,8 @@
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="10">
|
||||
ColumnSpacing="10"
|
||||
AutomationId="GeneratedPasswordRow">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -71,12 +74,14 @@
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform, text-html"
|
||||
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}" />
|
||||
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}"
|
||||
AutomationId="GeneratedPasswordValue" />
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}" />
|
||||
Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}"
|
||||
AutomationId="GeneratedPasswordDateLabel" />
|
||||
<controls:IconButton
|
||||
StyleClass="list-row-button, list-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Paste}}"
|
||||
@@ -86,7 +91,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}"
|
||||
AutomationId="CopyPasswordValueButton" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
<Label
|
||||
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="PasswordGeneratorPolicyInEffectLabel" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}"
|
||||
@@ -82,21 +83,24 @@
|
||||
x:Name="lblPassword"
|
||||
StyleClass="text-lg, text-html"
|
||||
Text="{Binding ColoredPassword, Mode=OneWay}"
|
||||
Margin="0, 20" />
|
||||
Margin="0, 20"
|
||||
AutomationId="GeneratedPasswordLabel" />
|
||||
<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}" />
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}"
|
||||
AutomationId="CopyValueButton" />
|
||||
<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}" />
|
||||
AutomationProperties.Name="{u:I18n GeneratePassword}"
|
||||
AutomationId="RegenerateValueButton" />
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsUsername}"
|
||||
StyleClass="box-row"
|
||||
@@ -107,21 +111,24 @@
|
||||
StyleClass="text-lg, text-html"
|
||||
Text="{Binding ColoredUsername, Mode=OneWay}"
|
||||
Margin="0, 20"
|
||||
HorizontalOptions="Start" />
|
||||
HorizontalOptions="Start"
|
||||
AutomationId="GeneratedPasswordLabel" />
|
||||
<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}" />
|
||||
AutomationProperties.Name="{u:I18n CopyUsername}"
|
||||
AutomationId="CopyValueButton" />
|
||||
<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}" />
|
||||
AutomationProperties.Name="{u:I18n GenerateUsername}"
|
||||
AutomationId="RegenerateValueButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"/>
|
||||
<StackLayout StyleClass="box"
|
||||
@@ -135,7 +142,8 @@
|
||||
ItemsSource="{Binding GeneratorTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding GeneratorTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="GeneratorTypePicker" />
|
||||
</StackLayout>
|
||||
<Label Text="{u:I18n Options, Header=True}"
|
||||
StyleClass="box-header, box-header-platform"
|
||||
@@ -161,7 +169,8 @@
|
||||
ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding UsernameTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="UsernameTypePicker" />
|
||||
<Label
|
||||
StyleClass="box-footer-label"
|
||||
Text="{Binding UsernameTypeDescriptionLabel}" />
|
||||
@@ -172,7 +181,8 @@
|
||||
StyleClass="box-label" />
|
||||
<Entry x:Name="_plusAddressedEmailEntry"
|
||||
Text="{Binding PlusAddressedEmail}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="PlusAddressedEmailEntry" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n EmailType}"
|
||||
StyleClass="box-label"
|
||||
@@ -203,7 +213,8 @@
|
||||
<Entry
|
||||
x:Name="_catchAllEmailDomainNameEntry"
|
||||
Text="{Binding CatchAllEmailDomain}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="CatchAllEmailDomainEntry" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n EmailType}"
|
||||
StyleClass="box-label"
|
||||
@@ -236,26 +247,27 @@
|
||||
ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding ForwardedEmailServiceSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
<!--ANONADDY OPTIONS-->
|
||||
<Grid IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
StyleClass="box-value"
|
||||
AutomationId="ServiceTypePicker" />
|
||||
<Grid
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Margin="0,10,0,0"
|
||||
Text="{u:I18n APIAccessToken}"
|
||||
Text="{Binding ForwardedEmailApiSecretLabel}"
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_anonAddyApiAccessTokenEntry"
|
||||
Text="{Binding AnonAddyApiAccessToken}"
|
||||
IsPassword="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"/>
|
||||
Text="{Binding ForwardedEmailApiSecret}"
|
||||
IsPassword="{Binding ShowForwardedEmailApiSecret, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
AutomationId="ForwardedEmailApiSecretEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
||||
Text="{Binding ShowForwardedEmailApiSecret, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"/>
|
||||
Grid.Column="1"
|
||||
AutomationId="ShowForwardedEmailApiSecretButton" />
|
||||
</Grid>
|
||||
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||
Text="{u:I18n DomainNameRequiredParenthesis}"
|
||||
@@ -263,92 +275,9 @@
|
||||
Margin="0,10,0,0"/>
|
||||
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||
x:Name="_anonAddyDomainNameEntry"
|
||||
Text="{Binding AnonAddyDomainName}"
|
||||
StyleClass="box-value"/>
|
||||
<!--FIREFOX RELAY OPTIONS-->
|
||||
<Grid StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.FirefoxRelay}}"
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Text="{u:I18n APIAccessToken}"
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_firefoxRelayApiAccessTokenEntry"
|
||||
Text="{Binding FirefoxRelayApiAccessToken}"
|
||||
StyleClass="box-value"
|
||||
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>
|
||||
Text="{Binding AddyIoDomainName}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="AnonAddyDomainNameEntry" />
|
||||
</StackLayout>
|
||||
<!--RANDOM WORD OPTIONS-->
|
||||
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
|
||||
@@ -359,7 +288,8 @@
|
||||
<Switch
|
||||
IsToggled="{Binding CapitalizeRandomWordUsername}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="CapitalizeRandomWordUsernameToggle" />
|
||||
</Grid>
|
||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
||||
StyleClass="box-row-separator" />
|
||||
@@ -371,7 +301,8 @@
|
||||
<Switch
|
||||
IsToggled="{Binding IncludeNumberRandomWordUsername}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="IncludeNumberRandomWordUsernameToggle" />
|
||||
</Grid>
|
||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
||||
StyleClass="box-row-separator" />
|
||||
@@ -386,7 +317,8 @@
|
||||
x:Name="_passwordTypePicker"
|
||||
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding PasswordTypeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="PasswordTypePicker" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0"
|
||||
Padding="0"
|
||||
@@ -403,12 +335,14 @@
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="NumberOfWordsLabel" />
|
||||
<controls:ExtendedStepper
|
||||
Value="{Binding NumWords}"
|
||||
Maximum="20"
|
||||
Minimum="3"
|
||||
Increment="1" />
|
||||
Increment="1"
|
||||
AutomationId="NumberOfWordsStepper" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
@@ -419,7 +353,8 @@
|
||||
Text="{Binding WordSeparator}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
StyleClass="box-value">
|
||||
StyleClass="box-value"
|
||||
AutomationId="WordSeparatorEntry">
|
||||
<Entry.Effects>
|
||||
<effects:NoEmojiKeyboardEffect />
|
||||
</Entry.Effects>
|
||||
@@ -435,7 +370,8 @@
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.Capitalize,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="CapitalizePassphraseToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -448,7 +384,8 @@
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.IncludeNumber,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="IncludeNumbersToggle" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding IsPassword}">
|
||||
@@ -462,7 +399,8 @@
|
||||
StyleClass="box-sub-label"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
WidthRequest="50" />
|
||||
WidthRequest="50"
|
||||
AutomationId="PasswordLengthLabel" />
|
||||
<controls:ExtendedSlider
|
||||
DragCompleted="LengthSlider_DragCompleted"
|
||||
Value="{Binding Length}"
|
||||
@@ -471,7 +409,8 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
Maximum="128"
|
||||
Minimum="5" />
|
||||
Minimum="5"
|
||||
AutomationId="PasswordLengthSlider" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -488,7 +427,8 @@
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n UppercaseAtoZ}"/>
|
||||
AutomationProperties.Name="{u:I18n UppercaseAtoZ}"
|
||||
AutomationId="UppercaseAtoZToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -505,7 +445,8 @@
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n LowercaseAtoZ}"/>
|
||||
AutomationProperties.Name="{u:I18n LowercaseAtoZ}"
|
||||
AutomationId="LowercaseAtoZToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -522,7 +463,8 @@
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n NumbersZeroToNine}"/>
|
||||
AutomationProperties.Name="{u:I18n NumbersZeroToNine}"
|
||||
AutomationId="NumbersZeroToNineToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -539,7 +481,8 @@
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n SpecialCharacters}"/>
|
||||
AutomationProperties.Name="{u:I18n SpecialCharacters}"
|
||||
AutomationId="SpecialCharactersToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-stepper">
|
||||
@@ -554,12 +497,14 @@
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="MinNumberValueLabel" />
|
||||
<controls:ExtendedStepper
|
||||
Value="{Binding MinNumber}"
|
||||
Maximum="5"
|
||||
Minimum="0"
|
||||
Increment="1" />
|
||||
Increment="1"
|
||||
AutomationId="MinNumberStepper" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-stepper">
|
||||
@@ -574,12 +519,14 @@
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="MinSpecialValueLabel" />
|
||||
<controls:ExtendedStepper
|
||||
Value="{Binding MinSpecial}"
|
||||
Maximum="5"
|
||||
Minimum="0"
|
||||
Increment="1" />
|
||||
Increment="1"
|
||||
AutomationId="MinSpecialStepper" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -590,7 +537,8 @@
|
||||
<Switch
|
||||
IsToggled="{Binding AvoidAmbiguousChars}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="AvoidAmbiguousCharsToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
@@ -23,7 +24,7 @@ namespace Bit.App.Pages
|
||||
private readonly IUsernameGenerationService _usernameGenerationService;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
|
||||
private PasswordGenerationOptions _options;
|
||||
private UsernameGenerationOptions _usernameOptions;
|
||||
@@ -49,11 +50,7 @@ namespace Bit.App.Pages
|
||||
private bool _doneIniting;
|
||||
private bool _showTypePicker;
|
||||
private string _emailWebsite;
|
||||
private bool _showFirefoxRelayApiAccessToken;
|
||||
private bool _showAnonAddyApiAccessToken;
|
||||
private bool _showSimpleLoginApiKey;
|
||||
private bool _showDuckDuckGoApiKey;
|
||||
private bool _showFastmailApiKey;
|
||||
private bool _showForwardedEmailApiSecret;
|
||||
private bool _editMode;
|
||||
|
||||
public GeneratorPageViewModel()
|
||||
@@ -96,7 +93,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 AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
|
||||
ToggleForwardedEmailHiddenValueCommand = new Command(() => ShowForwardedEmailApiSecret = !ShowForwardedEmailApiSecret);
|
||||
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
|
||||
CloseCommand = new AsyncCommand(CloseAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
|
||||
}
|
||||
@@ -415,7 +412,6 @@ namespace Bit.App.Pages
|
||||
|
||||
public string UsernameTypeDescriptionLabel => GetUsernameTypeLabelDescription(UsernameTypeSelected);
|
||||
|
||||
|
||||
public ForwardedEmailServiceType ForwardedEmailServiceSelected
|
||||
{
|
||||
get => _usernameOptions.ServiceType;
|
||||
@@ -425,7 +421,11 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_usernameOptions.ServiceType = value;
|
||||
Username = Constants.DefaultUsernameGenerated;
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected), new string[]
|
||||
{
|
||||
nameof(ForwardedEmailApiSecret),
|
||||
nameof(ForwardedEmailApiSecretLabel)
|
||||
});
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
@@ -445,30 +445,107 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public string AnonAddyApiAccessToken
|
||||
public string ForwardedEmailApiSecret
|
||||
{
|
||||
get => _usernameOptions.AnonAddyApiAccessToken;
|
||||
get
|
||||
{
|
||||
switch (ForwardedEmailServiceSelected)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
return _usernameOptions.AnonAddyApiAccessToken;
|
||||
case ForwardedEmailServiceType.DuckDuckGo:
|
||||
return _usernameOptions.DuckDuckGoApiKey;
|
||||
case ForwardedEmailServiceType.Fastmail:
|
||||
return _usernameOptions.FastMailApiKey;
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
return _usernameOptions.FirefoxRelayApiAccessToken;
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
return _usernameOptions.SimpleLoginApiKey;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.AnonAddyApiAccessToken != value)
|
||||
bool changed = false;
|
||||
switch (ForwardedEmailServiceSelected)
|
||||
{
|
||||
_usernameOptions.AnonAddyApiAccessToken = value;
|
||||
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
|
||||
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));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowAnonAddyApiAccessToken
|
||||
public string ForwardedEmailApiSecretLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showAnonAddyApiAccessToken;
|
||||
switch (ForwardedEmailServiceSelected)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
return AppResources.APIAccessToken;
|
||||
case ForwardedEmailServiceType.DuckDuckGo:
|
||||
case ForwardedEmailServiceType.Fastmail:
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
return AppResources.APIKeyRequiredParenthesis;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
set => SetProperty(ref _showAnonAddyApiAccessToken, value);
|
||||
}
|
||||
|
||||
public string AnonAddyDomainName
|
||||
public bool ShowForwardedEmailApiSecret
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showForwardedEmailApiSecret;
|
||||
}
|
||||
set => SetProperty(ref _showForwardedEmailApiSecret, value);
|
||||
}
|
||||
|
||||
public string AddyIoDomainName
|
||||
{
|
||||
get => _usernameOptions.AnonAddyDomainName;
|
||||
set
|
||||
@@ -476,105 +553,12 @@ namespace Bit.App.Pages
|
||||
if (_usernameOptions.AnonAddyDomainName != value)
|
||||
{
|
||||
_usernameOptions.AnonAddyDomainName = value;
|
||||
TriggerPropertyChanged(nameof(AnonAddyDomainName));
|
||||
TriggerPropertyChanged(nameof(AddyIoDomainName));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string FirefoxRelayApiAccessToken
|
||||
{
|
||||
get => _usernameOptions.FirefoxRelayApiAccessToken;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.FirefoxRelayApiAccessToken != value)
|
||||
{
|
||||
_usernameOptions.FirefoxRelayApiAccessToken = value;
|
||||
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowFirefoxRelayApiAccessToken
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showFirefoxRelayApiAccessToken;
|
||||
}
|
||||
set => SetProperty(ref _showFirefoxRelayApiAccessToken, value);
|
||||
}
|
||||
|
||||
public string SimpleLoginApiKey
|
||||
{
|
||||
get => _usernameOptions.SimpleLoginApiKey;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.SimpleLoginApiKey != value)
|
||||
{
|
||||
_usernameOptions.SimpleLoginApiKey = value;
|
||||
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSimpleLoginApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showSimpleLoginApiKey;
|
||||
}
|
||||
set => SetProperty(ref _showSimpleLoginApiKey, value);
|
||||
}
|
||||
|
||||
public string DuckDuckGoApiKey
|
||||
{
|
||||
get => _usernameOptions.DuckDuckGoApiKey;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.DuckDuckGoApiKey != value)
|
||||
{
|
||||
_usernameOptions.DuckDuckGoApiKey = value;
|
||||
TriggerPropertyChanged(nameof(DuckDuckGoApiKey));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowDuckDuckGoApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showDuckDuckGoApiKey;
|
||||
}
|
||||
set => SetProperty(ref _showDuckDuckGoApiKey, value);
|
||||
}
|
||||
|
||||
|
||||
public string FastmailApiKey
|
||||
{
|
||||
get => _usernameOptions.FastMailApiKey;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.FastMailApiKey != value)
|
||||
{
|
||||
_usernameOptions.FastMailApiKey = value;
|
||||
TriggerPropertyChanged(nameof(FastmailApiKey));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowFastmailApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showFastmailApiKey;
|
||||
}
|
||||
set => SetProperty(ref _showFastmailApiKey, value);
|
||||
}
|
||||
|
||||
public bool CapitalizeRandomWordUsername
|
||||
{
|
||||
get => _usernameOptions.CapitalizeRandomWordUsername;
|
||||
@@ -807,12 +791,9 @@ namespace Bit.App.Pages
|
||||
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
|
||||
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
|
||||
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
|
||||
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
|
||||
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
|
||||
TriggerPropertyChanged(nameof(AnonAddyDomainName));
|
||||
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
|
||||
TriggerPropertyChanged(nameof(DuckDuckGoApiKey));
|
||||
TriggerPropertyChanged(nameof(FastmailApiKey));
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailApiSecret));
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailApiSecretLabel));
|
||||
TriggerPropertyChanged(nameof(AddyIoDomainName));
|
||||
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
|
||||
TriggerPropertyChanged(nameof(UsernameTypeSelected));
|
||||
@@ -827,7 +808,7 @@ namespace Bit.App.Pages
|
||||
private void SetOptions()
|
||||
{
|
||||
_options.AllowAmbiguousChar = AllowAmbiguousChars;
|
||||
_options.Type = PasswordTypeSelectedIndex == 1 ? "passphrase" : "password";
|
||||
_options.Type = PasswordTypeSelectedIndex == 1 ? PasswordGenerationOptions.TYPE_PASSPHRASE : PasswordGenerationOptions.TYPE_PASSWORD;
|
||||
_options.MinNumber = MinNumber;
|
||||
_options.MinSpecial = MinSpecial;
|
||||
_options.Special = Special;
|
||||
@@ -845,15 +826,23 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
|
||||
string message = AppResources.GenericErrorMessage;
|
||||
|
||||
if (IsUsername && UsernameTypeSelected == UsernameType.ForwardedEmailAlias)
|
||||
{
|
||||
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));
|
||||
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, message, AppResources.Ok));
|
||||
}
|
||||
|
||||
private string GetUsernameTypeLabelDescription(UsernameType value)
|
||||
@@ -870,27 +859,5 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
<Label
|
||||
Text="{u:I18n SendDisabledWarning}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="SendDisabledWarningMessageLabel" />
|
||||
</Frame>
|
||||
<Frame
|
||||
IsVisible="{Binding SendOptionsPolicyInEffect}"
|
||||
@@ -83,7 +84,8 @@
|
||||
<Label
|
||||
Text="{u:I18n SendOptionsPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="SendOptionsPolicyInEffectLabel" />
|
||||
</Frame>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
@@ -93,7 +95,8 @@
|
||||
x:Name="_nameEntry"
|
||||
Text="{Binding Send.Name}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="SendNameEntry" />
|
||||
<Label
|
||||
Text="{u:I18n NameInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
@@ -123,6 +126,7 @@
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n File}"
|
||||
AutomationProperties.HelpText="{Binding FileTypeAccessibilityLabel}"
|
||||
AutomationId="SendFileButton"
|
||||
Grid.Column="0">
|
||||
</Button>
|
||||
<Button
|
||||
@@ -135,6 +139,7 @@
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Text}"
|
||||
AutomationProperties.HelpText="{Binding TextTypeAccessibilityLabel}"
|
||||
AutomationId="SendTextButton"
|
||||
Grid.Column="1">
|
||||
</Button>
|
||||
</Grid>
|
||||
@@ -152,12 +157,14 @@
|
||||
Text="{Binding Send.File.FileName, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationId="SendFileNameLabel" />
|
||||
<Label
|
||||
Text="{Binding Send.File.SizeName, Mode=OneWay}"
|
||||
StyleClass="box-sub-label"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="SendFileSizeLabel" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
|
||||
@@ -168,26 +175,29 @@
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Start"
|
||||
AutomationId="SendNoFileChosenLabel" />
|
||||
<Label
|
||||
IsVisible="{Binding FileName, Converter={StaticResource notNull}}"
|
||||
Text="{Binding FileName}"
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Start"
|
||||
AutomationId="SendCurrentFileNameLabel" />
|
||||
<Button
|
||||
Text="{u:I18n ChooseFile}"
|
||||
IsVisible="{Binding IsAddFromShare, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-button-row"
|
||||
Clicked="ChooseFile_Clicked" />
|
||||
Clicked="ChooseFile_Clicked"
|
||||
AutomationId="SendChooseFileButton" />
|
||||
<Label
|
||||
Margin="0, 5, 0, 0"
|
||||
Text="{u:I18n MaxFileSize}"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Start" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n TypeFileInfo}"
|
||||
@@ -207,7 +217,8 @@
|
||||
Text="{Binding Send.Text.Text}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Margin="{Binding EditorMargins}"
|
||||
Margin="{Binding EditorMargins}"
|
||||
AutomationId="SendTextContentEntry"
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
|
||||
<Editor.Behaviors>
|
||||
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
|
||||
@@ -235,22 +246,10 @@
|
||||
IsToggled="{Binding Send.Text.Hidden}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendHideTextByDefaultToggle" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{Binding ShareOnSaveText}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding ShareOnSave}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Spacing="0"
|
||||
@@ -263,21 +262,24 @@
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
Margin="0"
|
||||
AutomationProperties.IsInAccessibleTree="False"/>
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="SendShowHideOptionsButton" />
|
||||
<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"/>
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="SendOptionsDisplayed" />
|
||||
<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"/>
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="SendOptionsHidden" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding ShowOptions}">
|
||||
<StackLayout
|
||||
@@ -294,7 +296,8 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n DeletionTime}" />
|
||||
AutomationProperties.Name="{u:I18n DeletionTime}"
|
||||
AutomationId="SendDeletionOptionsPicker" />
|
||||
<Grid
|
||||
IsVisible="{Binding ShowDeletionCustomPickers}"
|
||||
Margin="0,5,0,0">
|
||||
@@ -308,14 +311,16 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n DeletionDate}"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="SendCustomDeletionDatePicker" />
|
||||
<controls:ExtendedTimePicker
|
||||
NullableTime="{Binding DeletionDateTimeViewModel.Time, Mode=TwoWay}"
|
||||
Format="t"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n DeletionTime}"
|
||||
Grid.Column="1" />
|
||||
Grid.Column="1"
|
||||
AutomationId="SendCustomDeletionTimePicker" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n DeletionDateInfo}"
|
||||
@@ -334,7 +339,8 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ExpirationTime}" />
|
||||
AutomationProperties.Name="{u:I18n ExpirationTime}"
|
||||
AutomationId="SendExpirationOptionsPicker" />
|
||||
<Grid
|
||||
IsVisible="{Binding ShowExpirationCustomPickers}"
|
||||
Margin="0,5,0,0">
|
||||
@@ -349,7 +355,8 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ExpirationDate}"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="SendCustomExpirationDatePicker" />
|
||||
<controls:ExtendedTimePicker
|
||||
NullableTime="{Binding ExpirationDateTimeViewModel.Time, Mode=TwoWay}"
|
||||
PlaceHolder="--:-- --"
|
||||
@@ -357,7 +364,8 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ExpirationTime}"
|
||||
Grid.Column="1" />
|
||||
Grid.Column="1"
|
||||
AutomationId="SendCustomExpirationTimePicker" />
|
||||
</Grid>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
@@ -374,7 +382,8 @@
|
||||
FontSize="{Binding SegmentedButtonFontSize}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-row-button"
|
||||
Clicked="ClearExpirationDate_Clicked" />
|
||||
Clicked="ClearExpirationDate_Clicked"
|
||||
AutomationId="SendClearExpirationDateButton" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
@@ -393,13 +402,15 @@
|
||||
Keyboard="Numeric"
|
||||
MaxLength="9"
|
||||
TextChanged="OnMaxAccessCountTextChanged"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="SendMaxAccessCountEntry" />
|
||||
<controls:ExtendedStepper
|
||||
x:Name="_maxAccessCountStepper"
|
||||
Value="{Binding MaxAccessCount}"
|
||||
Maximum="999999999"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
Margin="10,0,0,0" />
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendMaxAccessCountStepper" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MaximumAccessCountInfo}"
|
||||
@@ -419,7 +430,8 @@
|
||||
<Label
|
||||
Text="{Binding Send.AccessCount, Mode=OneWay}"
|
||||
StyleClass="box-label"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="SendCurrentAccessCountLabel" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
@@ -436,7 +448,8 @@
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="SendNewPasswordEntry" />
|
||||
<controls:IconButton
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
@@ -445,7 +458,8 @@
|
||||
Margin="10,0,0,0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="SendShowHidePasswordButton" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n PasswordInfo}"
|
||||
@@ -464,7 +478,8 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Margin="{Binding EditorMargins}"
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false"
|
||||
AutomationId="SendNotesEntry">
|
||||
<Editor.Behaviors>
|
||||
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
|
||||
</Editor.Behaviors>
|
||||
@@ -492,7 +507,8 @@
|
||||
IsToggled="{Binding Send.HideEmail}"
|
||||
IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendHideEmailSwitch" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
@@ -506,7 +522,8 @@
|
||||
IsToggled="{Binding Send.Disabled}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendDeactivateSwitch" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
|
||||
@@ -127,10 +127,9 @@ namespace Bit.App.Pages
|
||||
public SendType? Type { get; set; }
|
||||
public byte[] FileData { get; set; }
|
||||
public string NewPassword { get; set; }
|
||||
public bool ShareOnSave { get; set; }
|
||||
public bool DisableHideEmailControl { get; set; }
|
||||
public bool IsAddFromShare { get; set; }
|
||||
public string ShareOnSaveText => CopyInsteadOfShareAfterSaving ? AppResources.CopySendLinkOnSave : AppResources.ShareOnSave;
|
||||
public bool CopyInsteadOfShareAfterSaving { get; set; }
|
||||
public string OptionsAccessilibityText => ShowOptions ? AppResources.OptionsExpanded : AppResources.OptionsCollapsed;
|
||||
public List<KeyValuePair<string, SendType>> TypeOptions { get; }
|
||||
public List<KeyValuePair<string, string>> DeletionTypeOptions { get; }
|
||||
@@ -184,15 +183,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool CopyInsteadOfShareAfterSaving
|
||||
{
|
||||
get => _copyInsteadOfShareAfterSaving;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _copyInsteadOfShareAfterSaving, value);
|
||||
TriggerPropertyChanged(nameof(ShareOnSaveText));
|
||||
}
|
||||
}
|
||||
public SendView Send
|
||||
{
|
||||
get => _send;
|
||||
@@ -412,34 +402,25 @@ namespace Bit.App.Pages
|
||||
_messagingService.Send("sendUpdated");
|
||||
}
|
||||
|
||||
if (!ShareOnSave)
|
||||
{
|
||||
_platformUtilsService.ShowToast("success", null,
|
||||
EditMode ? AppResources.SendUpdated : AppResources.NewSendCreated);
|
||||
}
|
||||
|
||||
if (!CopyInsteadOfShareAfterSaving)
|
||||
{
|
||||
await CloseAsync();
|
||||
}
|
||||
|
||||
if (ShareOnSave)
|
||||
var savedSend = await _sendService.GetAsync(sendId);
|
||||
if (savedSend != null)
|
||||
{
|
||||
var savedSend = await _sendService.GetAsync(sendId);
|
||||
if (savedSend != null)
|
||||
var savedSendView = await savedSend.DecryptAsync();
|
||||
if (CopyInsteadOfShareAfterSaving)
|
||||
{
|
||||
var savedSendView = await savedSend.DecryptAsync();
|
||||
if (CopyInsteadOfShareAfterSaving)
|
||||
{
|
||||
await AppHelpers.CopySendUrlAsync(savedSendView);
|
||||
await AppHelpers.CopySendUrlAsync(savedSendView);
|
||||
|
||||
// wait so that the user sees the message before the view gets dismissed
|
||||
await Task.Delay(1300);
|
||||
}
|
||||
else
|
||||
{
|
||||
await AppHelpers.ShareSendUrlAsync(savedSendView);
|
||||
}
|
||||
// wait so that the user sees the message before the view gets dismissed
|
||||
await Task.Delay(1300);
|
||||
}
|
||||
else
|
||||
{
|
||||
await AppHelpers.ShareSendUrlAsync(savedSendView);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
Priority="-2"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Account}" />
|
||||
AutomationProperties.Name="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/>
|
||||
</ContentPage.ToolbarItems>
|
||||
@@ -93,13 +94,13 @@
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Start" />
|
||||
<Label
|
||||
Margin="0, 5, 0, 0"
|
||||
Text="{u:I18n MaxFileSize}"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Start" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
@@ -144,19 +145,6 @@
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{Binding ShareOnSaveText}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding ShareOnSave}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Spacing="0"
|
||||
|
||||
@@ -44,13 +44,15 @@
|
||||
<controls:SendViewCell
|
||||
Send="{Binding Send}"
|
||||
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}"
|
||||
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}" />
|
||||
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}"
|
||||
AutomationId="SendCell" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="sendGroupTemplate"
|
||||
x:DataType="pages:SendGroupingsPageListItem">
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform">
|
||||
StyleClass="list-row, list-row-platform"
|
||||
AutomationId="{Binding AutomationId}">
|
||||
<controls:IconLabel Text="{Binding Icon, Mode=OneWay}"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
@@ -64,12 +66,14 @@
|
||||
LineBreakMode="TailTruncation"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title" />
|
||||
StyleClass="list-title"
|
||||
AutomationId="SendFilterNameLabel" />
|
||||
<Label Text="{Binding ItemCount, Mode=OneWay}"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
StyleClass="list-sub" />
|
||||
StyleClass="list-sub"
|
||||
AutomationId="SendFilterCountLabel" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
|
||||
|
||||
@@ -66,5 +66,27 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@
|
||||
Margin="20, 0"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoSendDisplayedLabel" />
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowList}"
|
||||
ItemsSource="{Binding Sends}"
|
||||
@@ -67,13 +68,15 @@
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Sends Page">
|
||||
ExtraDataForLogging="Sends Page"
|
||||
AutomationId="SendCellList">
|
||||
<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}}" />
|
||||
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}"
|
||||
AutomationId="SendCell" />
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
|
||||
86
src/App/Pages/Settings/BlockAutofillUrisPage.xaml
Normal file
86
src/App/Pages/Settings/BlockAutofillUrisPage.xaml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?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>
|
||||
44
src/App/Pages/Settings/BlockAutofillUrisPage.xaml.cs
Normal file
44
src/App/Pages/Settings/BlockAutofillUrisPage.xaml.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Styles;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class BlockAutofillUrisPage : BaseContentPage, IThemeDirtablePage
|
||||
{
|
||||
private readonly BlockAutofillUrisPageViewModel _vm;
|
||||
|
||||
public BlockAutofillUrisPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_vm = BindingContext as BlockAutofillUrisPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
_vm.InitAsync().FireAndForget(_ => Navigation.PopAsync());
|
||||
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
public override async Task UpdateOnThemeChanged()
|
||||
{
|
||||
await base.UpdateOnThemeChanged();
|
||||
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
private void UpdatePlaceholder()
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
_emptyUrisPlaceholder.Source = ImageSource.FromFile(ThemeManager.UsingLightTheme ? "empty_uris_placeholder" : "empty_uris_placeholder_dark"));
|
||||
}
|
||||
}
|
||||
}
|
||||
186
src/App/Pages/Settings/BlockAutofillUrisPageViewModel.cs
Normal file
186
src/App/Pages/Settings/BlockAutofillUrisPageViewModel.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class BlockAutofillUrisPageViewModel : BaseViewModel
|
||||
{
|
||||
private const char URI_SEPARARTOR = ',';
|
||||
private const string URI_FORMAT = "https://domain.com";
|
||||
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
|
||||
public BlockAutofillUrisPageViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
|
||||
AddUriCommand = new AsyncCommand(AddUriAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
EditUriCommand = new AsyncCommand<BlockAutofillUriItemViewModel>(EditUriAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public ObservableRangeCollection<BlockAutofillUriItemViewModel> BlockedUris { get; set; } = new ObservableRangeCollection<BlockAutofillUriItemViewModel>();
|
||||
|
||||
public bool ShowList => BlockedUris.Any();
|
||||
|
||||
public ICommand AddUriCommand { get; }
|
||||
|
||||
public ICommand EditUriCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
if (blockedUrisList?.Any() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
BlockedUris.AddRange(blockedUrisList.OrderBy(uri => uri).Select(u => new BlockAutofillUriItemViewModel(u, EditUriCommand)).ToList());
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
}
|
||||
|
||||
private async Task AddUriAsync()
|
||||
{
|
||||
var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
|
||||
{
|
||||
Title = AppResources.NewUri,
|
||||
Subtitle = AppResources.EnterURI,
|
||||
ValueSubInfo = string.Format(AppResources.FormatXSeparateMultipleURIsWithAComma, URI_FORMAT),
|
||||
OkButtonText = AppResources.Save,
|
||||
ValidateText = text => ValidateUris(text, true)
|
||||
});
|
||||
if (response?.Text is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
foreach (var uri in response.Value.Text.Split(URI_SEPARARTOR).Where(s => !string.IsNullOrEmpty(s)))
|
||||
{
|
||||
var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
|
||||
BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
|
||||
}
|
||||
|
||||
BlockedUris = new ObservableRangeCollection<BlockAutofillUriItemViewModel>(BlockedUris.OrderBy(b => b.Uri));
|
||||
TriggerPropertyChanged(nameof(BlockedUris));
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
await UpdateAutofillBlacklistedUrisAsync();
|
||||
_deviceActionService.Toast(AppResources.URISaved);
|
||||
}
|
||||
|
||||
private async Task EditUriAsync(BlockAutofillUriItemViewModel uriItemViewModel)
|
||||
{
|
||||
var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
|
||||
{
|
||||
Title = AppResources.EditURI,
|
||||
Subtitle = AppResources.EnterURI,
|
||||
Text = uriItemViewModel.Uri,
|
||||
ValueSubInfo = string.Format(AppResources.FormatX, URI_FORMAT),
|
||||
OkButtonText = AppResources.Save,
|
||||
ThirdButtonText = AppResources.Remove,
|
||||
ValidateText = text => ValidateUris(text, false)
|
||||
});
|
||||
if (response is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Value.ExecuteThirdAction)
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
BlockedUris.Remove(uriItemViewModel);
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
await UpdateAutofillBlacklistedUrisAsync();
|
||||
_deviceActionService.Toast(AppResources.URIRemoved);
|
||||
return;
|
||||
}
|
||||
|
||||
var cleanedUri = response.Value.Text.Replace(Environment.NewLine, string.Empty).Trim();
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
BlockedUris.Remove(uriItemViewModel);
|
||||
BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
|
||||
BlockedUris = new ObservableRangeCollection<BlockAutofillUriItemViewModel>(BlockedUris.OrderBy(b => b.Uri));
|
||||
TriggerPropertyChanged(nameof(BlockedUris));
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
await UpdateAutofillBlacklistedUrisAsync();
|
||||
_deviceActionService.Toast(AppResources.URISaved);
|
||||
}
|
||||
|
||||
private string ValidateUris(string uris, bool allowMultipleUris)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uris))
|
||||
{
|
||||
return string.Format(AppResources.FormatX, URI_FORMAT);
|
||||
}
|
||||
|
||||
if (!allowMultipleUris && uris.Contains(URI_SEPARARTOR))
|
||||
{
|
||||
return AppResources.CannotEditMultipleURIsAtOnce;
|
||||
}
|
||||
|
||||
foreach (var uri in uris.Split(URI_SEPARARTOR).Where(u => !string.IsNullOrWhiteSpace(u)))
|
||||
{
|
||||
var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
|
||||
if (!cleanedUri.StartsWith("http://") && !cleanedUri.StartsWith("https://") &&
|
||||
!cleanedUri.StartsWith(Constants.AndroidAppProtocol))
|
||||
{
|
||||
return AppResources.InvalidFormatUseHttpsHttpOrAndroidApp;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(cleanedUri, UriKind.Absolute, out var _))
|
||||
{
|
||||
return AppResources.InvalidURI;
|
||||
}
|
||||
|
||||
if (BlockedUris.Any(uriItem => uriItem.Uri == cleanedUri))
|
||||
{
|
||||
return string.Format(AppResources.TheURIXIsAlreadyBlocked, cleanedUri);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task UpdateAutofillBlacklistedUrisAsync()
|
||||
{
|
||||
await _stateService.SetAutofillBlacklistedUrisAsync(BlockedUris.Any() ? BlockedUris.Select(bu => bu.Uri).ToList() : null);
|
||||
}
|
||||
}
|
||||
|
||||
public class BlockAutofillUriItemViewModel : ExtendedViewModel
|
||||
{
|
||||
public BlockAutofillUriItemViewModel(string uri, ICommand editUriCommand)
|
||||
{
|
||||
Uri = uri;
|
||||
EditUriCommand = new Command(() => editUriCommand.Execute(this));
|
||||
}
|
||||
|
||||
public string Uri { get; }
|
||||
|
||||
public ICommand EditUriCommand { get; }
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ namespace Bit.App.Pages
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IExportService _exportService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly ILogger _logger;
|
||||
@@ -45,8 +44,7 @@ namespace Bit.App.Pages
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_exportService = ServiceContainer.Resolve<IExportService>("exportService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>("userVerificationService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
@@ -67,7 +65,7 @@ namespace Bit.App.Pages
|
||||
_initialized = true;
|
||||
FileFormatSelectedIndex = FileFormatOptions.FindIndex(k => k.Key == "json");
|
||||
DisablePrivateVaultPolicyEnabled = await _policyService.PolicyAppliesToUser(PolicyType.DisablePersonalVaultExport);
|
||||
UseOTPVerification = await _keyConnectorService.GetUsesKeyConnector();
|
||||
UseOTPVerification = !await _userVerificationService.HasMasterPasswordAsync(true);
|
||||
|
||||
if (UseOTPVerification)
|
||||
{
|
||||
@@ -165,9 +163,9 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
var verificationType = await _keyConnectorService.GetUsesKeyConnector()
|
||||
? VerificationType.OTP
|
||||
: VerificationType.MasterPassword;
|
||||
var verificationType = await _userVerificationService.HasMasterPasswordAsync(true)
|
||||
? VerificationType.MasterPassword
|
||||
: VerificationType.OTP;
|
||||
if (!await _userVerificationService.VerifyUser(Secret, verificationType))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -13,8 +13,13 @@
|
||||
</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"
|
||||
@@ -43,7 +48,8 @@
|
||||
StyleClass="box-value"
|
||||
x:Name="_nameEntry"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="FolderNameEntry" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
Margin="20, 0"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center"></Label>
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoFoldersLabel"></Label>
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding Folders}"
|
||||
@@ -44,10 +45,12 @@
|
||||
<DataTemplate x:DataType="views:FolderView">
|
||||
<controls:ExtendedStackLayout
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10">
|
||||
Padding="10"
|
||||
AutomationId="FolderCell">
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Name, Mode=OneWay}" />
|
||||
Text="{Binding Name, Mode=OneWay}"
|
||||
AutomationId="FolderName" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
|
||||
@@ -32,33 +32,37 @@
|
||||
Padding="10, 0"
|
||||
RowSpacing="0"
|
||||
RowDefinitions="*, Auto, *, 10"
|
||||
ColumnDefinitions="*, *">
|
||||
ColumnDefinitions="*, *"
|
||||
AutomationId="LoginRequestCell">
|
||||
<Label
|
||||
Text="{u:I18n FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
Padding="0, 10, 0 ,0"
|
||||
FontAttributes="Bold"/>
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding RequestFingerprint}"
|
||||
FormattedText="{Binding FingerprintPhrase}"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
FontSize="Small"
|
||||
Padding="0, 5, 0, 10"
|
||||
VerticalTextAlignment="Center"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"/>
|
||||
TextColor="{DynamicResource FingerprintPhrase}"
|
||||
AutomationId="FingerprintPhraseLabel" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
HorizontalOptions="Start"
|
||||
HorizontalTextAlignment="Start"
|
||||
Text="{Binding RequestDeviceType}"
|
||||
StyleClass="list-header-sub" />
|
||||
StyleClass="list-header-sub"
|
||||
AutomationId="RequestDeviceLabel" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
HorizontalOptions="End"
|
||||
HorizontalTextAlignment="End"
|
||||
Text="{Binding CreationDate, Converter={StaticResource dateTime}}"
|
||||
StyleClass="list-header-sub" />
|
||||
StyleClass="list-header-sub"
|
||||
AutomationId="RequestDateLabel" />
|
||||
<BoxView
|
||||
StyleClass="list-section-separator-top, list-section-separator-top-platform"
|
||||
VerticalOptions="End"
|
||||
@@ -94,7 +98,8 @@
|
||||
Margin="10,0"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||
Label="{u:I18n DeclineAllRequests}"
|
||||
ButtonCommand="{Binding DeclineAllRequestsCommand}"/>
|
||||
ButtonCommand="{Binding DeclineAllRequestsCommand}"
|
||||
AutomationId="DeleteAllRequestsButton" />
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace Bit.App.Pages
|
||||
Id = loginRequestData.Id,
|
||||
IpAddress = loginRequestData.RequestIpAddress,
|
||||
Email = await _stateService.GetEmailAsync(),
|
||||
FingerprintPhrase = loginRequestData.RequestFingerprint,
|
||||
FingerprintPhrase = loginRequestData.FingerprintPhrase,
|
||||
RequestDate = loginRequestData.CreationDate,
|
||||
DeviceType = loginRequestData.RequestDeviceType,
|
||||
Origin = loginRequestData.Origin
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
x:Name="_themePicker"
|
||||
ItemsSource="{Binding ThemeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding ThemeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ThemeSelectorPicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
StyleClass="box-footer-label"
|
||||
@@ -44,7 +45,8 @@
|
||||
x:Name="_autoDarkThemePicker"
|
||||
ItemsSource="{Binding AutoDarkThemeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding AutoDarkThemeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="DefaultDarkThemePicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
StyleClass="box-footer-label"
|
||||
@@ -59,7 +61,8 @@
|
||||
x:Name="_uriMatchPicker"
|
||||
ItemsSource="{Binding UriMatchOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding UriMatchSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="DefaultUriMatchDetectionPicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n DefaultUriMatchDetectionDescription}"
|
||||
@@ -74,7 +77,8 @@
|
||||
x:Name="_clearClipboardPicker"
|
||||
ItemsSource="{Binding ClearClipboardOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding ClearClipboardSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ClearClipboardPicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n ClearClipboardDescription}"
|
||||
@@ -90,7 +94,8 @@
|
||||
ItemsSource="{Binding LocalesOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding SelectedLocale}"
|
||||
ItemDisplayBinding="{Binding Value}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="LanguagePicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n LanguageChangeRequiresAppRestart}"
|
||||
@@ -105,7 +110,8 @@
|
||||
<Switch
|
||||
IsToggled="{Binding AutoTotpCopy}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="CopyTotpAutomaticallyToggle" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n CopyTotpAutomaticallyDescription}"
|
||||
@@ -120,7 +126,8 @@
|
||||
<Switch
|
||||
IsToggled="{Binding Favicon}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="ShowWebsiteIconsToggle" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n ShowWebsiteIconsDescription}"
|
||||
@@ -146,22 +153,14 @@
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
Text="{u:I18n 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>
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToBlockAutofillUrisCommand}" />
|
||||
</StackLayout.GestureRecognizers>
|
||||
<Label
|
||||
Text="{u:I18n AutofillBlockedUrisDescription}"
|
||||
Text="{u:I18n BlockAutoFill}"
|
||||
StyleClass="box-label-regular" />
|
||||
<Label
|
||||
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
@@ -44,17 +42,6 @@ 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())
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
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
|
||||
@@ -19,7 +20,6 @@ namespace Bit.App.Pages
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
|
||||
private bool _autofillSavePrompt;
|
||||
private string _autofillBlockedUris;
|
||||
private bool _favicon;
|
||||
private bool _autoTotpCopy;
|
||||
private int _clearClipboardSelectedIndex;
|
||||
@@ -84,6 +84,10 @@ 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; }
|
||||
@@ -192,25 +196,18 @@ 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();
|
||||
@@ -288,41 +285,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAutofillBlockedUris()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(AutofillBlockedUris))
|
||||
{
|
||||
await _stateService.SetAutofillBlacklistedUrisAsync(null);
|
||||
AutofillBlockedUris = null;
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
var csv = AutofillBlockedUris;
|
||||
var urisList = new List<string>();
|
||||
foreach (var uri in csv.Split(','))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uri))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var cleanedUri = uri.Replace(System.Environment.NewLine, string.Empty).Trim();
|
||||
if (!cleanedUri.StartsWith("http://") && !cleanedUri.StartsWith("https://") &&
|
||||
!cleanedUri.StartsWith(Constants.AndroidAppProtocol))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
urisList.Add(cleanedUri);
|
||||
}
|
||||
await _stateService.SetAutofillBlacklistedUrisAsync(urisList);
|
||||
AutofillBlockedUris = string.Join(", ", urisList);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateCurrentLocaleAsync()
|
||||
{
|
||||
if (!_inited)
|
||||
|
||||
@@ -32,19 +32,21 @@
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<Label IsVisible="{Binding UseFrame, Converter={StaticResource inverseBool}}"
|
||||
<controls:CustomLabel IsVisible="{Binding UseFrame, Converter={StaticResource inverseBool}}"
|
||||
Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="{Binding LineBreakMode}"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title"/>
|
||||
<Label Text="{Binding SubLabel, Mode=OneWay}"
|
||||
StyleClass="list-title"
|
||||
AutomationId="{Binding AutomationIdSettingName}" />
|
||||
<controls:CustomLabel Text="{Binding SubLabel, Mode=OneWay}"
|
||||
IsVisible="{Binding ShowSubLabel}"
|
||||
HorizontalOptions="End"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
TextColor="{Binding SubLabelColor}"
|
||||
StyleClass="list-sub" />
|
||||
StyleClass="list-sub"
|
||||
AutomationId="{Binding AutomationIdSettingStatus}" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
<DataTemplate
|
||||
@@ -57,7 +59,8 @@
|
||||
Padding="10"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
BorderColor="{DynamicResource PrimaryColor}"
|
||||
AutomationId="SettingActivePolicyTextLabel">
|
||||
<Label
|
||||
Text="{Binding Name, Mode=OneWay}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
@@ -75,7 +78,8 @@
|
||||
VerticalOptions="Center"
|
||||
FontSize="Small"
|
||||
TextColor="{Binding SubLabelColor}"
|
||||
StyleClass="list-sub" Margin="-5"/>
|
||||
StyleClass="list-sub" Margin="-5"
|
||||
AutomationId="SettingCustomVaultTimeoutPicker" />
|
||||
<controls:ExtendedStackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="ActivateTimePicker"/>
|
||||
</controls:ExtendedStackLayout.GestureRecognizers>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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
|
||||
@@ -22,5 +24,29 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ 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;
|
||||
@@ -32,7 +29,7 @@ namespace Bit.App.Pages
|
||||
private readonly IBiometricService _biometricService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ILocalizeService _localizeService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ILogger _loggerService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
@@ -51,7 +48,8 @@ namespace Bit.App.Pages
|
||||
private bool _reportLoggingEnabled;
|
||||
private bool _approvePasswordlessLoginRequests;
|
||||
private bool _shouldConnectToWatch;
|
||||
private List<KeyValuePair<string, int?>> _vaultTimeouts =
|
||||
private bool _hasMasterPassword;
|
||||
private readonly static List<KeyValuePair<string, int?>> VaultTimeoutOptions =
|
||||
new List<KeyValuePair<string, int?>>
|
||||
{
|
||||
new KeyValuePair<string, int?>(AppResources.Immediately, 0),
|
||||
@@ -65,7 +63,7 @@ namespace Bit.App.Pages
|
||||
new KeyValuePair<string, int?>(AppResources.Never, null),
|
||||
new KeyValuePair<string, int?>(AppResources.Custom, CustomVaultTimeoutValue),
|
||||
};
|
||||
private List<KeyValuePair<string, VaultTimeoutAction>> _vaultTimeoutActions =
|
||||
private readonly static List<KeyValuePair<string, VaultTimeoutAction>> VaultTimeoutActionOptions =
|
||||
new List<KeyValuePair<string, VaultTimeoutAction>>
|
||||
{
|
||||
new KeyValuePair<string, VaultTimeoutAction>(AppResources.Lock, VaultTimeoutAction.Lock),
|
||||
@@ -74,6 +72,8 @@ 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");
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||
@@ -101,12 +101,17 @@ 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)
|
||||
@@ -117,23 +122,36 @@ 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 minutes = _policyService.GetPolicyInt(_vaultTimeoutPolicy, "minutes").GetValueOrDefault();
|
||||
_vaultTimeouts = _vaultTimeouts.Where(t =>
|
||||
t.Value <= minutes &&
|
||||
var policyMinutes = _vaultTimeoutPolicy.GetInt(Policy.MINUTES_KEY);
|
||||
_vaultTimeoutOptions = _vaultTimeoutOptions.Where(t =>
|
||||
t.Value <= policyMinutes &&
|
||||
(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)
|
||||
@@ -141,8 +159,7 @@ namespace Bit.App.Pages
|
||||
_vaultTimeoutDisplayValue = AppResources.Custom;
|
||||
}
|
||||
|
||||
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
|
||||
!await _keyConnectorService.GetUsesKeyConnector();
|
||||
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() && await _userVerificationService.HasMasterPasswordAsync();
|
||||
_reportLoggingEnabled = await _loggerService.IsEnabled();
|
||||
_approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
|
||||
_shouldConnectToWatch = await _stateService.GetShouldConnectToWatchAsync();
|
||||
@@ -266,7 +283,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
var oldTimeout = _vaultTimeout;
|
||||
|
||||
var options = _vaultTimeouts.Select(
|
||||
var options = _vaultTimeoutOptions.Select(
|
||||
o => o.Key == _vaultTimeoutDisplayValue ? $"✓ {o.Key}" : o.Key).ToArray();
|
||||
if (promptOptions)
|
||||
{
|
||||
@@ -277,7 +294,7 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
var cleanSelection = selection.Replace("✓ ", string.Empty);
|
||||
var selectionOption = _vaultTimeouts.FirstOrDefault(o => o.Key == cleanSelection);
|
||||
var selectionOption = _vaultTimeoutOptions.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)
|
||||
@@ -295,13 +312,13 @@ namespace Bit.App.Pages
|
||||
|
||||
if (_vaultTimeoutPolicy != null)
|
||||
{
|
||||
var maximumTimeout = _policyService.GetPolicyInt(_vaultTimeoutPolicy, "minutes");
|
||||
var maximumTimeout = _vaultTimeoutPolicy.GetInt(Policy.MINUTES_KEY);
|
||||
|
||||
if (newTimeout > maximumTimeout)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutToLarge, AppResources.Warning);
|
||||
var timeout = await _vaultTimeoutService.GetVaultTimeout();
|
||||
_vaultTimeoutDisplayValue = _vaultTimeouts.FirstOrDefault(o => o.Value == timeout).Key ??
|
||||
_vaultTimeoutDisplayValue = _vaultTimeoutOptions.FirstOrDefault(o => o.Value == timeout).Key ??
|
||||
AppResources.Custom;
|
||||
return;
|
||||
}
|
||||
@@ -316,6 +333,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (oldTimeout != newTimeout)
|
||||
{
|
||||
await _cryptoService.RefreshKeysAsync();
|
||||
await Device.InvokeOnMainThreadAsync(BuildList);
|
||||
}
|
||||
}
|
||||
@@ -374,8 +392,17 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task VaultTimeoutActionAsync()
|
||||
{
|
||||
var options = _vaultTimeoutActions.Select(o =>
|
||||
o.Key == _vaultTimeoutActionDisplayValue ? $"✓ {o.Key}" : o.Key).ToArray();
|
||||
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 selection = await Page.DisplayActionSheet(AppResources.VaultTimeoutAction,
|
||||
AppResources.Cancel, null, options);
|
||||
if (selection == null || selection == AppResources.Cancel)
|
||||
@@ -393,7 +420,7 @@ namespace Bit.App.Pages
|
||||
cleanSelection = AppResources.Lock;
|
||||
}
|
||||
}
|
||||
var selectionOption = _vaultTimeoutActions.FirstOrDefault(o => o.Key == cleanSelection);
|
||||
var selectionOption = _vaultTimeoutActionOptions.FirstOrDefault(o => o.Key == cleanSelection);
|
||||
var changed = _vaultTimeoutActionDisplayValue != selectionOption.Key;
|
||||
_vaultTimeoutActionDisplayValue = selectionOption.Key;
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(_vaultTimeout,
|
||||
@@ -415,7 +442,7 @@ namespace Bit.App.Pages
|
||||
if (!string.IsNullOrWhiteSpace(pin))
|
||||
{
|
||||
var masterPassOnRestart = false;
|
||||
if (!await _keyConnectorService.GetUsesKeyConnector())
|
||||
if (await _userVerificationService.HasMasterPasswordAsync())
|
||||
{
|
||||
masterPassOnRestart = await _platformUtilsService.ShowDialogAsync(
|
||||
AppResources.PINRequireMasterPasswordRestart, AppResources.UnlockWithPIN,
|
||||
@@ -424,19 +451,20 @@ namespace Bit.App.Pages
|
||||
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(pin, email, kdfConfig);
|
||||
var key = await _cryptoService.GetKeyAsync();
|
||||
var pinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
|
||||
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);
|
||||
|
||||
if (masterPassOnRestart)
|
||||
{
|
||||
var encPin = await _cryptoService.EncryptAsync(pin);
|
||||
await _stateService.SetProtectedPinAsync(encPin.EncryptedString);
|
||||
await _stateService.SetPinProtectedKeyAsync(pinProtectedKey);
|
||||
await _stateService.SetPinKeyEncryptedUserKeyEphemeralAsync(protectedPinKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stateService.SetPinProtectedAsync(pinProtectedKey.EncryptedString);
|
||||
await _stateService.SetPinKeyEncryptedUserKeyAsync(protectedPinKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -446,8 +474,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (!_pin)
|
||||
{
|
||||
await _cryptoService.ClearPinProtectedKeyAsync();
|
||||
await _vaultTimeoutService.ClearAsync();
|
||||
await UpdateVaultTimeoutActionIfNeededAsync();
|
||||
}
|
||||
BuildList();
|
||||
}
|
||||
@@ -476,9 +504,10 @@ namespace Bit.App.Pages
|
||||
else
|
||||
{
|
||||
await _stateService.SetBiometricUnlockAsync(null);
|
||||
await UpdateVaultTimeoutActionIfNeededAsync();
|
||||
}
|
||||
await _stateService.SetBiometricLockedAsync(false);
|
||||
await _cryptoService.ToggleKeyAsync();
|
||||
await _cryptoService.RefreshKeysAsync();
|
||||
BuildList();
|
||||
}
|
||||
|
||||
@@ -597,14 +626,36 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (_vaultTimeoutPolicy != null)
|
||||
{
|
||||
var maximumTimeout = _policyService.GetPolicyInt(_vaultTimeoutPolicy, "minutes").GetValueOrDefault();
|
||||
securityItems.Insert(0, new SettingsPageListItem
|
||||
var policyMinutes = _vaultTimeoutPolicy.GetInt(Policy.MINUTES_KEY);
|
||||
var policyAction = _vaultTimeoutPolicy.GetString(Policy.ACTION_KEY);
|
||||
|
||||
if (policyMinutes.HasValue || !string.IsNullOrWhiteSpace(policyAction))
|
||||
{
|
||||
Name = string.Format(AppResources.VaultTimeoutPolicyInEffect,
|
||||
Math.Floor((float)maximumTimeout / 60),
|
||||
maximumTimeout % 60),
|
||||
UseFrame = true,
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
@@ -792,17 +843,19 @@ namespace Bit.App.Pages
|
||||
|
||||
private VaultTimeoutAction GetVaultTimeoutActionFromKey(string key)
|
||||
{
|
||||
return _vaultTimeoutActions.FirstOrDefault(o => o.Key == key).Value;
|
||||
return _vaultTimeoutActionOptions.FirstOrDefault(o => o.Key == key).Value;
|
||||
}
|
||||
|
||||
private int? GetVaultTimeoutFromKey(string key)
|
||||
{
|
||||
return _vaultTimeouts.FirstOrDefault(o => o.Key == key).Value;
|
||||
return _vaultTimeoutOptions.FirstOrDefault(o => o.Key == key).Value;
|
||||
}
|
||||
|
||||
private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option;
|
||||
private string CreateSelectableOption(string option, bool selected) => selected ? ToSelectedOption(option) : option;
|
||||
|
||||
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == $"✓ {compareTo}";
|
||||
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == ToSelectedOption(compareTo);
|
||||
|
||||
private string ToSelectedOption(string option) => $"✓ {option}";
|
||||
|
||||
public async Task SetScreenCaptureAllowedAsync()
|
||||
{
|
||||
@@ -834,5 +887,17 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -15,6 +16,7 @@ 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;
|
||||
@@ -26,6 +28,7 @@ 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))
|
||||
{
|
||||
@@ -91,10 +94,17 @@ namespace Bit.App.Pages
|
||||
}
|
||||
});
|
||||
await UpdateVaultButtonTitleAsync();
|
||||
if (await _keyConnectorService.UserNeedsMigration())
|
||||
if (await _keyConnectorService.UserNeedsMigrationAsync())
|
||||
{
|
||||
_messagingService.Send("convertAccountToKeyConnector");
|
||||
}
|
||||
|
||||
var forcePasswordResetReason = await _stateService.GetForcePasswordResetReasonAsync();
|
||||
|
||||
if (forcePasswordResetReason.HasValue)
|
||||
{
|
||||
_messagingService.Send(Constants.ForceUpdatePassword);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
|
||||
@@ -33,23 +33,25 @@
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row" Padding="10, 20"
|
||||
IsVisible="{Binding HasAttachments, Converter={StaticResource inverseBool}}">
|
||||
<Label Text="{u:I18n NoAttachments}" HorizontalTextAlignment="Center" />
|
||||
<Label Text="{u:I18n NoAttachments}" HorizontalTextAlignment="Center" AutomationId="NoAttachmentsLabel" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Attachments}" IsVisible="{Binding HasAttachments}">
|
||||
<controls:RepeaterView ItemsSource="{Binding Attachments}" IsVisible="{Binding HasAttachments}" AutomationId="AttachmentsList">
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:AttachmentView">
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10">
|
||||
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10" AutomationId="AttachmentRow">
|
||||
<Label
|
||||
Text="{Binding FileName, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationId="AttachmentFileNameLabel" />
|
||||
<Label
|
||||
Text="{Binding SizeName, Mode=OneWay}"
|
||||
StyleClass="box-sub-label"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="AttachmentFileSizeLabel" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||
@@ -57,7 +59,8 @@
|
||||
CommandParameter="{Binding .}"
|
||||
VerticalOptions="Center"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Delete}" />
|
||||
AutomationProperties.Name="{u:I18n Delete}"
|
||||
AutomationId="AttachmentDeleteButton" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@@ -77,17 +80,20 @@
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoFileChosenLabel" />
|
||||
<Label
|
||||
IsVisible="{Binding FileName, Converter={StaticResource notNull}}"
|
||||
Text="{Binding FileName}"
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NewAttachmentNameLabel" />
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n ChooseFile}" StyleClass="box-button-row"
|
||||
Clicked="ChooseFile_Clicked"></Button>
|
||||
Clicked="ChooseFile_Clicked"
|
||||
AutomationId="ChooseFileButton"></Button>
|
||||
<Label
|
||||
Margin="0, 10, 0, 0"
|
||||
Text="{u:I18n MaxFileSize}"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user