mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
215 Commits
v2023.3.0
...
vault/pm-5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
621bf76db8 | ||
|
|
f14be2a3a2 | ||
|
|
8ee744b746 | ||
|
|
15a03ba573 | ||
|
|
82711a0235 | ||
|
|
e6635564aa | ||
|
|
6c078fe343 | ||
|
|
743e71ff92 | ||
|
|
7b579b7aa5 | ||
|
|
fe10fd7766 | ||
|
|
3c0de8aacc | ||
|
|
18d9a77f25 | ||
|
|
9eca82a62b | ||
|
|
b90e030b8f | ||
|
|
9a28419a4e | ||
|
|
f4c468e6a1 | ||
|
|
2c346eb710 | ||
|
|
9c0908f7b7 | ||
|
|
827fbbc9ce | ||
|
|
5b249bed67 | ||
|
|
afbcb212f6 | ||
|
|
a71c28536d | ||
|
|
ba5fa8a518 | ||
|
|
65ea5574de | ||
|
|
f013f69669 | ||
|
|
f98dfa6581 | ||
|
|
0723999652 | ||
|
|
96343eccf7 | ||
|
|
793c5fef6f | ||
|
|
3a13ba4efa | ||
|
|
c5288d3921 | ||
|
|
9506595fdd | ||
|
|
7a65bf7fd7 | ||
|
|
d0ce89fedb | ||
|
|
3c94ea4579 | ||
|
|
658c1eaf64 | ||
|
|
02b0265767 | ||
|
|
bd2481b3e4 | ||
|
|
12c72b2833 | ||
|
|
2e5fb414b5 | ||
|
|
4dda7a6634 | ||
|
|
a1808f64b3 | ||
|
|
142c3145f0 | ||
|
|
72de17bd1d | ||
|
|
ed3467515e | ||
|
|
21fc56457d | ||
|
|
bc2eb212a6 | ||
|
|
a1912526c2 | ||
|
|
9d0209751c | ||
|
|
f2936c95fa | ||
|
|
bb2f1f0f5f | ||
|
|
5a0c2115a1 | ||
|
|
a67f50b145 | ||
|
|
757e5ea647 | ||
|
|
b23f29511c | ||
|
|
71731bb9b7 | ||
|
|
f2be840a7d | ||
|
|
685e0f407a | ||
|
|
bbef0f8c93 | ||
|
|
3cdf5ccd3b | ||
|
|
e97a37222a | ||
|
|
218a30b510 | ||
|
|
828043ec97 | ||
|
|
b25c8b0842 | ||
|
|
a4a0d31fc6 | ||
|
|
6ef6cf5d84 | ||
|
|
597f629920 | ||
|
|
b8cef16711 | ||
|
|
c4f6ae9077 | ||
|
|
8b9658d2c5 | ||
|
|
43bf0fbdb3 | ||
|
|
11922c6f49 | ||
|
|
a6f05338c2 | ||
|
|
b932824b5a | ||
|
|
efd1671f48 | ||
|
|
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 |
37
.github/CODEOWNERS
vendored
Normal file
37
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Please sort lines alphabetically, this will ensure we don't accidentally add duplicates.
|
||||||
|
#
|
||||||
|
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||||
|
|
||||||
|
# The following owners will be the default owners for everything in the repo.
|
||||||
|
# Unless a later match takes precedence
|
||||||
|
# @bitwarden/tech-leads
|
||||||
|
|
||||||
|
@bitwarden/dept-development-mobile
|
||||||
|
|
||||||
|
## Auth team files ##
|
||||||
|
|
||||||
|
## Platform team files ##
|
||||||
|
appIcons @bitwarden/team-platform-dev
|
||||||
|
build.cake @bitwarden/team-platform-dev
|
||||||
|
|
||||||
|
## Vault team files ##
|
||||||
|
src/watchOS @bitwarden/team-vault-dev
|
||||||
|
|
||||||
|
## Tools team files ##
|
||||||
|
src/Core/Services/EmailForwarders @bitwarden/team-tools-dev
|
||||||
|
|
||||||
|
## Crowdin Sync files ##
|
||||||
|
src/App/Resources @bitwarden/team-tools-dev
|
||||||
|
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization @bitwarden/team-tools-dev
|
||||||
|
store/apple @bitwarden/team-tools-dev
|
||||||
|
store/google @bitwarden/team-tools-dev
|
||||||
|
|
||||||
|
## Locales ##
|
||||||
|
src/App/Resources/AppResources.Designer.cs
|
||||||
|
src/App/Resources/AppResources.resx
|
||||||
|
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization/en.lproj
|
||||||
|
store/apple/en
|
||||||
|
store/google/en
|
||||||
|
|
||||||
|
## Utils ##
|
||||||
|
store/google/Publisher
|
||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
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
|
- name: Report mobile autofill failure
|
||||||
url: https://docs.google.com/forms/d/e/1FAIpQLScMopHyN7KGJs8hW562VTzbIGL4KcFnx0wJcsW0GYE1BnPiGA/viewform
|
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!
|
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
|
- name: Bitwarden Community Forums
|
||||||
url: https://community.bitwarden.com
|
url: https://community.bitwarden.com
|
||||||
about: Please visit the community forums for general community discussion, support and the development roadmap.
|
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
|
- name: Security Issues
|
||||||
url: https://hackerone.com/bitwarden
|
url: https://hackerone.com/bitwarden
|
||||||
about: We use HackerOne to manage security disclosures.
|
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",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": [
|
||||||
"config:base",
|
"config:base",
|
||||||
"schedule:monthly",
|
":combinePatchMinorReleases",
|
||||||
":maintainLockFilesMonthly",
|
":dependencyDashboard",
|
||||||
":preserveSemverRanges",
|
":maintainLockFilesWeekly",
|
||||||
":rebaseStalePrs",
|
":pinAllExceptPeerDependencies",
|
||||||
":disableDependencyDashboard"
|
":prConcurrentLimit10",
|
||||||
],
|
":rebaseStalePrs",
|
||||||
"enabledManagers": [
|
"schedule:weekends",
|
||||||
"nuget"
|
":separateMajorReleases"
|
||||||
],
|
],
|
||||||
"packageRules": [
|
"enabledManagers": ["cargo", "github-actions", "npm", "nuget"],
|
||||||
{
|
"packageRules": [
|
||||||
"matchManagers": ["nuget"],
|
{
|
||||||
"groupName": "Nuget updates",
|
"groupName": "cargo minor",
|
||||||
"groupSlug": "nuget",
|
"matchManagers": ["cargo"],
|
||||||
"separateMajorMinor": false
|
"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
|
# Feature request
|
||||||
- if: github.event.label.name == 'feature-request'
|
- if: github.event.label.name == 'feature-request'
|
||||||
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:
|
with:
|
||||||
comment: |
|
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.
|
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
|
# Intended behavior
|
||||||
- if: github.event.label.name == 'intended-behavior'
|
- if: github.event.label.name == 'intended-behavior'
|
||||||
name: Intended behaviour
|
name: Intended behaviour
|
||||||
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
|
||||||
with:
|
with:
|
||||||
comment: |
|
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.
|
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
|
# Customer support request
|
||||||
- if: github.event.label.name == 'customer-support'
|
- if: github.event.label.name == 'customer-support'
|
||||||
name: Customer Support request
|
name: Customer Support request
|
||||||
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
|
||||||
with:
|
with:
|
||||||
comment: |
|
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.
|
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
|
# Resolved
|
||||||
- if: github.event.label.name == 'resolved'
|
- if: github.event.label.name == 'resolved'
|
||||||
name: Resolved
|
name: Resolved
|
||||||
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
|
||||||
with:
|
with:
|
||||||
comment: |
|
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.
|
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
|
# Stale
|
||||||
- if: github.event.label.name == 'stale'
|
- if: github.event.label.name == 'stale'
|
||||||
name: Stale
|
name: Stale
|
||||||
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1
|
||||||
with:
|
with:
|
||||||
comment: |
|
comment: |
|
||||||
As we haven’t heard from you about this problem in some time, this issue will now be closed.
|
As we haven’t heard from you about this problem in some time, this issue will now be closed.
|
||||||
|
|||||||
242
.github/workflows/build.yml
vendored
242
.github/workflows/build.yml
vendored
@@ -9,15 +9,14 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/**"
|
- ".github/workflows/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cloc:
|
cloc:
|
||||||
name: CLOC
|
name: CLOC
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Set up CLOC
|
- name: Set up CLOC
|
||||||
run: |
|
run: |
|
||||||
@@ -30,13 +29,13 @@ jobs:
|
|||||||
|
|
||||||
setup:
|
setup:
|
||||||
name: Setup
|
name: Setup
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
rc_branch_exists: ${{ steps.branch-check.outputs.rc_branch_exists }}
|
rc_branch_exists: ${{ steps.branch-check.outputs.rc_branch_exists }}
|
||||||
hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }}
|
hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
with:
|
with:
|
||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
|
|
||||||
@@ -54,7 +53,6 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
|
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
shell: bash
|
|
||||||
|
|
||||||
|
|
||||||
android:
|
android:
|
||||||
@@ -67,35 +65,27 @@ jobs:
|
|||||||
variant: ["prod", "qa"]
|
variant: ["prod", "qa"]
|
||||||
steps:
|
steps:
|
||||||
- name: Setup NuGet
|
- name: Setup NuGet
|
||||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
||||||
with:
|
with:
|
||||||
nuget-version: 5.9.0
|
nuget-version: 5.9.0
|
||||||
|
|
||||||
|
- name: Set up .NET
|
||||||
|
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||||
|
with:
|
||||||
|
dotnet-version: '3.1.x'
|
||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
uses: microsoft/setup-msbuild@1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c # v1.3.1
|
||||||
|
|
||||||
- name: Setup Windows builder
|
- name: Setup Windows builder
|
||||||
run: choco install checksum --no-progress
|
run: choco install checksum --no-progress
|
||||||
|
|
||||||
- name: Work Around for broken Windows 2022 Runner Image
|
- name: Install Microsoft OpenJDK 11
|
||||||
run: |
|
run: |
|
||||||
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
choco install microsoft-openjdk11 --no-progress
|
||||||
$InstallPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
|
Write-Output "JAVA_HOME=$(Get-ChildItem -Path 'C:\Program Files\Microsoft\jdk*' | Select -First 1 -ExpandProperty FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||||
$componentsToAdd = @(
|
Write-Output "Java Home: $env:JAVA_HOME"
|
||||||
"Component.Xamarin"
|
|
||||||
)
|
|
||||||
[string]$workloadArgs = $componentsToAdd | ForEach-Object {" --add " + $_}
|
|
||||||
$Arguments = ('/c', "vs_installer.exe", 'modify', '--installPath', "`"$InstallPath`"",$workloadArgs, '--quiet', '--norestart', '--nocache')
|
|
||||||
$process = Start-Process -FilePath cmd.exe -ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
|
|
||||||
if ($process.ExitCode -eq 0)
|
|
||||||
{
|
|
||||||
Write-Host "components have been successfully added"
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Write-Host "components were not installed"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
@@ -105,9 +95,10 @@ jobs:
|
|||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Decrypt secrets
|
- name: Decrypt secrets
|
||||||
env:
|
env:
|
||||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||||
@@ -121,6 +112,7 @@ jobs:
|
|||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
|
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Decrypt secrets - Google Services
|
- name: Decrypt secrets - Google Services
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
env:
|
env:
|
||||||
@@ -129,6 +121,7 @@ jobs:
|
|||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
--output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg
|
--output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Increment version
|
- name: Increment version
|
||||||
run: |
|
run: |
|
||||||
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
||||||
@@ -146,18 +139,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Restore tools
|
- name: Restore tools
|
||||||
run: dotnet tool restore
|
run: dotnet tool restore
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Verify Format
|
- name: Verify Format
|
||||||
run: dotnet tool run dotnet-format --check
|
run: dotnet tool run dotnet-format --check
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Run Core tests
|
- name: Run Core tests
|
||||||
run: dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx"
|
run: dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx"
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Report test results
|
- name: Report test results
|
||||||
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226
|
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: Test Results
|
name: Test Results
|
||||||
@@ -181,8 +171,6 @@ jobs:
|
|||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
|
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
|
||||||
|
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Sign Android Build
|
- name: Sign Android Build
|
||||||
env:
|
env:
|
||||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||||
@@ -191,7 +179,7 @@ jobs:
|
|||||||
$androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj");
|
$androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj");
|
||||||
$packageName = "com.x8bit.bitwarden";
|
$packageName = "com.x8bit.bitwarden";
|
||||||
|
|
||||||
if ("${{ matrix.variant }}" -ne "prod")
|
if ("${{ matrix.variant }}" -ne "prod")
|
||||||
{
|
{
|
||||||
$packageName = "com.x8bit.bitwarden.${{ matrix.variant }}";
|
$packageName = "com.x8bit.bitwarden.${{ matrix.variant }}";
|
||||||
}
|
}
|
||||||
@@ -229,10 +217,10 @@ jobs:
|
|||||||
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk");
|
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk");
|
||||||
|
|
||||||
Copy-Item $signedApkPath $signedApkDestPath
|
Copy-Item $signedApkPath $signedApkDestPath
|
||||||
shell: pwsh
|
|
||||||
- name: Upload Prod .aab artifact
|
- name: Upload Prod .aab artifact
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.aab
|
name: com.x8bit.bitwarden.aab
|
||||||
path: ./com.x8bit.bitwarden.aab
|
path: ./com.x8bit.bitwarden.aab
|
||||||
@@ -240,7 +228,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Prod .apk artifact
|
- name: Upload Prod .apk artifact
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.apk
|
name: com.x8bit.bitwarden.apk
|
||||||
path: ./com.x8bit.bitwarden.apk
|
path: ./com.x8bit.bitwarden.apk
|
||||||
@@ -248,7 +236,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Other .apk artifact
|
- name: Upload Other .apk artifact
|
||||||
if: ${{ matrix.variant != 'prod' }}
|
if: ${{ matrix.variant != 'prod' }}
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||||
@@ -268,7 +256,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload .apk sha file for prod
|
- name: Upload .apk sha file for prod
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
with:
|
with:
|
||||||
name: bw-android-apk-sha256.txt
|
name: bw-android-apk-sha256.txt
|
||||||
path: ./bw-android-apk-sha256.txt
|
path: ./bw-android-apk-sha256.txt
|
||||||
@@ -276,14 +264,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload .apk sha file for other
|
- name: Upload .apk sha file for other
|
||||||
if: ${{ matrix.variant != 'prod' }}
|
if: ${{ matrix.variant != 'prod' }}
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
with:
|
with:
|
||||||
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||||
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Deploy to Play Store
|
- name: Deploy to Play Store
|
||||||
if: ${{ matrix.variant == 'prod' && (( github.ref == 'refs/heads/master'
|
if: ${{ matrix.variant == 'prod' && (( github.ref == 'refs/heads/main'
|
||||||
&& needs.setup.outputs.rc_branch_exists == 0
|
&& needs.setup.outputs.rc_branch_exists == 0
|
||||||
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
@@ -303,35 +291,21 @@ jobs:
|
|||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
steps:
|
steps:
|
||||||
- name: Setup NuGet
|
- name: Setup NuGet
|
||||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
||||||
with:
|
with:
|
||||||
nuget-version: 5.9.0
|
nuget-version: 5.9.0
|
||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
uses: microsoft/setup-msbuild@1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c # v1.3.1
|
||||||
|
|
||||||
- name: Setup Windows builder
|
- name: Setup Windows builder
|
||||||
run: choco install checksum --no-progress
|
run: choco install checksum --no-progress
|
||||||
|
|
||||||
- name: Work Around for broken Windows 2022 Runner Image
|
- name: Install Microsoft OpenJDK 11
|
||||||
run: |
|
run: |
|
||||||
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
choco install microsoft-openjdk11 --no-progress
|
||||||
$InstallPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
|
Write-Output "JAVA_HOME=$(Get-ChildItem -Path 'C:\Program Files\Microsoft\jdk*' | Select -First 1 -ExpandProperty FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||||
$componentsToAdd = @(
|
Write-Output "Java Home: $env:JAVA_HOME"
|
||||||
"Component.Xamarin"
|
|
||||||
)
|
|
||||||
[string]$workloadArgs = $componentsToAdd | ForEach-Object {" --add " + $_}
|
|
||||||
$Arguments = ('/c', "vs_installer.exe", 'modify', '--installPath', "`"$InstallPath`"",$workloadArgs, '--quiet', '--norestart', '--nocache')
|
|
||||||
$process = Start-Process -FilePath cmd.exe -ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
|
|
||||||
if ($process.ExitCode -eq 0)
|
|
||||||
{
|
|
||||||
Write-Host "components have been successfully added"
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Write-Host "components were not installed"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
@@ -342,7 +316,7 @@ jobs:
|
|||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Decrypt secrets
|
- name: Decrypt secrets
|
||||||
env:
|
env:
|
||||||
@@ -436,7 +410,6 @@ jobs:
|
|||||||
$appCenterNode.ParentNode.RemoveChild($appCenterNode);
|
$appCenterNode.ParentNode.RemoveChild($appCenterNode);
|
||||||
|
|
||||||
$xml.Save($corePath);
|
$xml.Save($corePath);
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Restore packages
|
- name: Restore packages
|
||||||
run: nuget restore
|
run: nuget restore
|
||||||
@@ -450,7 +423,6 @@ jobs:
|
|||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
|
|
||||||
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
|
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Sign for F-Droid
|
- name: Sign for F-Droid
|
||||||
env:
|
env:
|
||||||
@@ -474,10 +446,9 @@ jobs:
|
|||||||
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden-fdroid.apk");
|
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden-fdroid.apk");
|
||||||
|
|
||||||
Copy-Item $signedApkPath $signedApkDestPath
|
Copy-Item $signedApkPath $signedApkDestPath
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Upload F-Droid .apk artifact
|
- name: Upload F-Droid .apk artifact
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden-fdroid.apk
|
name: com.x8bit.bitwarden-fdroid.apk
|
||||||
path: ./com.x8bit.bitwarden-fdroid.apk
|
path: ./com.x8bit.bitwarden-fdroid.apk
|
||||||
@@ -489,7 +460,7 @@ jobs:
|
|||||||
-t sha256 | Out-File -Encoding ASCII ./bw-fdroid-apk-sha256.txt
|
-t sha256 | Out-File -Encoding ASCII ./bw-fdroid-apk-sha256.txt
|
||||||
|
|
||||||
- name: Upload F-Droid sha file
|
- name: Upload F-Droid sha file
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
with:
|
with:
|
||||||
name: bw-fdroid-apk-sha256.txt
|
name: bw-fdroid-apk-sha256.txt
|
||||||
path: ./bw-fdroid-apk-sha256.txt
|
path: ./bw-fdroid-apk-sha256.txt
|
||||||
@@ -502,7 +473,7 @@ jobs:
|
|||||||
needs: setup
|
needs: setup
|
||||||
steps:
|
steps:
|
||||||
- name: Setup NuGet
|
- name: Setup NuGet
|
||||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
||||||
with:
|
with:
|
||||||
nuget-version: 5.9.0
|
nuget-version: 5.9.0
|
||||||
|
|
||||||
@@ -515,28 +486,21 @@ jobs:
|
|||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
with:
|
with:
|
||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
|
|
||||||
- name: Login to Azure - Prod Subscription
|
- name: Login to Azure - CI Subscription
|
||||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
env:
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
KEYVAULT: bitwarden-prod-kv
|
with:
|
||||||
SECRETS: |
|
keyvault: "bitwarden-ci"
|
||||||
appcenter-ios-token
|
secrets: "appcenter-ios-token"
|
||||||
run: |
|
|
||||||
for i in ${SECRETS//,/ }
|
|
||||||
do
|
|
||||||
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
|
|
||||||
echo "::add-mask::$VALUE"
|
|
||||||
echo "$i=$VALUE" >> $GITHUB_OUTPUT
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Decrypt secrets
|
- name: Decrypt secrets
|
||||||
env:
|
env:
|
||||||
@@ -565,7 +529,6 @@ jobs:
|
|||||||
./.github/secrets/dist_watch_app_extension.mobileprovision.gpg
|
./.github/secrets/dist_watch_app_extension.mobileprovision.gpg
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
--output ./src/watchOS/bitwarden/GoogleService-Info.plist ./.github/secrets/GoogleService-Info.plist.gpg
|
--output ./src/watchOS/bitwarden/GoogleService-Info.plist ./.github/secrets/GoogleService-Info.plist.gpg
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Increment version
|
- name: Increment version
|
||||||
run: |
|
run: |
|
||||||
@@ -581,8 +544,6 @@ jobs:
|
|||||||
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.ShareExtension/Info.plist
|
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.ShareExtension/Info.plist
|
||||||
cd src/watchOS/bitwarden
|
cd src/watchOS/bitwarden
|
||||||
agvtool new-version -all $BUILD_NUMBER
|
agvtool new-version -all $BUILD_NUMBER
|
||||||
cd ../../..
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Update Entitlements
|
- name: Update Entitlements
|
||||||
run: |
|
run: |
|
||||||
@@ -591,7 +552,6 @@ jobs:
|
|||||||
echo "########################################"
|
echo "########################################"
|
||||||
|
|
||||||
perl -0777 -pi.bak -e 's/<key>aps-environment<\/key>\s*<string>development<\/string>/<key>aps-environment<\/key>\n\t<string>production<\/string>/' ./src/iOS/Entitlements.plist
|
perl -0777 -pi.bak -e 's/<key>aps-environment<\/key>\s*<string>development<\/string>/<key>aps-environment<\/key>\n\t<string>production<\/string>/' ./src/iOS/Entitlements.plist
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Set up Keychain
|
- name: Set up Keychain
|
||||||
env:
|
env:
|
||||||
@@ -608,7 +568,6 @@ jobs:
|
|||||||
security import ~/secrets/iphone-distribution-cert.p12 -k build.keychain -P $DIST_CERT_PASSWORD \
|
security import ~/secrets/iphone-distribution-cert.p12 -k build.keychain -P $DIST_CERT_PASSWORD \
|
||||||
-T /usr/bin/codesign -T /usr/bin/security
|
-T /usr/bin/codesign -T /usr/bin/security
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Set up provisioning profiles
|
- name: Set up provisioning profiles
|
||||||
run: |
|
run: |
|
||||||
@@ -639,7 +598,6 @@ jobs:
|
|||||||
|
|
||||||
WATCH_APP_EXTENSION_UUID=$(grep UUID -A1 -a $WATCH_APP_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
|
WATCH_APP_EXTENSION_UUID=$(grep UUID -A1 -a $WATCH_APP_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
|
||||||
cp $WATCH_APP_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_EXTENSION_UUID.mobileprovision"
|
cp $WATCH_APP_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_EXTENSION_UUID.mobileprovision"
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Bulid WatchApp
|
- name: Bulid WatchApp
|
||||||
run: |
|
run: |
|
||||||
@@ -652,7 +610,6 @@ jobs:
|
|||||||
echo "########################################"
|
echo "########################################"
|
||||||
echo "##### Done"
|
echo "##### Done"
|
||||||
echo "########################################"
|
echo "########################################"
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Restore packages
|
- name: Restore packages
|
||||||
run: nuget restore
|
run: nuget restore
|
||||||
@@ -662,6 +619,22 @@ jobs:
|
|||||||
$configuration = "AppStore";
|
$configuration = "AppStore";
|
||||||
$platform = "iPhone";
|
$platform = "iPhone";
|
||||||
|
|
||||||
|
Write-Output "########################################"
|
||||||
|
Write-Output "##### Archive $configuration Configuration for $platform Platform"
|
||||||
|
Write-Output "########################################"
|
||||||
|
msbuild "$($env:GITHUB_WORKSPACE + "/src/iOS/iOS.csproj")" "/p:Platform=$platform" `
|
||||||
|
"/p:Configuration=$configuration" "/p:ArchiveOnBuild=true" "/t:`"Build`""
|
||||||
|
|
||||||
|
Write-Output "########################################"
|
||||||
|
Write-Output "##### Done"
|
||||||
|
Write-Output "########################################"
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Archive Build for Mobile Automation
|
||||||
|
run: |
|
||||||
|
$configuration = "Release";
|
||||||
|
$platform = "iPhoneSimulator";
|
||||||
|
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
Write-Output "##### Archive $configuration Configuration for $platform Platform"
|
Write-Output "##### Archive $configuration Configuration for $platform Platform"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
@@ -682,7 +655,14 @@ jobs:
|
|||||||
|
|
||||||
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportPath $EXPORT_PATH \
|
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportPath $EXPORT_PATH \
|
||||||
-exportOptionsPlist $EXPORT_OPTIONS_PATH
|
-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
|
||||||
|
|
||||||
- name: Copy all dSYMs files to upload
|
- name: Copy all dSYMs files to upload
|
||||||
run: |
|
run: |
|
||||||
@@ -695,10 +675,9 @@ jobs:
|
|||||||
cp -r -v $ARCHIVE_DSYMS_PATH $EXPORT_PATH
|
cp -r -v $ARCHIVE_DSYMS_PATH $EXPORT_PATH
|
||||||
mkdir $WATCH_DSYMS_EXPORT_PATH
|
mkdir $WATCH_DSYMS_EXPORT_PATH
|
||||||
cp -r -v $WATCH_ARCHIVE_DSYMS_PATH $WATCH_DSYMS_EXPORT_PATH
|
cp -r -v $WATCH_ARCHIVE_DSYMS_PATH $WATCH_DSYMS_EXPORT_PATH
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Upload App Store .ipa & dSYMs artifacts
|
- name: Upload App Store .ipa & dSYMs artifacts
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
with:
|
with:
|
||||||
name: Bitwarden iOS
|
name: Bitwarden iOS
|
||||||
path: |
|
path: |
|
||||||
@@ -706,9 +685,16 @@ jobs:
|
|||||||
./bitwarden-export/dSYMs/*.*
|
./bitwarden-export/dSYMs/*.*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload .app file for Automation CI
|
||||||
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
|
with:
|
||||||
|
name: BitwardeniOS.app.zip
|
||||||
|
path: ./bitwarden-export/BitwardeniOS.app.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Install AppCenter CLI
|
- name: Install AppCenter CLI
|
||||||
if: |
|
if: |
|
||||||
(github.ref == 'refs/heads/master'
|
(github.ref == 'refs/heads/main'
|
||||||
&& needs.setup.outputs.rc_branch_exists == 0
|
&& needs.setup.outputs.rc_branch_exists == 0
|
||||||
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
@@ -717,7 +703,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload dSYMs to App Center
|
- name: Upload dSYMs to App Center
|
||||||
if: |
|
if: |
|
||||||
(github.ref == 'refs/heads/master'
|
(github.ref == 'refs/heads/main'
|
||||||
&& needs.setup.outputs.rc_branch_exists == 0
|
&& needs.setup.outputs.rc_branch_exists == 0
|
||||||
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
@@ -725,27 +711,24 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
APPCENTER_IOS_TOKEN: ${{ steps.retrieve-secrets.outputs.appcenter-ios-token }}
|
APPCENTER_IOS_TOKEN: ${{ steps.retrieve-secrets.outputs.appcenter-ios-token }}
|
||||||
run: appcenter crashes upload-symbols -a bitwarden/bitwarden -s "./bitwarden-export/dSYMs" --token $APPCENTER_IOS_TOKEN
|
run: appcenter crashes upload-symbols -a bitwarden/bitwarden -s "./bitwarden-export/dSYMs" --token $APPCENTER_IOS_TOKEN
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Upload Watch dSYMs to Firebase Crashlytics
|
- name: Upload Watch dSYMs to Firebase Crashlytics
|
||||||
if: |
|
if: |
|
||||||
(github.ref == 'refs/heads/master'
|
(github.ref == 'refs/heads/main'
|
||||||
&& needs.setup.outputs.rc_branch_exists == 0
|
&& needs.setup.outputs.rc_branch_exists == 0
|
||||||
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| github.ref == 'refs/heads/hotfix-rc'
|
|| github.ref == 'refs/heads/hotfix-rc'
|
||||||
run: |
|
run: |
|
||||||
|
|
||||||
echo "########################################"
|
echo "########################################"
|
||||||
echo "##### Uploading Watch dSYMs to Firebase"
|
echo "##### Uploading Watch dSYMs to Firebase"
|
||||||
echo "########################################"
|
echo "########################################"
|
||||||
|
|
||||||
find "$HOME/Library/Developer/XCode/DerivedData" -name "upload-symbols" -exec chmod +x {} \; -exec {} -gsp "./src/watchOS/bitwarden/GoogleService-Info.plist" -p ios "./bitwarden-export/Watch_dSYMs" \;
|
find "$HOME/Library/Developer/XCode/DerivedData" -name "upload-symbols" -exec chmod +x {} \; -exec {} -gsp "./src/watchOS/bitwarden/GoogleService-Info.plist" -p ios "./bitwarden-export/Watch_dSYMs" \;
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Deploy to App Store
|
- name: Deploy to App Store
|
||||||
if: |
|
if: |
|
||||||
(github.ref == 'refs/heads/master'
|
(github.ref == 'refs/heads/main'
|
||||||
&& needs.setup.outputs.rc_branch_exists == 0
|
&& needs.setup.outputs.rc_branch_exists == 0
|
||||||
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
@@ -756,50 +739,42 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
xcrun altool --upload-app --type ios --file "./bitwarden-export/Bitwarden.ipa" \
|
xcrun altool --upload-app --type ios --file "./bitwarden-export/Bitwarden.ipa" \
|
||||||
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
|
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
|
||||||
shell: bash
|
|
||||||
|
|
||||||
|
|
||||||
crowdin-push:
|
crowdin-push:
|
||||||
name: Crowdin Push
|
name: Crowdin Push
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/main'
|
||||||
needs:
|
needs:
|
||||||
- android
|
- android
|
||||||
- f-droid
|
- f-droid
|
||||||
- ios
|
- ios
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
env:
|
env:
|
||||||
_CROWDIN_PROJECT_ID: "269690"
|
_CROWDIN_PROJECT_ID: "269690"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
- name: Login to Azure
|
- name: Login to Azure - CI Subscription
|
||||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
env:
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
KEYVAULT: bitwarden-prod-kv
|
with:
|
||||||
SECRETS: |
|
keyvault: "bitwarden-ci"
|
||||||
crowdin-api-token
|
secrets: "crowdin-api-token"
|
||||||
run: |
|
|
||||||
for i in ${SECRETS//,/ }
|
|
||||||
do
|
|
||||||
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
|
|
||||||
echo "::add-mask::$VALUE"
|
|
||||||
echo "$i=$VALUE" >> $GITHUB_OUTPUT
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Upload Sources
|
- name: Upload Sources
|
||||||
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
|
uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||||
with:
|
with:
|
||||||
config: crowdin.yml
|
config: crowdin.yml
|
||||||
crowdin_branch_name: master
|
crowdin_branch_name: main
|
||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
|
|
||||||
@@ -807,7 +782,7 @@ jobs:
|
|||||||
check-failures:
|
check-failures:
|
||||||
name: Check for failures
|
name: Check for failures
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- cloc
|
- cloc
|
||||||
- android
|
- android
|
||||||
@@ -817,7 +792,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: |
|
if: |
|
||||||
(github.ref == 'refs/heads/master')
|
(github.ref == 'refs/heads/main')
|
||||||
|| (github.ref == 'refs/heads/rc')
|
|| (github.ref == 'refs/heads/rc')
|
||||||
|| (github.ref == 'refs/heads/hotfix-rc')
|
|| (github.ref == 'refs/heads/hotfix-rc')
|
||||||
env:
|
env:
|
||||||
@@ -839,29 +814,22 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Login to Azure - Prod Subscription
|
- name: Login to Azure - CI Subscription
|
||||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
with:
|
||||||
KEYVAULT: bitwarden-prod-kv
|
keyvault: "bitwarden-ci"
|
||||||
SECRETS: |
|
secrets: "devops-alerts-slack-webhook-url"
|
||||||
devops-alerts-slack-webhook-url
|
|
||||||
run: |
|
|
||||||
for i in ${SECRETS//,/ }
|
|
||||||
do
|
|
||||||
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
|
|
||||||
echo "::add-mask::$VALUE"
|
|
||||||
echo "$i=$VALUE" >> $GITHUB_OUTPUT
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Notify Slack on failure
|
- name: Notify Slack on failure
|
||||||
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||||
|
|||||||
18
.github/workflows/crowdin-pull.yml
vendored
18
.github/workflows/crowdin-pull.yml
vendored
@@ -15,28 +15,28 @@ jobs:
|
|||||||
_CROWDIN_PROJECT_ID: "269690"
|
_CROWDIN_PROJECT_ID: "269690"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||||
|
|
||||||
- name: Login to Azure
|
- name: Login to Azure - CI Subscription
|
||||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-prod-kv"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
|
uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||||
with:
|
with:
|
||||||
config: crowdin.yml
|
config: crowdin.yml
|
||||||
crowdin_branch_name: master
|
crowdin_branch_name: main
|
||||||
upload_sources: false
|
upload_sources: false
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
download_translations: true
|
download_translations: true
|
||||||
@@ -48,4 +48,4 @@ jobs:
|
|||||||
pull_request_title: "Autosync Crowdin Translations"
|
pull_request_title: "Autosync Crowdin Translations"
|
||||||
pull_request_body: "Autosync the updated translations"
|
pull_request_body: "Autosync the updated translations"
|
||||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
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
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Enforce Label
|
- name: Enforce Label
|
||||||
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
|
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
|
||||||
with:
|
with:
|
||||||
BANNED_LABELS: "hold,needs-qa"
|
BANNED_LABELS: "hold,needs-qa"
|
||||||
BANNED_LABELS_DESCRIPTION: "PRs with the hold or needs-qa labels cannot be merged"
|
BANNED_LABELS_DESCRIPTION: "PRs with the hold or needs-qa labels cannot be merged"
|
||||||
|
|||||||
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
|
||||||
34
.github/workflows/release.yml
vendored
34
.github/workflows/release.yml
vendored
@@ -38,11 +38,11 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
|
|
||||||
- name: Check Release Version
|
- name: Check Release Version
|
||||||
id: version
|
id: version
|
||||||
uses: bitwarden/gh-actions/release-version-check@8f055ef543c7433c967a1b9b04a0f230923233bb
|
uses: bitwarden/gh-actions/release-version-check@main
|
||||||
with:
|
with:
|
||||||
release-type: ${{ github.event.inputs.release_type }}
|
release-type: ${{ github.event.inputs.release_type }}
|
||||||
project-type: xamarin
|
project-type: xamarin
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create GitHub deployment
|
- name: Create GitHub deployment
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48
|
uses: chrnorm/deployment-action@d42cde7132fcec920de534fffc3be83794335c00 # v2.0.5
|
||||||
id: deployment
|
id: deployment
|
||||||
with:
|
with:
|
||||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
@@ -76,25 +76,25 @@ jobs:
|
|||||||
|
|
||||||
- name: Dry Run - Download all artifacts
|
- name: Dry Run - Download all artifacts
|
||||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
|
|
||||||
- name: Prep Bitwarden iOS release asset
|
- name: Prep Bitwarden iOS release asset
|
||||||
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
|
uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0
|
||||||
with:
|
with:
|
||||||
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
||||||
./com.x8bit.bitwarden.apk/com.x8bit.bitwarden.apk,
|
./com.x8bit.bitwarden.apk/com.x8bit.bitwarden.apk,
|
||||||
./com.x8bit.bitwarden-fdroid.apk/com.x8bit.bitwarden-fdroid.apk,
|
./com.x8bit.bitwarden-fdroid.apk/com.x8bit.bitwarden-fdroid.apk,
|
||||||
./Bitwarden iOS.zip,
|
./Bitwarden iOS.zip,
|
||||||
./bw-android-apk-sha256.txt,
|
./bw-android-apk-sha256.txt/bw-android-apk-sha256.txt,
|
||||||
./bw-fdroid-apk-sha256.txt"
|
./bw-fdroid-apk-sha256.txt/bw-fdroid-apk-sha256.txt"
|
||||||
commit: ${{ github.sha }}
|
commit: ${{ github.sha }}
|
||||||
tag: v${{ steps.version.outputs.version }}
|
tag: v${{ steps.version.outputs.version }}
|
||||||
name: Version ${{ steps.version.outputs.version }}
|
name: Version ${{ steps.version.outputs.version }}
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Update deployment status to Success
|
- name: Update deployment status to Success
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
|
||||||
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||||
with:
|
with:
|
||||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
state: 'success'
|
state: 'success'
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Update deployment status to Failure
|
- name: Update deployment status to Failure
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
|
||||||
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||||
with:
|
with:
|
||||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
state: 'failure'
|
state: 'failure'
|
||||||
@@ -126,11 +126,11 @@ jobs:
|
|||||||
if: inputs.fdroid_publish
|
if: inputs.fdroid_publish
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
|
|
||||||
- name: Download F-Droid .apk artifact
|
- name: Download F-Droid .apk artifact
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
@@ -139,17 +139,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Dry Run - Download F-Droid .apk artifact
|
- name: Dry Run - Download F-Droid .apk artifact
|
||||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: master
|
branch: main
|
||||||
name: com.x8bit.bitwarden-fdroid.apk
|
name: com.x8bit.bitwarden-fdroid.apk
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2.5.1
|
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
|
||||||
with:
|
with:
|
||||||
node-version: '10.x'
|
node-version: '16.x'
|
||||||
|
|
||||||
- name: Set up F-Droid server
|
- name: Set up F-Droid server
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
4
.github/workflows/stale-bot.yml
vendored
4
.github/workflows/stale-bot.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: 'Run stale action'
|
- name: 'Run stale action'
|
||||||
uses: actions/stale@3cc123766321e9f15a6676375c154ccffb12a358 # v5.0.0
|
uses: actions/stale@f7176fd3007623b69d27091f9b9d4ab7995f0a06 # v5.2.1
|
||||||
with:
|
with:
|
||||||
stale-issue-label: 'needs-reply'
|
stale-issue-label: 'needs-reply'
|
||||||
stale-pr-label: 'needs-changes'
|
stale-pr-label: 'needs-changes'
|
||||||
@@ -27,4 +27,4 @@ jobs:
|
|||||||
|
|
||||||
If you’re still working on this, please respond here after you’ve made the changes we’ve requested and our team will re-open it for further review.
|
If you’re still working on this, please respond here after you’ve made the changes we’ve requested and our team will re-open it for further review.
|
||||||
|
|
||||||
Please make sure to resolve any conflicts with the master branch before requesting another review.
|
Please make sure to resolve any conflicts with the main branch before requesting another review.
|
||||||
|
|||||||
37
.github/workflows/version-auto-bump.yml
vendored
37
.github/workflows/version-auto-bump.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
version_number: ${{ steps.version.outputs.new-version }}
|
version_number: ${{ steps.version.outputs.new-version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
|
|
||||||
- name: Calculate bumped version
|
- name: Calculate bumped version
|
||||||
id: version
|
id: version
|
||||||
@@ -32,32 +32,9 @@ jobs:
|
|||||||
echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT
|
echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
trigger_version_bump:
|
trigger_version_bump:
|
||||||
name: "Trigger version bump workflow"
|
name: Bump version to ${{ needs.setup.outputs.version_number }}
|
||||||
runs-on: ubuntu-22.04
|
needs: setup
|
||||||
needs:
|
uses: ./.github/workflows/version-bump.yml
|
||||||
- setup
|
with:
|
||||||
steps:
|
version_number: ${{ needs.setup.outputs.version_number }}
|
||||||
- name: Login to Azure
|
secrets: inherit
|
||||||
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
|
|
||||||
157
.github/workflows/version-bump.yml
vendored
157
.github/workflows/version-bump.yml
vendored
@@ -1,35 +1,45 @@
|
|||||||
---
|
---
|
||||||
name: Version Bump
|
name: Version Bump
|
||||||
|
run-name: Version Bump - v${{ inputs.version_number }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version_number:
|
version_number:
|
||||||
description: "New Version"
|
description: "New version (example: '2024.1.0')"
|
||||||
required: true
|
required: true
|
||||||
|
cut_rc_branch:
|
||||||
|
description: "Cut RC branch?"
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump_version:
|
bump_version:
|
||||||
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
|
name: "Bump Version to v${{ inputs.version_number }}"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Branch
|
- name: Login to Azure - CI Subscription
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||||
|
|
||||||
- name: Login to Azure - Prod Subscription
|
|
||||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-prod-kv"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
|
secrets: "github-gpg-private-key,
|
||||||
|
github-gpg-private-key-passphrase,
|
||||||
|
github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
|
- name: Checkout Branch
|
||||||
|
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
repository: bitwarden/mobile
|
||||||
|
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
uses: crazy-max/ghaction-import-gpg@c8bb57c57e8df1be8c73ff3d59deab1dbc00e0d1
|
uses: crazy-max/ghaction-import-gpg@d6f3f49f3345e29369fe57596a3ca8f94c4d2ca7 # v5.4.0
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||||
@@ -37,37 +47,68 @@ jobs:
|
|||||||
git_commit_gpgsign: true
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
- name: Create Version Branch
|
- name: Create Version Branch
|
||||||
run: git switch -c version_bump_${{ github.event.inputs.version_number }}
|
id: create-branch
|
||||||
|
run: |
|
||||||
|
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
||||||
|
git switch -c $NAME
|
||||||
|
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Install xmllint
|
||||||
|
run: sudo apt install -y libxml2-utils
|
||||||
|
|
||||||
|
- name: Verify input version
|
||||||
|
env:
|
||||||
|
NEW_VERSION: ${{ inputs.version_number }}
|
||||||
|
run: |
|
||||||
|
CURRENT_VERSION=$(xmllint --xpath '
|
||||||
|
string(/manifest/@*[local-name()="versionName"
|
||||||
|
and namespace-uri()="http://schemas.android.com/apk/res/android"])
|
||||||
|
' src/Android/Properties/AndroidManifest.xml)
|
||||||
|
|
||||||
|
# Error if version has not changed.
|
||||||
|
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||||
|
echo "Version has not changed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if version is newer.
|
||||||
|
printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Version check successful."
|
||||||
|
else
|
||||||
|
echo "Version check failed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Bump Version - Android XML
|
- name: Bump Version - Android XML
|
||||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ github.event.inputs.version_number }}
|
version: ${{ inputs.version_number }}
|
||||||
file_path: "./src/Android/Properties/AndroidManifest.xml"
|
file_path: "src/Android/Properties/AndroidManifest.xml"
|
||||||
|
|
||||||
- name: Bump Version - iOS.Autofill
|
- name: Bump Version - iOS.Autofill
|
||||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ github.event.inputs.version_number }}
|
version: ${{ inputs.version_number }}
|
||||||
file_path: "./src/iOS.Autofill/Info.plist"
|
file_path: "src/iOS.Autofill/Info.plist"
|
||||||
|
|
||||||
- name: Bump Version - iOS.Extension
|
- name: Bump Version - iOS.Extension
|
||||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ github.event.inputs.version_number }}
|
version: ${{ inputs.version_number }}
|
||||||
file_path: "./src/iOS.Extension/Info.plist"
|
file_path: "src/iOS.Extension/Info.plist"
|
||||||
|
|
||||||
- name: Bump Version - iOS.ShareExtension
|
- name: Bump Version - iOS.ShareExtension
|
||||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ github.event.inputs.version_number }}
|
version: ${{ inputs.version_number }}
|
||||||
file_path: "./src/iOS.ShareExtension/Info.plist"
|
file_path: "src/iOS.ShareExtension/Info.plist"
|
||||||
|
|
||||||
- name: Bump Version - iOS
|
- name: Bump Version - iOS
|
||||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ github.event.inputs.version_number }}
|
version: ${{ inputs.version_number }}
|
||||||
file_path: "./src/iOS/Info.plist"
|
file_path: "src/iOS/Info.plist"
|
||||||
|
|
||||||
- name: Setup git
|
- name: Setup git
|
||||||
run: |
|
run: |
|
||||||
@@ -86,22 +127,24 @@ jobs:
|
|||||||
|
|
||||||
- name: Commit files
|
- name: Commit files
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
run: git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
run: git commit -m "Bumped version to ${{ inputs.version_number }}" -a
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
env:
|
||||||
|
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||||
|
run: git push -u origin $PR_BRANCH
|
||||||
|
|
||||||
- name: Create Version PR
|
- name: Create Version PR
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
|
id: create-pr
|
||||||
env:
|
env:
|
||||||
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||||
BASE_BRANCH: master
|
TITLE: "Bump version to ${{ inputs.version_number }}"
|
||||||
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
|
|
||||||
run: |
|
run: |
|
||||||
gh pr create --title "$TITLE" \
|
PR_URL=$(gh pr create --title "$TITLE" \
|
||||||
--base "$BASE" \
|
--base "main" \
|
||||||
--head "$PR_BRANCH" \
|
--head "$PR_BRANCH" \
|
||||||
--label "version update" \
|
--label "version update" \
|
||||||
--label "automated pr" \
|
--label "automated pr" \
|
||||||
@@ -114,4 +157,42 @@ jobs:
|
|||||||
- [X] Other
|
- [X] Other
|
||||||
|
|
||||||
## Objective
|
## Objective
|
||||||
Automated version bump to ${{ github.event.inputs.version_number }}"
|
Automated version bump to ${{ inputs.version_number }}")
|
||||||
|
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Approve PR
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
|
run: gh pr review $PR_NUMBER --approve
|
||||||
|
|
||||||
|
- name: Merge PR
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
|
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||||
|
|
||||||
|
cut_rc:
|
||||||
|
name: Cut RC branch
|
||||||
|
needs: bump_version
|
||||||
|
if: ${{ inputs.cut_rc_branch == true }}
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout Branch
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Check if RC branch exists
|
||||||
|
run: |
|
||||||
|
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||||
|
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
|
||||||
|
echo "Remote RC branch exists."
|
||||||
|
echo "Please delete current RC branch before running again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Cut RC branch
|
||||||
|
run: |
|
||||||
|
git switch --quiet --create rc
|
||||||
|
git push --quiet --set-upstream origin rc
|
||||||
|
|||||||
2
.github/workflows/workflow-linter.yml
vendored
2
.github/workflows/workflow-linter.yml
vendored
@@ -8,4 +8,4 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
call-workflow:
|
call-workflow:
|
||||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@master
|
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@main
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
# Bitwarden Mobile Application
|
# Bitwarden Mobile Application
|
||||||
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden" target="_blank"><img alt="Get it on Google Play" src="https://imgur.com/YQzmZi9.png" width="153" height="46"></a> <a href="https://mobileapp.bitwarden.com/fdroid/" target="_blank"><img alt="Get it on 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.
|
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
|
pt-PT: pt-PT
|
||||||
en-GB: en-GB
|
en-GB: en-GB
|
||||||
en-IN: en-IN
|
en-IN: en-IN
|
||||||
|
- source: "/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/Localizable.strings"
|
||||||
|
dest: "/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/%original_file_name%"
|
||||||
|
translation: "/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization//%two_letters_code%.lproj/%original_file_name%"
|
||||||
|
update_option: update_as_unapproved
|
||||||
|
languages_mapping:
|
||||||
|
two_letters_code:
|
||||||
|
zh-CN: zh-Hans
|
||||||
|
zh-TW: zh-Hant
|
||||||
|
pt-BR: pt-BR
|
||||||
|
pt-PT: pt-PT
|
||||||
|
en-GB: en-GB
|
||||||
|
en-IN: en-IN
|
||||||
|
|||||||
7
global.json
Normal file
7
global.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "7.0.400",
|
||||||
|
"rollForward": "latestPatch",
|
||||||
|
"allowPrerelease": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,6 +107,7 @@ namespace Bit.Droid.Accessibility
|
|||||||
new Browser("org.bromite.chromium", "url_bar"),
|
new Browser("org.bromite.chromium", "url_bar"),
|
||||||
new Browser("org.chromium.chrome", "url_bar"),
|
new Browser("org.chromium.chrome", "url_bar"),
|
||||||
new Browser("org.codeaurora.swe.browser", "url_bar"),
|
new Browser("org.codeaurora.swe.browser", "url_bar"),
|
||||||
|
new Browser("org.cromite.cromite", "url_bar"),
|
||||||
new Browser("org.gnu.icecat", "url_bar_title,mozac_browser_toolbar_url_view"), // 2nd = Anticipation
|
new Browser("org.gnu.icecat", "url_bar_title,mozac_browser_toolbar_url_view"), // 2nd = Anticipation
|
||||||
new Browser("org.mozilla.fenix", "mozac_browser_toolbar_url_view"),
|
new Browser("org.mozilla.fenix", "mozac_browser_toolbar_url_view"),
|
||||||
new Browser("org.mozilla.fenix.nightly", "mozac_browser_toolbar_url_view"), // [DEPRECATED ENTRY]
|
new Browser("org.mozilla.fenix.nightly", "mozac_browser_toolbar_url_view"), // [DEPRECATED ENTRY]
|
||||||
|
|||||||
@@ -77,22 +77,21 @@
|
|||||||
<PackageReference Include="Portable.BouncyCastle">
|
<PackageReference Include="Portable.BouncyCastle">
|
||||||
<Version>1.9.0</Version>
|
<Version>1.9.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1.1" />
|
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.6.1.3" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.14" />
|
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.17" />
|
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.21" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0.1" />
|
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.10.1.2" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.15" />
|
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.4.0.2" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1.1" />
|
|
||||||
<PackageReference Include="Xamarin.Essentials">
|
<PackageReference Include="Xamarin.Essentials">
|
||||||
<Version>1.7.3</Version>
|
<Version>1.8.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||||
<Version>123.0.8</Version>
|
<Version>123.1.2.2</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.6.1.1" />
|
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.9.0.2" />
|
||||||
<PackageReference Include="Xamarin.Google.Dagger" Version="2.41.0.2" />
|
<PackageReference Include="Xamarin.Google.Dagger" Version="2.46.1.2" />
|
||||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
||||||
<Version>118.0.1.2</Version>
|
<Version>118.0.1.5</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -160,6 +159,7 @@
|
|||||||
<Compile Include="Constants.cs" />
|
<Compile Include="Constants.cs" />
|
||||||
<Compile Include="Effects\RemoveFontPaddingEffect.cs" />
|
<Compile Include="Effects\RemoveFontPaddingEffect.cs" />
|
||||||
<Compile Include="Services\WatchDeviceService.cs" />
|
<Compile Include="Services\WatchDeviceService.cs" />
|
||||||
|
<Compile Include="Renderers\CustomLabelRenderer.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
||||||
@@ -233,6 +233,26 @@
|
|||||||
<SubType></SubType>
|
<SubType></SubType>
|
||||||
<Generator></Generator>
|
<Generator></Generator>
|
||||||
</AndroidResource>
|
</AndroidResource>
|
||||||
|
<AndroidResource Include="Resources\layout\validatable_input_dialog_layout.xml">
|
||||||
|
<SubType></SubType>
|
||||||
|
<Generator></Generator>
|
||||||
|
</AndroidResource>
|
||||||
|
<AndroidResource Include="Resources\drawable\empty_uris_placeholder.xml">
|
||||||
|
<SubType></SubType>
|
||||||
|
<Generator></Generator>
|
||||||
|
</AndroidResource>
|
||||||
|
<AndroidResource Include="Resources\drawable\empty_uris_placeholder_dark.xml">
|
||||||
|
<SubType></SubType>
|
||||||
|
<Generator></Generator>
|
||||||
|
</AndroidResource>
|
||||||
|
<AndroidResource Include="Resources\drawable\empty_login_requests.xml">
|
||||||
|
<SubType></SubType>
|
||||||
|
<Generator></Generator>
|
||||||
|
</AndroidResource>
|
||||||
|
<AndroidResource Include="Resources\drawable\empty_login_requests_dark.xml">
|
||||||
|
<SubType></SubType>
|
||||||
|
<Generator></Generator>
|
||||||
|
</AndroidResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
|
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
|
||||||
|
|||||||
Binary file not shown.
@@ -20,6 +20,7 @@ using AndroidX.AutoFill.Inline.V1;
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using SaveFlags = Android.Service.Autofill.SaveFlags;
|
using SaveFlags = Android.Service.Autofill.SaveFlags;
|
||||||
using Bit.Droid.Utilities;
|
using Bit.Droid.Utilities;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Droid.Autofill
|
namespace Bit.Droid.Autofill
|
||||||
{
|
{
|
||||||
@@ -127,6 +128,7 @@ namespace Bit.Droid.Autofill
|
|||||||
"org.bromite.chromium",
|
"org.bromite.chromium",
|
||||||
"org.chromium.chrome",
|
"org.chromium.chrome",
|
||||||
"org.codeaurora.swe.browser",
|
"org.codeaurora.swe.browser",
|
||||||
|
"org.cromite.cromite",
|
||||||
"org.gnu.icecat",
|
"org.gnu.icecat",
|
||||||
"org.mozilla.fenix",
|
"org.mozilla.fenix",
|
||||||
"org.mozilla.fenix.nightly",
|
"org.mozilla.fenix.nightly",
|
||||||
@@ -152,8 +154,9 @@ namespace Bit.Droid.Autofill
|
|||||||
"androidapp://com.oneplus.applocker",
|
"androidapp://com.oneplus.applocker",
|
||||||
};
|
};
|
||||||
|
|
||||||
public static async Task<List<FilledItem>> GetFillItemsAsync(Parser parser, ICipherService cipherService)
|
public static async Task<List<FilledItem>> GetFillItemsAsync(Parser parser, ICipherService cipherService, IUserVerificationService userVerificationService)
|
||||||
{
|
{
|
||||||
|
var userHasMasterPassword = await userVerificationService.HasMasterPasswordAsync();
|
||||||
if (parser.FieldCollection.FillableForLogin)
|
if (parser.FieldCollection.FillableForLogin)
|
||||||
{
|
{
|
||||||
var ciphers = await cipherService.GetAllDecryptedByUrlAsync(parser.Uri);
|
var ciphers = await cipherService.GetAllDecryptedByUrlAsync(parser.Uri);
|
||||||
@@ -161,14 +164,14 @@ namespace Bit.Droid.Autofill
|
|||||||
{
|
{
|
||||||
var allCiphers = ciphers.Item1.ToList();
|
var allCiphers = ciphers.Item1.ToList();
|
||||||
allCiphers.AddRange(ciphers.Item2.ToList());
|
allCiphers.AddRange(ciphers.Item2.ToList());
|
||||||
var nonPromptCiphers = allCiphers.Where(cipher => cipher.Reprompt == CipherRepromptType.None);
|
var nonPromptCiphers = allCiphers.Where(cipher => !userHasMasterPassword || cipher.Reprompt == CipherRepromptType.None);
|
||||||
return nonPromptCiphers.Select(c => new FilledItem(c)).ToList();
|
return nonPromptCiphers.Select(c => new FilledItem(c)).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (parser.FieldCollection.FillableForCard)
|
else if (parser.FieldCollection.FillableForCard)
|
||||||
{
|
{
|
||||||
var ciphers = await cipherService.GetAllDecryptedAsync();
|
var ciphers = await cipherService.GetAllDecryptedAsync();
|
||||||
return ciphers.Where(c => c.Type == CipherType.Card && c.Reprompt == CipherRepromptType.None).Select(c => new FilledItem(c)).ToList();
|
return ciphers.Where(c => c.Type == CipherType.Card && (!userHasMasterPassword || c.Reprompt == CipherRepromptType.None)).Select(c => new FilledItem(c)).ToList();
|
||||||
}
|
}
|
||||||
return new List<FilledItem>();
|
return new List<FilledItem>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using Android.Widget;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Droid.Autofill
|
namespace Bit.Droid.Autofill
|
||||||
@@ -26,6 +27,7 @@ namespace Bit.Droid.Autofill
|
|||||||
private IPolicyService _policyService;
|
private IPolicyService _policyService;
|
||||||
private IStateService _stateService;
|
private IStateService _stateService;
|
||||||
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||||
|
private IUserVerificationService _userVerificationService;
|
||||||
|
|
||||||
public async override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal,
|
public async override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal,
|
||||||
FillCallback callback)
|
FillCallback callback)
|
||||||
@@ -64,11 +66,9 @@ namespace Bit.Droid.Autofill
|
|||||||
var locked = await _vaultTimeoutService.IsLockedAsync();
|
var locked = await _vaultTimeoutService.IsLockedAsync();
|
||||||
if (!locked)
|
if (!locked)
|
||||||
{
|
{
|
||||||
if (_cipherService == null)
|
_cipherService ??= ServiceContainer.Resolve<ICipherService>();
|
||||||
{
|
_userVerificationService ??= ServiceContainer.Resolve<IUserVerificationService>();
|
||||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService, _userVerificationService);
|
||||||
}
|
|
||||||
items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// build response
|
// build response
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace Bit.Droid.Autofill
|
|||||||
private List<Field> _passwordFields = null;
|
private List<Field> _passwordFields = null;
|
||||||
private List<Field> _usernameFields = null;
|
private List<Field> _usernameFields = null;
|
||||||
private HashSet<string> _ignoreSearchTerms = new HashSet<string> { "search", "find", "recipient", "edit" };
|
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" };
|
private HashSet<string> _passwordTerms = new HashSet<string> { "password", "pswd" };
|
||||||
|
|
||||||
public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>();
|
public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>();
|
||||||
@@ -54,15 +54,14 @@ namespace Bit.Droid.Autofill
|
|||||||
if (HintToFieldsMap.ContainsKey(View.AutofillHintPassword))
|
if (HintToFieldsMap.ContainsKey(View.AutofillHintPassword))
|
||||||
{
|
{
|
||||||
_passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]);
|
_passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]);
|
||||||
|
return _passwordFields;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
_passwordFields = Fields.Where(f => FieldIsPassword(f)).ToList();
|
||||||
|
if (!_passwordFields.Any())
|
||||||
{
|
{
|
||||||
_passwordFields = Fields.Where(f => FieldIsPassword(f)).ToList();
|
_passwordFields = Fields.Where(f => FieldHasPasswordTerms(f)).ToList();
|
||||||
if (!_passwordFields.Any())
|
|
||||||
{
|
|
||||||
_passwordFields = Fields.Where(f => FieldHasPasswordTerms(f)).ToList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return _passwordFields;
|
return _passwordFields;
|
||||||
}
|
}
|
||||||
@@ -87,24 +86,26 @@ namespace Bit.Droid.Autofill
|
|||||||
{
|
{
|
||||||
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]);
|
_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;
|
return _usernameFields;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,11 @@
|
|||||||
public static class Constants
|
public static class Constants
|
||||||
{
|
{
|
||||||
public const string PACKAGE_NAME = "com.x8bit.bitwarden";
|
public const string PACKAGE_NAME = "com.x8bit.bitwarden";
|
||||||
|
public const string TEMP_CAMERA_IMAGE_NAME = "temp_camera_image.jpg";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This directory must also be declared in filepaths.xml
|
||||||
|
/// </summary>
|
||||||
|
public const string TEMP_CAMERA_IMAGE_DIR = "camera_temp";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
ListenYubiKey((bool)message.Data);
|
ListenYubiKey((bool)message.Data);
|
||||||
}
|
}
|
||||||
else if (message.Command == "updatedTheme")
|
else if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
|
||||||
{
|
{
|
||||||
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => AppearanceAdjustments());
|
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => AppearanceAdjustments());
|
||||||
}
|
}
|
||||||
@@ -239,18 +239,22 @@ namespace Bit.Droid
|
|||||||
string fileName = null;
|
string fileName = null;
|
||||||
if (data != null && data.Data != null)
|
if (data != null && data.Data != null)
|
||||||
{
|
{
|
||||||
uri = data.Data;
|
if (data.Data.ToString()?.Contains(Constants.PACKAGE_NAME) != true)
|
||||||
fileName = AndroidHelpers.GetFileName(ApplicationContext, uri);
|
{
|
||||||
|
uri = data.Data;
|
||||||
|
fileName = AndroidHelpers.GetFileName(ApplicationContext, uri);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// camera
|
// camera
|
||||||
var file = new Java.IO.File(FilesDir, "temp_camera_photo.jpg");
|
var tmpDir = new Java.IO.File(FilesDir, Constants.TEMP_CAMERA_IMAGE_DIR);
|
||||||
|
var file = new Java.IO.File(tmpDir, Constants.TEMP_CAMERA_IMAGE_NAME);
|
||||||
uri = FileProvider.GetUriForFile(this, "com.x8bit.bitwarden.fileprovider", file);
|
uri = FileProvider.GetUriForFile(this, "com.x8bit.bitwarden.fileprovider", file);
|
||||||
fileName = $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg";
|
fileName = $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uri == null)
|
if (uri == null || fileName == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ using Bit.App.Utilities;
|
|||||||
using Bit.App.Pages;
|
using Bit.App.Pages;
|
||||||
using Bit.App.Utilities.AccountManagement;
|
using Bit.App.Utilities.AccountManagement;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
|
using Bit.Core.Enums;
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
using Android.Gms.Security;
|
using Android.Gms.Security;
|
||||||
#endif
|
#endif
|
||||||
@@ -67,9 +68,9 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Register<IDeleteAccountActionFlowExecutioner>("deleteAccountActionFlowExecutioner", deleteAccountActionFlowExecutioner);
|
ServiceContainer.Register<IDeleteAccountActionFlowExecutioner>("deleteAccountActionFlowExecutioner", deleteAccountActionFlowExecutioner);
|
||||||
|
|
||||||
var verificationActionsFlowHelper = new VerificationActionsFlowHelper(
|
var verificationActionsFlowHelper = new VerificationActionsFlowHelper(
|
||||||
ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService"),
|
|
||||||
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
|
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
|
||||||
ServiceContainer.Resolve<ICryptoService>("cryptoService"));
|
ServiceContainer.Resolve<ICryptoService>("cryptoService"),
|
||||||
|
ServiceContainer.Resolve<IUserVerificationService>());
|
||||||
ServiceContainer.Register<IVerificationActionsFlowHelper>("verificationActionsFlowHelper", verificationActionsFlowHelper);
|
ServiceContainer.Register<IVerificationActionsFlowHelper>("verificationActionsFlowHelper", verificationActionsFlowHelper);
|
||||||
|
|
||||||
var accountsManager = new AccountsManager(
|
var accountsManager = new AccountsManager(
|
||||||
@@ -81,7 +82,8 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Resolve<IAuthService>("authService"),
|
ServiceContainer.Resolve<IAuthService>("authService"),
|
||||||
ServiceContainer.Resolve<ILogger>("logger"),
|
ServiceContainer.Resolve<ILogger>("logger"),
|
||||||
ServiceContainer.Resolve<IMessagingService>("messagingService"),
|
ServiceContainer.Resolve<IMessagingService>("messagingService"),
|
||||||
ServiceContainer.Resolve<IWatchDeviceService>());
|
ServiceContainer.Resolve<IWatchDeviceService>(),
|
||||||
|
ServiceContainer.Resolve<IConditionedAwaiterManager>());
|
||||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||||
}
|
}
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
@@ -146,7 +148,7 @@ namespace Bit.Droid
|
|||||||
var storageMediatorService = new StorageMediatorService(mobileStorageService, secureStorageService, preferencesStorage);
|
var storageMediatorService = new StorageMediatorService(mobileStorageService, secureStorageService, preferencesStorage);
|
||||||
var stateService = new StateService(mobileStorageService, secureStorageService, storageMediatorService, messagingService);
|
var stateService = new StateService(mobileStorageService, secureStorageService, storageMediatorService, messagingService);
|
||||||
var stateMigrationService =
|
var stateMigrationService =
|
||||||
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
new StateMigrationService(DeviceType.Android, liteDbStorage, preferencesStorage, secureStorageService);
|
||||||
var clipboardService = new ClipboardService(stateService);
|
var clipboardService = new ClipboardService(stateService);
|
||||||
var deviceActionService = new DeviceActionService(stateService, messagingService);
|
var deviceActionService = new DeviceActionService(stateService, messagingService);
|
||||||
var fileService = new FileService(stateService, broadcasterService);
|
var fileService = new FileService(stateService, broadcasterService);
|
||||||
@@ -154,10 +156,11 @@ namespace Bit.Droid
|
|||||||
messagingService, broadcasterService);
|
messagingService, broadcasterService);
|
||||||
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService,
|
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService,
|
||||||
platformUtilsService, new LazyResolve<IEventService>());
|
platformUtilsService, new LazyResolve<IEventService>());
|
||||||
var biometricService = new BiometricService();
|
|
||||||
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
||||||
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
|
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
|
||||||
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService);
|
var biometricService = new BiometricService(stateService, cryptoService);
|
||||||
|
var userPinService = new UserPinService(stateService, cryptoService);
|
||||||
|
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
|
||||||
|
|
||||||
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
||||||
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
|
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
|
||||||
@@ -180,6 +183,7 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
||||||
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
||||||
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
||||||
|
ServiceContainer.Register<IUserPinService>(userPinService);
|
||||||
|
|
||||||
// Push
|
// Push
|
||||||
#if FDROID
|
#if FDROID
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.3.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.12.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/Android/Resources/drawable/empty_login_requests.xml
Normal file
41
src/Android/Resources/drawable/empty_login_requests.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:viewportWidth="200"
|
||||||
|
android:viewportHeight="143"
|
||||||
|
android:width="200dp"
|
||||||
|
android:height="143dp">
|
||||||
|
<path
|
||||||
|
android:pathData="M34 43H10C6.68629 43 4 45.6863 4 49V109C4 112.314 6.68629 115 10 115H34C37.3137 115 40 112.314 40 109V49C40 45.6863 37.3137 43 34 43ZM10 39C4.47715 39 0 43.4772 0 49V109C0 114.523 4.47715 119 10 119H34C39.5228 119 44 114.523 44 109V49C44 43.4772 39.5228 39 34 39H10Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#89929F" />
|
||||||
|
<path
|
||||||
|
android:pathData="M20.3701 47.809C20.3701 47.2567 20.8178 46.809 21.3701 46.809H22.6122C23.1645 46.809 23.6122 47.2567 23.6122 47.809C23.6122 48.3612 23.1645 48.809 22.6122 48.809H21.3701C20.8178 48.809 20.3701 48.3612 20.3701 47.809Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#89929F" />
|
||||||
|
<path
|
||||||
|
android:pathData="M68 120C68 119.448 68.4477 119 69 119H127C127.552 119 128 119.448 128 120C128 120.552 127.552 121 127 121H69C68.4477 121 68 120.552 68 120Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#89929F" />
|
||||||
|
<path
|
||||||
|
android:pathData="M87.7402 120V102.236H89.7402V120H87.7402Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#89929F" />
|
||||||
|
<path
|
||||||
|
android:pathData="M107.71 120V102.236H109.71V120H107.71Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#89929F" />
|
||||||
|
<path
|
||||||
|
android:pathData="M27 25C27 17.268 33.268 11 41 11H157C164.732 11 171 17.268 171 25V31H167V25C167 19.4772 162.523 15 157 15H41C35.4772 15 31 19.4772 31 25V41H27V25ZM42 99H127V103H42V99Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#89929F" />
|
||||||
|
<path
|
||||||
|
android:pathData="M35 26C35 22.134 38.134 19 42 19H156C159.866 19 163 22.134 163 26V31H161V26C161 23.2386 158.761 21 156 21H42C39.2386 21 37 23.2386 37 26V41H35V26ZM42 93H127V95H42V93Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#89929F" />
|
||||||
|
<path
|
||||||
|
android:pathData="M125 39C125 33.4771 129.477 29 135 29H188C193.523 29 198 33.4772 198 39V119C198 124.523 193.523 129 188 129H135C129.477 129 125 124.523 125 119V39ZM135 33C131.686 33 129 35.6863 129 39V119C129 122.314 131.686 125 135 125H188C191.314 125 194 122.314 194 119V39C194 35.6863 191.314 33 188 33H135Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#89929F" />
|
||||||
|
<path
|
||||||
|
android:pathData="M164 120C164 121.105 163.105 122 162 122C160.895 122 160 121.105 160 120C160 118.895 160.895 118 162 118C163.105 118 164 118.895 164 120Z"
|
||||||
|
android:fillColor="#89929F" />
|
||||||
|
</vector>
|
||||||
41
src/Android/Resources/drawable/empty_login_requests_dark.xml
Normal file
41
src/Android/Resources/drawable/empty_login_requests_dark.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:viewportWidth="200"
|
||||||
|
android:viewportHeight="143"
|
||||||
|
android:width="200dp"
|
||||||
|
android:height="143dp">
|
||||||
|
<path
|
||||||
|
android:pathData="M34 43H10C6.68629 43 4 45.6863 4 49V109C4 112.314 6.68629 115 10 115H34C37.3137 115 40 112.314 40 109V49C40 45.6863 37.3137 43 34 43ZM10 39C4.47715 39 0 43.4772 0 49V109C0 114.523 4.47715 119 10 119H34C39.5228 119 44 114.523 44 109V49C44 43.4772 39.5228 39 34 39H10Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#A3A3A3" />
|
||||||
|
<path
|
||||||
|
android:pathData="M20.3701 47.809C20.3701 47.2567 20.8178 46.809 21.3701 46.809H22.6122C23.1645 46.809 23.6122 47.2567 23.6122 47.809C23.6122 48.3612 23.1645 48.809 22.6122 48.809H21.3701C20.8178 48.809 20.3701 48.3612 20.3701 47.809Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#A3A3A3" />
|
||||||
|
<path
|
||||||
|
android:pathData="M68 120C68 119.448 68.4477 119 69 119H127C127.552 119 128 119.448 128 120C128 120.552 127.552 121 127 121H69C68.4477 121 68 120.552 68 120Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#A3A3A3" />
|
||||||
|
<path
|
||||||
|
android:pathData="M87.7402 120V102.236H89.7402V120H87.7402Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#A3A3A3" />
|
||||||
|
<path
|
||||||
|
android:pathData="M107.71 120V102.236H109.71V120H107.71Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#A3A3A3" />
|
||||||
|
<path
|
||||||
|
android:pathData="M27 25C27 17.268 33.268 11 41 11H157C164.732 11 171 17.268 171 25V31H167V25C167 19.4772 162.523 15 157 15H41C35.4772 15 31 19.4772 31 25V41H27V25ZM42 99H127V103H42V99Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#A3A3A3" />
|
||||||
|
<path
|
||||||
|
android:pathData="M35 26C35 22.134 38.134 19 42 19H156C159.866 19 163 22.134 163 26V31H161V26C161 23.2386 158.761 21 156 21H42C39.2386 21 37 23.2386 37 26V41H35V26ZM42 93H127V95H42V93Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#A3A3A3" />
|
||||||
|
<path
|
||||||
|
android:pathData="M125 39C125 33.4771 129.477 29 135 29H188C193.523 29 198 33.4772 198 39V119C198 124.523 193.523 129 188 129H135C129.477 129 125 124.523 125 119V39ZM135 33C131.686 33 129 35.6863 129 39V119C129 122.314 131.686 125 135 125H188C191.314 125 194 122.314 194 119V39C194 35.6863 191.314 33 188 33H135Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:fillColor="#A3A3A3" />
|
||||||
|
<path
|
||||||
|
android:pathData="M164 120C164 121.105 163.105 122 162 122C160.895 122 160 121.105 160 120C160 118.895 160.895 118 162 118C163.105 118 164 118.895 164 120Z"
|
||||||
|
android:fillColor="#A3A3A3" />
|
||||||
|
</vector>
|
||||||
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">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<dimen name="design_bottom_navigation_text_size" tools:override="true">15sp</dimen>
|
<dimen name="design_bottom_navigation_text_size" tools:override="true">15sp</dimen>
|
||||||
<dimen name="design_bottom_navigation_active_text_size" tools:override="true">15sp</dimen>
|
<dimen name="design_bottom_navigation_active_text_size" tools:override="true">15sp</dimen>
|
||||||
|
<dimen name="dialog_input_text_size">16sp</dimen>
|
||||||
|
<dimen name="dialog_header_text_size">12sp</dimen>
|
||||||
|
<dimen name="dialog_sub_value_info_text_size">12sp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -236,6 +236,9 @@
|
|||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="org.codeaurora.swe.browser"
|
android:name="org.codeaurora.swe.browser"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="org.cromite.cromite"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="org.gnu.icecat"
|
android:name="org.gnu.icecat"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<cache-path name="cache" path="." />
|
<cache-path name="cache" path="." />
|
||||||
<files-path name="internal" path="." />
|
<files-path name="temp_camera_images" path="camera_temp/" />
|
||||||
</paths>
|
</paths>
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Security.Keystore;
|
using Android.Security.Keystore;
|
||||||
|
using Bit.App.Services;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Java.Security;
|
using Java.Security;
|
||||||
using Javax.Crypto;
|
using Javax.Crypto;
|
||||||
|
|
||||||
namespace Bit.Droid.Services
|
namespace Bit.Droid.Services
|
||||||
{
|
{
|
||||||
public class BiometricService : IBiometricService
|
public class BiometricService : BaseBiometricService
|
||||||
{
|
{
|
||||||
private const string KeyName = "com.8bit.bitwarden.biometric_integrity";
|
private const string KeyName = "com.8bit.bitwarden.biometric_integrity";
|
||||||
|
|
||||||
@@ -23,28 +23,28 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
private readonly KeyStore _keystore;
|
private readonly KeyStore _keystore;
|
||||||
|
|
||||||
public BiometricService()
|
public BiometricService(IStateService stateService, ICryptoService cryptoService)
|
||||||
|
: base(stateService, cryptoService)
|
||||||
{
|
{
|
||||||
_keystore = KeyStore.GetInstance(KeyStoreName);
|
_keystore = KeyStore.GetInstance(KeyStoreName);
|
||||||
_keystore.Load(null);
|
_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)
|
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)
|
if (Build.VERSION.SdkInt < BuildVersionCodes.M)
|
||||||
{
|
{
|
||||||
return Task.FromResult(true);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -55,7 +55,7 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
if (key == null || cipher == null)
|
if (key == null || cipher == null)
|
||||||
{
|
{
|
||||||
return Task.FromResult(true);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
cipher.Init(CipherMode.EncryptMode, key);
|
cipher.Init(CipherMode.EncryptMode, key);
|
||||||
@@ -63,25 +63,32 @@ namespace Bit.Droid.Services
|
|||||||
catch (KeyPermanentlyInvalidatedException e)
|
catch (KeyPermanentlyInvalidatedException e)
|
||||||
{
|
{
|
||||||
// Biometric has changed
|
// Biometric has changed
|
||||||
return Task.FromResult(false);
|
await ClearStateAsync(bioIntegritySrcKey);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
catch (UnrecoverableKeyException e)
|
catch (UnrecoverableKeyException e)
|
||||||
{
|
{
|
||||||
// Biometric was disabled and re-enabled
|
// Biometric was disabled and re-enabled
|
||||||
return Task.FromResult(false);
|
await ClearStateAsync(bioIntegritySrcKey);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
catch (InvalidKeyException e)
|
catch (InvalidKeyException e)
|
||||||
{
|
{
|
||||||
// Fallback for old bitwarden users without a key
|
// Fallback for old bitwarden users without a key
|
||||||
LoggerHelper.LogEvenIfCantBeResolved(e);
|
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
|
try
|
||||||
{
|
{
|
||||||
var keyGen = KeyGenerator.GetInstance(KeyAlgorithm, KeyStoreName);
|
var keyGen = KeyGenerator.GetInstance(KeyAlgorithm, KeyStoreName);
|
||||||
@@ -101,5 +108,16 @@ namespace Bit.Droid.Services
|
|||||||
LoggerHelper.LogEvenIfCantBeResolved(e);
|
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 Android.Widget;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.App.Utilities.Prompts;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Droid.Utilities;
|
using Bit.Droid.Utilities;
|
||||||
using Plugin.CurrentActivity;
|
using Plugin.CurrentActivity;
|
||||||
using static Bit.App.Pages.SettingsPageViewModel;
|
using Xamarin.Forms.Platform.Android;
|
||||||
|
|
||||||
namespace Bit.Droid.Services
|
namespace Bit.Droid.Services
|
||||||
{
|
{
|
||||||
@@ -209,10 +210,7 @@ namespace Bit.Droid.Services
|
|||||||
}
|
}
|
||||||
if (numericKeyboard)
|
if (numericKeyboard)
|
||||||
{
|
{
|
||||||
input.InputType = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned;
|
SetNumericKeyboardTo(input);
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||||
input.KeyListener = DigitsKeyListener.GetInstance(false, false);
|
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
}
|
}
|
||||||
if (password)
|
if (password)
|
||||||
{
|
{
|
||||||
@@ -248,6 +246,83 @@ namespace Bit.Droid.Services
|
|||||||
return result.Task;
|
return result.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config)
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
if (activity == null)
|
||||||
|
{
|
||||||
|
return Task.FromResult<ValidatablePromptResponse?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var alertBuilder = new AlertDialog.Builder(activity);
|
||||||
|
alertBuilder.SetTitle(config.Title);
|
||||||
|
var view = activity.LayoutInflater.Inflate(Resource.Layout.validatable_input_dialog_layout, null);
|
||||||
|
alertBuilder.SetView(view);
|
||||||
|
|
||||||
|
var result = new TaskCompletionSource<ValidatablePromptResponse?>();
|
||||||
|
|
||||||
|
alertBuilder.SetOnCancelListener(new BasicDialogWithResultCancelListener(result));
|
||||||
|
alertBuilder.SetPositiveButton(config.OkButtonText ?? AppResources.Ok, listener: null);
|
||||||
|
alertBuilder.SetNegativeButton(config.CancelButtonText ?? AppResources.Cancel, (sender, args) => result.TrySetResult(null));
|
||||||
|
if (!string.IsNullOrEmpty(config.ThirdButtonText))
|
||||||
|
{
|
||||||
|
alertBuilder.SetNeutralButton(config.ThirdButtonText, (sender, args) => result.TrySetResult(new ValidatablePromptResponse(null, true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var alert = alertBuilder.Create();
|
||||||
|
|
||||||
|
var input = view.FindViewById<EditText>(Resource.Id.txtValue);
|
||||||
|
var lblHeader = view.FindViewById<TextView>(Resource.Id.lblHeader);
|
||||||
|
var lblValueSubinfo = view.FindViewById<TextView>(Resource.Id.lblValueSubinfo);
|
||||||
|
|
||||||
|
lblHeader.Text = config.Subtitle;
|
||||||
|
lblValueSubinfo.Text = config.ValueSubInfo;
|
||||||
|
|
||||||
|
var defaultSubInfoColor = lblValueSubinfo.TextColors;
|
||||||
|
|
||||||
|
input.InputType = InputTypes.ClassText;
|
||||||
|
|
||||||
|
if (config.NumericKeyboard)
|
||||||
|
{
|
||||||
|
SetNumericKeyboardTo(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.ImeOptions = input.ImeOptions | (ImeAction)ImeFlags.NoPersonalizedLearning | (ImeAction)ImeFlags.NoExtractUi;
|
||||||
|
input.Text = config.Text ?? string.Empty;
|
||||||
|
input.SetSelection(config.Text?.Length ?? 0);
|
||||||
|
input.AfterTextChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (lblValueSubinfo.Text != config.ValueSubInfo)
|
||||||
|
{
|
||||||
|
lblValueSubinfo.Text = config.ValueSubInfo;
|
||||||
|
lblHeader.SetTextColor(defaultSubInfoColor);
|
||||||
|
lblValueSubinfo.SetTextColor(defaultSubInfoColor);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
alert.Window.SetSoftInputMode(SoftInput.StateVisible);
|
||||||
|
alert.Show();
|
||||||
|
|
||||||
|
var positiveButton = alert.GetButton((int)DialogButtonType.Positive);
|
||||||
|
positiveButton.Click += (sender, args) =>
|
||||||
|
{
|
||||||
|
var error = config.ValidateText(input.Text);
|
||||||
|
if (error != null)
|
||||||
|
{
|
||||||
|
lblHeader.SetTextColor(ThemeManager.GetResourceColor("DangerColor").ToAndroid());
|
||||||
|
lblValueSubinfo.SetTextColor(ThemeManager.GetResourceColor("DangerColor").ToAndroid());
|
||||||
|
lblValueSubinfo.Text = error;
|
||||||
|
lblValueSubinfo.SendAccessibilityEvent(Android.Views.Accessibility.EventTypes.ViewFocused);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.TrySetResult(new ValidatablePromptResponse(input.Text, false));
|
||||||
|
alert.Dismiss();
|
||||||
|
};
|
||||||
|
|
||||||
|
return result.Task;
|
||||||
|
}
|
||||||
|
|
||||||
public void RateApp()
|
public void RateApp()
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
@@ -472,6 +547,12 @@ namespace Bit.Droid.Services
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
|
||||||
|
|
||||||
|
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;
|
||||||
|
|
||||||
|
public bool SupportsDrawOver() => Build.VERSION.SdkInt >= BuildVersionCodes.M;
|
||||||
|
|
||||||
private Intent RateIntentForUrl(string url, Activity activity)
|
private Intent RateIntentForUrl(string url, Activity activity)
|
||||||
{
|
{
|
||||||
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
|
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
|
||||||
@@ -525,5 +606,61 @@ namespace Bit.Droid.Services
|
|||||||
// only used by iOS
|
// only used by iOS
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetAutofillAccessibilityDescription()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.LollipopMr1)
|
||||||
|
{
|
||||||
|
return AppResources.AccessibilityDescription;
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
|
||||||
|
{
|
||||||
|
return AppResources.AccessibilityDescription2;
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.NMr1)
|
||||||
|
{
|
||||||
|
return AppResources.AccessibilityDescription3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppResources.AccessibilityDescription4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetAutofillDrawOverDescription()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
|
||||||
|
{
|
||||||
|
return AppResources.DrawOverDescription;
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.NMr1)
|
||||||
|
{
|
||||||
|
return AppResources.DrawOverDescription2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppResources.DrawOverDescription3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetNumericKeyboardTo(EditText editText)
|
||||||
|
{
|
||||||
|
editText.InputType = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned;
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
editText.KeyListener = DigitsKeyListener.GetInstance(false, false);
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BasicDialogWithResultCancelListener : Java.Lang.Object, IDialogInterfaceOnCancelListener
|
||||||
|
{
|
||||||
|
private readonly TaskCompletionSource<ValidatablePromptResponse?> _taskCompletionSource;
|
||||||
|
|
||||||
|
public BasicDialogWithResultCancelListener(TaskCompletionSource<ValidatablePromptResponse?> taskCompletionSource)
|
||||||
|
{
|
||||||
|
_taskCompletionSource = taskCompletionSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnCancel(IDialogInterface dialog)
|
||||||
|
{
|
||||||
|
_taskCompletionSource?.TrySetResult(null);
|
||||||
|
dialog?.Dismiss();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,7 +190,8 @@ namespace Bit.Droid.Services
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
|
var tmpDir = new Java.IO.File(activity.FilesDir, Constants.TEMP_CAMERA_IMAGE_DIR);
|
||||||
|
var file = new Java.IO.File(tmpDir, Constants.TEMP_CAMERA_IMAGE_NAME);
|
||||||
if (!file.Exists())
|
if (!file.Exists())
|
||||||
{
|
{
|
||||||
file.ParentFile.Mkdirs();
|
file.ParentFile.Mkdirs();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
|
using Java.Lang;
|
||||||
|
|
||||||
namespace Bit.Droid.Utilities
|
namespace Bit.Droid.Utilities
|
||||||
{
|
{
|
||||||
@@ -13,7 +14,12 @@ namespace Bit.Droid.Utilities
|
|||||||
// Note: getting the bundle like this will cause to call unparcel() internally
|
// Note: getting the bundle like this will cause to call unparcel() internally
|
||||||
var b = intent?.Extras?.GetBundle("trashstringwhichhasnousebuttocheckunparcel");
|
var b = intent?.Extras?.GetBundle("trashstringwhichhasnousebuttocheckunparcel");
|
||||||
}
|
}
|
||||||
catch (BadParcelableException)
|
catch (Exception ex) when
|
||||||
|
(
|
||||||
|
ex is BadParcelableException ||
|
||||||
|
ex is ClassNotFoundException ||
|
||||||
|
ex is RuntimeException
|
||||||
|
)
|
||||||
{
|
{
|
||||||
intent.ReplaceExtras((Bundle)null);
|
intent.ReplaceExtras((Bundle)null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Utilities.Prompts;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ namespace Bit.App.Abstractions
|
|||||||
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
|
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
|
||||||
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
||||||
bool autofocus = true, bool password = false);
|
bool autofocus = true, bool password = false);
|
||||||
|
Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config);
|
||||||
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
|
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
|
||||||
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
|
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
|
||||||
|
|
||||||
@@ -26,6 +28,9 @@ namespace Bit.App.Abstractions
|
|||||||
bool SupportsNfc();
|
bool SupportsNfc();
|
||||||
bool SupportsCamera();
|
bool SupportsCamera();
|
||||||
bool SupportsFido2();
|
bool SupportsFido2();
|
||||||
|
bool SupportsAutofillServices();
|
||||||
|
bool SupportsInlineAutofill();
|
||||||
|
bool SupportsDrawOver();
|
||||||
|
|
||||||
bool LaunchApp(string appName);
|
bool LaunchApp(string appName);
|
||||||
void RateApp();
|
void RateApp();
|
||||||
@@ -39,5 +44,7 @@ namespace Bit.App.Abstractions
|
|||||||
Task SetScreenCaptureAllowedAsync();
|
Task SetScreenCaptureAllowedAsync();
|
||||||
void OpenAppSettings();
|
void OpenAppSettings();
|
||||||
void CloseExtensionPopUp();
|
void CloseExtensionPopUp();
|
||||||
|
string GetAutofillAccessibilityDescription();
|
||||||
|
string GetAutofillDrawOverDescription();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.App.Abstractions
|
namespace Bit.App.Abstractions
|
||||||
{
|
{
|
||||||
@@ -6,10 +7,8 @@ namespace Bit.App.Abstractions
|
|||||||
{
|
{
|
||||||
string[] ProtectedFields { get; }
|
string[] ProtectedFields { get; }
|
||||||
|
|
||||||
Task<bool> ShowPasswordPromptAsync();
|
Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
|
||||||
|
|
||||||
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
|
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
|
||||||
|
|
||||||
Task<bool> Enabled();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
|
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
|
||||||
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.88.2" />
|
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.88.3" />
|
||||||
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.5" />
|
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.6" />
|
||||||
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
|
<PackageReference Include="Xamarin.Essentials" Version="1.8.0" />
|
||||||
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
||||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
|
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2612" />
|
||||||
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
|
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
|
||||||
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
|
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
|
||||||
<PackageReference Include="MessagePack" Version="2.4.59" />
|
<PackageReference Include="MessagePack" Version="2.4.59" />
|
||||||
@@ -59,9 +59,6 @@
|
|||||||
<Compile Update="Pages\Settings\ExtensionPage.xaml.cs">
|
<Compile Update="Pages\Settings\ExtensionPage.xaml.cs">
|
||||||
<DependentUpon>ExtensionPage.xaml</DependentUpon>
|
<DependentUpon>ExtensionPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Pages\Settings\AutofillServicesPage.xaml.cs">
|
|
||||||
<DependentUpon>AutofillServicesPage.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Update="Pages\Settings\FolderAddEditPage.xaml.cs">
|
<Compile Update="Pages\Settings\FolderAddEditPage.xaml.cs">
|
||||||
<DependentUpon>FolderAddEditPage.xaml</DependentUpon>
|
<DependentUpon>FolderAddEditPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
@@ -71,12 +68,6 @@
|
|||||||
<Compile Update="Pages\Settings\ExportVaultPage.xaml.cs">
|
<Compile Update="Pages\Settings\ExportVaultPage.xaml.cs">
|
||||||
<DependentUpon>ExportVaultPage.xaml</DependentUpon>
|
<DependentUpon>ExportVaultPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Pages\Settings\OptionsPage.xaml.cs">
|
|
||||||
<DependentUpon>OptionsPage.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Update="Pages\Settings\SyncPage.xaml.cs">
|
|
||||||
<DependentUpon>SyncPage.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Update="Pages\Vault\AttachmentsPage.xaml.cs">
|
<Compile Update="Pages\Vault\AttachmentsPage.xaml.cs">
|
||||||
<DependentUpon>AttachmentsPage.xaml</DependentUpon>
|
<DependentUpon>AttachmentsPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
@@ -145,6 +136,9 @@
|
|||||||
<Folder Include="Controls\DateTime\" />
|
<Folder Include="Controls\DateTime\" />
|
||||||
<Folder Include="Controls\IconLabelButton\" />
|
<Folder Include="Controls\IconLabelButton\" />
|
||||||
<Folder Include="Controls\PasswordStrengthProgressBar\" />
|
<Folder Include="Controls\PasswordStrengthProgressBar\" />
|
||||||
|
<Folder Include="Utilities\Automation\" />
|
||||||
|
<Folder Include="Utilities\Prompts\" />
|
||||||
|
<Folder Include="Controls\Settings\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -440,5 +434,8 @@
|
|||||||
<None Remove="MessagePack" />
|
<None Remove="MessagePack" />
|
||||||
<None Remove="MessagePack.MSBuild.Tasks" />
|
<None Remove="MessagePack.MSBuild.Tasks" />
|
||||||
<None Remove="Controls\PasswordStrengthProgressBar\" />
|
<None Remove="Controls\PasswordStrengthProgressBar\" />
|
||||||
|
<None Remove="Utilities\Automation\" />
|
||||||
|
<None Remove="Utilities\Prompts\" />
|
||||||
|
<None Remove="Controls\Settings\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ namespace Bit.App
|
|||||||
private readonly IFileService _fileService;
|
private readonly IFileService _fileService;
|
||||||
private readonly IAccountsManager _accountsManager;
|
private readonly IAccountsManager _accountsManager;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
private static bool _isResumed;
|
private static bool _isResumed;
|
||||||
// these variables are static because the app is launching new activities on notification click, creating new instances of App.
|
// these variables are static because the app is launching new activities on notification click, creating new instances of App.
|
||||||
private static bool _pendingCheckPasswordlessLoginRequests;
|
private static bool _pendingCheckPasswordlessLoginRequests;
|
||||||
@@ -61,6 +62,7 @@ namespace Bit.App
|
|||||||
_fileService = ServiceContainer.Resolve<IFileService>();
|
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||||
|
_configService = ServiceContainer.Resolve<IConfigService>();
|
||||||
|
|
||||||
_accountsManager.Init(() => Options, this);
|
_accountsManager.Init(() => Options, this);
|
||||||
|
|
||||||
@@ -89,7 +91,7 @@ namespace Bit.App
|
|||||||
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (message.Command == "resumed")
|
else if (message.Command == AppHelpers.RESUMED_MESSAGE_COMMAND)
|
||||||
{
|
{
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
{
|
{
|
||||||
@@ -161,6 +163,23 @@ namespace Bit.App
|
|||||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
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 == Constants.ForceSetPassword)
|
||||||
|
{
|
||||||
|
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(
|
||||||
|
new NavigationPage(new SetPasswordPage(orgIdentifier: (string)message.Data))));
|
||||||
|
}
|
||||||
|
else if (message.Command == "syncCompleted")
|
||||||
|
{
|
||||||
|
await _configService.GetAsync(true);
|
||||||
|
}
|
||||||
else if (message.Command == Constants.PasswordlessLoginRequestKey
|
else if (message.Command == Constants.PasswordlessLoginRequestKey
|
||||||
|| message.Command == "unlocked"
|
|| message.Command == "unlocked"
|
||||||
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
||||||
@@ -285,6 +304,8 @@ namespace Bit.App
|
|||||||
// Reset delay on every start
|
// Reset delay on every start
|
||||||
_vaultTimeoutService.DelayLockAndLogoutMs = null;
|
_vaultTimeoutService.DelayLockAndLogoutMs = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _configService.GetAsync();
|
||||||
_messagingService.Send("startEventTimer");
|
_messagingService.Send("startEventTimer");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +370,7 @@ namespace Bit.App
|
|||||||
await Device.InvokeOnMainThreadAsync(() =>
|
await Device.InvokeOnMainThreadAsync(() =>
|
||||||
{
|
{
|
||||||
ThemeManager.SetTheme(Current.Resources);
|
ThemeManager.SetTheme(Current.Resources);
|
||||||
_messagingService.Send("updatedTheme");
|
_messagingService.Send(ThemeManager.UPDATED_THEME_MESSAGE_KEY);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,15 @@
|
|||||||
BackgroundColor="{DynamicResource BackgroundColor}"
|
BackgroundColor="{DynamicResource BackgroundColor}"
|
||||||
VerticalOptions="Start"
|
VerticalOptions="Start"
|
||||||
RowHeight="{Binding AccountListRowHeight, Source={x:Reference _mainOverlay}}"
|
RowHeight="{Binding AccountListRowHeight, Source={x:Reference _mainOverlay}}"
|
||||||
effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never">
|
effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never"
|
||||||
|
AutomationId="AccountListView">
|
||||||
<ListView.ItemTemplate>
|
<ListView.ItemTemplate>
|
||||||
<DataTemplate x:DataType="view:AccountView">
|
<DataTemplate x:DataType="view:AccountView">
|
||||||
<controls:AccountViewCell
|
<controls:AccountViewCell
|
||||||
Account="{Binding .}"
|
Account="{Binding .}"
|
||||||
SelectAccountCommand="{Binding SelectAccountCommand, Source={x:Reference _mainOverlay}}"
|
SelectAccountCommand="{Binding SelectAccountCommand, Source={x:Reference _mainOverlay}}"
|
||||||
LongPressAccountCommand="{Binding LongPressAccountCommand, Source={x:Reference _mainOverlay}}"
|
LongPressAccountCommand="{Binding LongPressAccountCommand, Source={x:Reference _mainOverlay}}"
|
||||||
|
AutomationId="AccountViewCell"
|
||||||
/>
|
/>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListView.ItemTemplate>
|
</ListView.ItemTemplate>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
|
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
|
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
|
||||||
@@ -60,20 +60,23 @@
|
|||||||
Text="{Binding AccountView.Email}"
|
Text="{Binding AccountView.Email}"
|
||||||
IsVisible="{Binding IsActive}"
|
IsVisible="{Binding IsActive}"
|
||||||
StyleClass="accountlist-title, accountlist-title-platform"
|
StyleClass="accountlist-title, accountlist-title-platform"
|
||||||
LineBreakMode="TailTruncation" />
|
LineBreakMode="TailTruncation"
|
||||||
|
AutomationId="AccountEmailLabel" />
|
||||||
<Label
|
<Label
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Text="{Binding AccountView.Email}"
|
Text="{Binding AccountView.Email}"
|
||||||
IsVisible="{Binding IsActive, Converter={StaticResource inverseBool}}"
|
IsVisible="{Binding IsActive, Converter={StaticResource inverseBool}}"
|
||||||
StyleClass="accountlist-title, accountlist-title-platform"
|
StyleClass="accountlist-title, accountlist-title-platform"
|
||||||
TextColor="{DynamicResource MutedColor}"
|
TextColor="{DynamicResource MutedColor}"
|
||||||
LineBreakMode="TailTruncation" />
|
LineBreakMode="TailTruncation"
|
||||||
|
AutomationId="AccountEmailLabel" />
|
||||||
<Label
|
<Label
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
IsVisible="{Binding ShowHostname}"
|
IsVisible="{Binding ShowHostname}"
|
||||||
Text="{Binding AccountView.Hostname}"
|
Text="{Binding AccountView.Hostname}"
|
||||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||||
LineBreakMode="TailTruncation" />
|
LineBreakMode="TailTruncation"
|
||||||
|
AutomationId="AccountHostUrlLabel" />
|
||||||
<Label
|
<Label
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Text="{u:I18n AccountUnlocked}"
|
Text="{u:I18n AccountUnlocked}"
|
||||||
@@ -81,7 +84,8 @@
|
|||||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||||
FontAttributes="Italic"
|
FontAttributes="Italic"
|
||||||
TextTransform="Lowercase"
|
TextTransform="Lowercase"
|
||||||
LineBreakMode="TailTruncation" />
|
LineBreakMode="TailTruncation"
|
||||||
|
AutomationId="AccountStatusLabel" />
|
||||||
<Label
|
<Label
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Text="{u:I18n AccountLocked}"
|
Text="{u:I18n AccountLocked}"
|
||||||
@@ -89,7 +93,8 @@
|
|||||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||||
FontAttributes="Italic"
|
FontAttributes="Italic"
|
||||||
TextTransform="Lowercase"
|
TextTransform="Lowercase"
|
||||||
LineBreakMode="TailTruncation" />
|
LineBreakMode="TailTruncation"
|
||||||
|
AutomationId="AccountStatusLabel" />
|
||||||
<Label
|
<Label
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Text="{u:I18n AccountLoggedOut}"
|
Text="{u:I18n AccountLoggedOut}"
|
||||||
@@ -97,7 +102,8 @@
|
|||||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||||
FontAttributes="Italic"
|
FontAttributes="Italic"
|
||||||
TextTransform="Lowercase"
|
TextTransform="Lowercase"
|
||||||
LineBreakMode="TailTruncation" />
|
LineBreakMode="TailTruncation"
|
||||||
|
AutomationId="AccountStatusLabel" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
@@ -107,7 +113,8 @@
|
|||||||
Margin="12,0"
|
Margin="12,0"
|
||||||
HorizontalOptions="Center"
|
HorizontalOptions="Center"
|
||||||
VerticalOptions="Center"
|
VerticalOptions="Center"
|
||||||
StyleClass="list-icon, list-icon-platform" />
|
StyleClass="list-icon, list-icon-platform"
|
||||||
|
AutomationId="InactiveVaultIcon" />
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Text="{Binding AuthStatusIconActive}"
|
Text="{Binding AuthStatusIconActive}"
|
||||||
@@ -116,7 +123,8 @@
|
|||||||
HorizontalOptions="Center"
|
HorizontalOptions="Center"
|
||||||
VerticalOptions="Center"
|
VerticalOptions="Center"
|
||||||
StyleClass="list-icon, list-icon-platform"
|
StyleClass="list-icon, list-icon-platform"
|
||||||
TextColor="{DynamicResource TextColor}"/>
|
TextColor="{DynamicResource TextColor}"
|
||||||
|
AutomationId="ActiveVaultIcon" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
@@ -147,7 +155,8 @@
|
|||||||
StyleClass="accountlist-title, accountlist-title-platform"
|
StyleClass="accountlist-title, accountlist-title-platform"
|
||||||
LineBreakMode="TailTruncation"
|
LineBreakMode="TailTruncation"
|
||||||
VerticalOptions="Center"
|
VerticalOptions="Center"
|
||||||
Grid.Column="1" />
|
Grid.Column="1"
|
||||||
|
AutomationId="AddAccountButton" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ViewCell>
|
</ViewCell>
|
||||||
@@ -36,7 +36,7 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
public bool ShowHostname
|
public bool ShowHostname
|
||||||
{
|
{
|
||||||
get => !string.IsNullOrWhiteSpace(AccountView.Hostname) && AccountView.Hostname != "vault.bitwarden.com";
|
get => !string.IsNullOrWhiteSpace(AccountView.Hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsActive
|
public bool IsActive
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
StyleClass="list-row, list-row-platform"
|
StyleClass="list-row, list-row-platform"
|
||||||
RowSpacing="0"
|
RowSpacing="0"
|
||||||
ColumnSpacing="0"
|
ColumnSpacing="0"
|
||||||
x:DataType="controls:CipherViewCellViewModel">
|
x:DataType="controls:CipherViewCellViewModel"
|
||||||
|
AutomationId="CipherCell">
|
||||||
|
|
||||||
<Grid.Resources>
|
<Grid.Resources>
|
||||||
<u:IconGlyphConverter x:Key="iconGlyphConverter"/>
|
<u:IconGlyphConverter x:Key="iconGlyphConverter"/>
|
||||||
@@ -36,7 +37,8 @@
|
|||||||
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
|
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
|
||||||
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
|
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
|
||||||
ShouldUpdateFontSizeDynamicallyForAccesibility="True"
|
ShouldUpdateFontSizeDynamicallyForAccesibility="True"
|
||||||
AutomationProperties.IsInAccessibleTree="False" />
|
AutomationProperties.IsInAccessibleTree="False"
|
||||||
|
AutomationId="CipherTypeIcon" />
|
||||||
|
|
||||||
<ff:CachedImage
|
<ff:CachedImage
|
||||||
x:Name="_iconImage"
|
x:Name="_iconImage"
|
||||||
@@ -52,7 +54,8 @@
|
|||||||
Aspect="AspectFit"
|
Aspect="AspectFit"
|
||||||
IsVisible="{Binding ShowIconImage}"
|
IsVisible="{Binding ShowIconImage}"
|
||||||
Source="{Binding IconImageSource, Mode=OneTime}"
|
Source="{Binding IconImageSource, Mode=OneTime}"
|
||||||
AutomationProperties.IsInAccessibleTree="False" />
|
AutomationProperties.IsInAccessibleTree="False"
|
||||||
|
AutomationId="CipherWebsiteIcon" />
|
||||||
|
|
||||||
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center" Padding="0, 7">
|
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center" Padding="0, 7">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -71,7 +74,8 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
StyleClass="list-title, list-title-platform"
|
StyleClass="list-title, list-title-platform"
|
||||||
Text="{Binding Cipher.Name}" />
|
Text="{Binding Cipher.Name}"
|
||||||
|
AutomationId="CipherNameLabel" />
|
||||||
<Label
|
<Label
|
||||||
LineBreakMode="TailTruncation"
|
LineBreakMode="TailTruncation"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
@@ -80,7 +84,8 @@
|
|||||||
StyleClass="list-subtitle, list-subtitle-platform"
|
StyleClass="list-subtitle, list-subtitle-platform"
|
||||||
Text="{Binding Cipher.SubTitle}"
|
Text="{Binding Cipher.SubTitle}"
|
||||||
IsVisible="{Binding Source={RelativeSource Self}, Path=Text,
|
IsVisible="{Binding Source={RelativeSource Self}, Path=Text,
|
||||||
Converter={StaticResource stringHasValueConverter}}"/>
|
Converter={StaticResource stringHasValueConverter}}"
|
||||||
|
AutomationId="CipherSubTitleLabel" />
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
@@ -91,7 +96,8 @@
|
|||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Collection}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Collection}}"
|
||||||
IsVisible="{Binding Cipher.Shared, Mode=OneTime}"
|
IsVisible="{Binding Cipher.Shared, Mode=OneTime}"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Shared}" />
|
AutomationProperties.Name="{u:I18n Shared}"
|
||||||
|
AutomationId="CipherInCollectionIcon" />
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
@@ -102,7 +108,8 @@
|
|||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Paperclip}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Paperclip}}"
|
||||||
IsVisible="{Binding Cipher.HasAttachments, Mode=OneTime}"
|
IsVisible="{Binding Cipher.HasAttachments, Mode=OneTime}"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Attachments}" />
|
AutomationProperties.Name="{u:I18n Attachments}"
|
||||||
|
AutomationId="CipherWithAttachmentsIcon" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<controls:MiButton
|
<controls:MiButton
|
||||||
@@ -114,6 +121,7 @@
|
|||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
HorizontalOptions="EndAndExpand"
|
HorizontalOptions="EndAndExpand"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
AutomationProperties.Name="{u:I18n Options}"
|
||||||
|
AutomationId="CipherOptionsButton" />
|
||||||
|
|
||||||
</controls:ExtendedGrid>
|
</controls:ExtendedGrid>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@@ -18,7 +19,7 @@ namespace Bit.App.Controls
|
|||||||
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(CipherViewCell));
|
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(CipherViewCell));
|
||||||
|
|
||||||
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
|
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
|
||||||
nameof(ButtonCommand), typeof(Command<CipherView>), typeof(CipherViewCell));
|
nameof(ButtonCommand), typeof(ICommand), typeof(CipherViewCell));
|
||||||
|
|
||||||
public CipherViewCell()
|
public CipherViewCell()
|
||||||
{
|
{
|
||||||
@@ -42,9 +43,9 @@ namespace Bit.App.Controls
|
|||||||
set => SetValue(CipherProperty, value);
|
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);
|
set => SetValue(ButtonCommandProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace Bit.App.Controls
|
|||||||
public bool ShowIconImage
|
public bool ShowIconImage
|
||||||
{
|
{
|
||||||
get => WebsiteIconsEnabled
|
get => WebsiteIconsEnabled
|
||||||
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
|
&& !string.IsNullOrWhiteSpace(Cipher.LaunchUri)
|
||||||
&& IconImageSource != null;
|
&& IconImageSource != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
if (_iconImageSource == string.Empty) // default value since icon source can return null
|
if (_iconImageSource == string.Empty) // default value since icon source can return null
|
||||||
{
|
{
|
||||||
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
|
_iconImageSource = IconImageHelper.GetIconImage(Cipher);
|
||||||
}
|
}
|
||||||
return _iconImageSource;
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/App/Controls/ExternalLinkItemView.xaml
Normal file
29
src/App/Controls/ExternalLinkItemView.xaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<ContentView
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
x:Class="Bit.App.Controls.ExternalLinkItemView"
|
||||||
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
|
x:Name="_contentView">
|
||||||
|
<ContentView.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding GoToLinkCommand, Mode=OneWay, Source={x:Reference _contentView}}" />
|
||||||
|
</ContentView.GestureRecognizers>
|
||||||
|
<StackLayout
|
||||||
|
Orientation="Horizontal">
|
||||||
|
<controls:CustomLabel
|
||||||
|
Text="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}"
|
||||||
|
HorizontalOptions="StartAndExpand"
|
||||||
|
LineBreakMode="TailTruncation" />
|
||||||
|
|
||||||
|
<controls:IconLabel
|
||||||
|
Text="{Binding Source={x:Static core:BitwardenIcons.ShareSquare}}"
|
||||||
|
TextColor="{DynamicResource TextColor}"
|
||||||
|
HorizontalOptions="End"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}" />
|
||||||
|
|
||||||
|
</StackLayout>
|
||||||
|
</ContentView>
|
||||||
|
|
||||||
31
src/App/Controls/ExternalLinkItemView.xaml.cs
Normal file
31
src/App/Controls/ExternalLinkItemView.xaml.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public partial class ExternalLinkItemView : ContentView
|
||||||
|
{
|
||||||
|
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
|
||||||
|
nameof(Title), typeof(string), typeof(ExternalLinkItemView), null, BindingMode.OneWay);
|
||||||
|
|
||||||
|
public static readonly BindableProperty GoToLinkCommandProperty = BindableProperty.Create(
|
||||||
|
nameof(GoToLinkCommand), typeof(ICommand), typeof(ExternalLinkItemView));
|
||||||
|
|
||||||
|
public ExternalLinkItemView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get { return (string)GetValue(TitleProperty); }
|
||||||
|
set { SetValue(TitleProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand GoToLinkCommand
|
||||||
|
{
|
||||||
|
get => GetValue(GoToLinkCommandProperty) as ICommand;
|
||||||
|
set => SetValue(GoToLinkCommandProperty, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
|
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Bit.App.Controls.SendViewCell"
|
x:Class="Bit.App.Controls.SendViewCell"
|
||||||
@@ -54,14 +54,16 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
StyleClass="list-title, list-title-platform"
|
StyleClass="list-title, list-title-platform"
|
||||||
Text="{Binding Send.Name}" />
|
Text="{Binding Send.Name}"
|
||||||
|
AutomationId="SendNameLabel" />
|
||||||
<Label
|
<Label
|
||||||
LineBreakMode="TailTruncation"
|
LineBreakMode="TailTruncation"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.ColumnSpan="6"
|
Grid.ColumnSpan="6"
|
||||||
StyleClass="list-subtitle, list-subtitle-platform"
|
StyleClass="list-subtitle, list-subtitle-platform"
|
||||||
Text="{Binding Send.DisplayDate}" />
|
Text="{Binding Send.DisplayDate}"
|
||||||
|
AutomationId="SendDateLabel" />
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
@@ -72,7 +74,8 @@
|
|||||||
Text="{Binding Source={x:Static core:BitwardenIcons.ExclamationTriangle}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.ExclamationTriangle}}"
|
||||||
IsVisible="{Binding Send.Disabled, Mode=OneTime}"
|
IsVisible="{Binding Send.Disabled, Mode=OneTime}"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Disabled}" />
|
AutomationProperties.Name="{u:I18n Disabled}"
|
||||||
|
AutomationId="DisabledSendLabel" />
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
@@ -83,7 +86,8 @@
|
|||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Key}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Key}}"
|
||||||
IsVisible="{Binding Send.HasPassword, Mode=OneTime}"
|
IsVisible="{Binding Send.HasPassword, Mode=OneTime}"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Password}" />
|
AutomationProperties.Name="{u:I18n Password}"
|
||||||
|
AutomationId="PasswordProtectedSendLabel" />
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Grid.Column="3"
|
Grid.Column="3"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
@@ -94,7 +98,8 @@
|
|||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Ban}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Ban}}"
|
||||||
IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneTime}"
|
IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneTime}"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n MaxAccessCountReached}" />
|
AutomationProperties.Name="{u:I18n MaxAccessCountReached}"
|
||||||
|
AutomationId="SendMaxAccessCountReachedLabel" />
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Grid.Column="4"
|
Grid.Column="4"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
@@ -105,7 +110,8 @@
|
|||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clock}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clock}}"
|
||||||
IsVisible="{Binding Send.Expired, Mode=OneTime}"
|
IsVisible="{Binding Send.Expired, Mode=OneTime}"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Expired}" />
|
AutomationProperties.Name="{u:I18n Expired}"
|
||||||
|
AutomationId="ExpiredSendLabel" />
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Grid.Column="5"
|
Grid.Column="5"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
@@ -116,7 +122,8 @@
|
|||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||||
IsVisible="{Binding Send.PendingDelete, Mode=OneTime}"
|
IsVisible="{Binding Send.PendingDelete, Mode=OneTime}"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n PendingDelete}" />
|
AutomationProperties.Name="{u:I18n PendingDelete}"
|
||||||
|
AutomationId="SendWithPendingDeletionLabel" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<controls:MiButton
|
<controls:MiButton
|
||||||
@@ -129,6 +136,7 @@
|
|||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
HorizontalOptions="EndAndExpand"
|
HorizontalOptions="EndAndExpand"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
AutomationProperties.Name="{u:I18n Options}"
|
||||||
|
AutomationId="SendOptionsButton" />
|
||||||
|
|
||||||
</controls:ExtendedGrid>
|
</controls:ExtendedGrid>
|
||||||
|
|||||||
27
src/App/Controls/Settings/BaseSettingControlView.cs
Normal file
27
src/App/Controls/Settings/BaseSettingControlView.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public class BaseSettingItemView : ContentView
|
||||||
|
{
|
||||||
|
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
|
||||||
|
nameof(Title), typeof(string), typeof(SwitchItemView), null);
|
||||||
|
|
||||||
|
public static readonly BindableProperty SubtitleProperty = BindableProperty.Create(
|
||||||
|
nameof(Subtitle), typeof(string), typeof(SwitchItemView), null);
|
||||||
|
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get { return (string)GetValue(TitleProperty); }
|
||||||
|
set { SetValue(TitleProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Subtitle
|
||||||
|
{
|
||||||
|
get { return (string)GetValue(SubtitleProperty); }
|
||||||
|
set { SetValue(SubtitleProperty, value); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/App/Controls/Settings/SettingChooserItemView.xaml
Normal file
19
src/App/Controls/Settings/SettingChooserItemView.xaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<controls:BaseSettingItemView
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
x:Class="Bit.App.Controls.SettingChooserItemView"
|
||||||
|
x:Name="_contentView"
|
||||||
|
ControlTemplate="{StaticResource SettingControlTemplate}">
|
||||||
|
<controls:BaseSettingItemView.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding ChooseCommand, Mode=OneWay, Source={x:Reference _contentView}}" />
|
||||||
|
</controls:BaseSettingItemView.GestureRecognizers>
|
||||||
|
|
||||||
|
<controls:CustomLabel
|
||||||
|
Text="{Binding DisplayValue, Source={x:Reference _contentView}}"
|
||||||
|
HorizontalTextAlignment="End"
|
||||||
|
TextColor="{DynamicResource MutedColor}"
|
||||||
|
StyleClass="list-sub" />
|
||||||
|
|
||||||
|
</controls:BaseSettingItemView>
|
||||||
31
src/App/Controls/Settings/SettingChooserItemView.xaml.cs
Normal file
31
src/App/Controls/Settings/SettingChooserItemView.xaml.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public partial class SettingChooserItemView : BaseSettingItemView
|
||||||
|
{
|
||||||
|
public static readonly BindableProperty DisplayValueProperty = BindableProperty.Create(
|
||||||
|
nameof(DisplayValue), typeof(string), typeof(SettingChooserItemView), null);
|
||||||
|
|
||||||
|
public static readonly BindableProperty ChooseCommandProperty = BindableProperty.Create(
|
||||||
|
nameof(ChooseCommand), typeof(ICommand), typeof(ExternalLinkItemView));
|
||||||
|
|
||||||
|
public string DisplayValue
|
||||||
|
{
|
||||||
|
get { return (string)GetValue(DisplayValueProperty); }
|
||||||
|
set { SetValue(DisplayValueProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public SettingChooserItemView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand ChooseCommand
|
||||||
|
{
|
||||||
|
get => GetValue(ChooseCommandProperty) as ICommand;
|
||||||
|
set => SetValue(ChooseCommandProperty, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/App/Controls/Settings/SwitchItemView.xaml
Normal file
19
src/App/Controls/Settings/SwitchItemView.xaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<controls:BaseSettingItemView
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
x:Class="Bit.App.Controls.SwitchItemView"
|
||||||
|
x:Name="_contentView"
|
||||||
|
ControlTemplate="{StaticResource SettingControlTemplate}">
|
||||||
|
<controls:BaseSettingItemView.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Tapped="ContentView_Tapped" />
|
||||||
|
</controls:BaseSettingItemView.GestureRecognizers>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
x:Name="_switch"
|
||||||
|
HeightRequest="20"
|
||||||
|
Scale="{OnPlatform iOS=0.8, Android=1}"
|
||||||
|
IsToggled="{Binding IsToggled, Mode=TwoWay, Source={x:Reference _contentView}}"
|
||||||
|
AutomationId="{Binding SwitchAutomationId, Mode=OneWay, Source={x:Reference _contentView}}"/>
|
||||||
|
</controls:BaseSettingItemView>
|
||||||
45
src/App/Controls/Settings/SwitchItemView.xaml.cs
Normal file
45
src/App/Controls/Settings/SwitchItemView.xaml.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public partial class SwitchItemView : BaseSettingItemView
|
||||||
|
{
|
||||||
|
public static readonly BindableProperty IsToggledProperty = BindableProperty.Create(
|
||||||
|
nameof(IsToggled), typeof(bool), typeof(SwitchItemView), null, BindingMode.TwoWay);
|
||||||
|
|
||||||
|
public static readonly BindableProperty SwitchAutomationIdProperty = BindableProperty.Create(
|
||||||
|
nameof(SwitchAutomationId), typeof(string), typeof(SwitchItemView), null, BindingMode.OneWay);
|
||||||
|
|
||||||
|
public static readonly BindableProperty ToggleSwitchCommandProperty = BindableProperty.Create(
|
||||||
|
nameof(ToggleSwitchCommand), typeof(ICommand), typeof(ExternalLinkItemView));
|
||||||
|
|
||||||
|
public SwitchItemView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsToggled
|
||||||
|
{
|
||||||
|
get { return (bool)GetValue(IsToggledProperty); }
|
||||||
|
set { SetValue(IsToggledProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SwitchAutomationId
|
||||||
|
{
|
||||||
|
get { return (string)GetValue(SwitchAutomationIdProperty); }
|
||||||
|
set { SetValue(SwitchAutomationIdProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand ToggleSwitchCommand
|
||||||
|
{
|
||||||
|
get => GetValue(ToggleSwitchCommandProperty) as ICommand;
|
||||||
|
set => SetValue(ToggleSwitchCommandProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ContentView_Tapped(System.Object sender, System.EventArgs e)
|
||||||
|
{
|
||||||
|
_switch.IsToggled = !_switch.IsToggled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,8 @@
|
|||||||
StyleClass="box-label"
|
StyleClass="box-label"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||||
|
AutomationId="BooleanCustomFieldNameLabel" />
|
||||||
<Label
|
<Label
|
||||||
Text="{Binding Field.Name, Mode=OneWay}"
|
Text="{Binding Field.Name, Mode=OneWay}"
|
||||||
IsVisible="{Binding IsEditing}"
|
IsVisible="{Binding IsEditing}"
|
||||||
@@ -49,13 +50,15 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="0, 5, 0, 0"
|
Margin="0, 5, 0, 0"
|
||||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||||
|
AutomationId="BooleanCustomFieldValueLabel" />
|
||||||
<Switch
|
<Switch
|
||||||
IsToggled="{Binding BooleanValue}"
|
IsToggled="{Binding BooleanValue}"
|
||||||
IsVisible="{Binding IsEditing}"
|
IsVisible="{Binding IsEditing}"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.RowSpan="2" />
|
Grid.RowSpan="2"
|
||||||
|
AutomationId="BooleanCustomFieldValueToggle" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
Text="{Binding Field.Name, Mode=OneWay}"
|
Text="{Binding Field.Name, Mode=OneWay}"
|
||||||
StyleClass="box-label"
|
StyleClass="box-label"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0" />
|
Grid.Column="0"
|
||||||
|
AutomationId="HiddenCustomFieldNameLabel" />
|
||||||
<StackLayout
|
<StackLayout
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
@@ -39,7 +40,8 @@
|
|||||||
<controls:MonoLabel
|
<controls:MonoLabel
|
||||||
Text="{Binding ValueText, Mode=OneWay}"
|
Text="{Binding ValueText, Mode=OneWay}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
IsVisible="{Binding ShowHiddenValue}" />
|
IsVisible="{Binding ShowHiddenValue}"
|
||||||
|
AutomationId="HiddenCustomFieldValueLabel" />
|
||||||
<controls:MonoLabel
|
<controls:MonoLabel
|
||||||
Text="{Binding Field.MaskedValue, Mode=OneWay}"
|
Text="{Binding Field.MaskedValue, Mode=OneWay}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
@@ -54,7 +56,10 @@
|
|||||||
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
|
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
|
||||||
IsEnabled="{Binding ShowViewHidden}"
|
IsEnabled="{Binding ShowViewHidden}"
|
||||||
IsSpellCheckEnabled="False"
|
IsSpellCheckEnabled="False"
|
||||||
IsTextPredictionEnabled="False">
|
IsTextPredictionEnabled="False"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{Binding Field.Name}"
|
||||||
|
AutomationId="HiddenCustomFieldValueEntry">
|
||||||
<Entry.Keyboard>
|
<Entry.Keyboard>
|
||||||
<Keyboard x:FactoryMethod="Create">
|
<Keyboard x:FactoryMethod="Create">
|
||||||
<x:Arguments>
|
<x:Arguments>
|
||||||
@@ -72,7 +77,8 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
|
AutomationId="HiddenCustomFieldShowValueButton" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
|
|||||||
@@ -29,13 +29,15 @@
|
|||||||
Text="{Binding Field.Name, Mode=OneWay}"
|
Text="{Binding Field.Name, Mode=OneWay}"
|
||||||
StyleClass="box-label"
|
StyleClass="box-label"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0" />
|
Grid.Column="0"
|
||||||
|
AutomationId="LinkedCustomFieldNameLabel" />
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Text="{Binding ValueText, Mode=OneWay}"
|
Text="{Binding ValueText, Mode=OneWay}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||||
|
AutomationId="LinkedCustomFieldValueLabel" />
|
||||||
<StackLayout
|
<StackLayout
|
||||||
StyleClass="box-row, box-row-input"
|
StyleClass="box-row, box-row-input"
|
||||||
IsVisible="{Binding IsEditing}">
|
IsVisible="{Binding IsEditing}">
|
||||||
@@ -44,7 +46,8 @@
|
|||||||
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
|
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
|
||||||
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
|
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
|
||||||
ItemDisplayBinding="{Binding Key}"
|
ItemDisplayBinding="{Binding Key}"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value"
|
||||||
|
AutomationId="LinkedCustomFieldValuePicker" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
@@ -55,7 +58,8 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
AutomationProperties.Name="{u:I18n Options}"
|
||||||
|
AutomationId="LinkedCustomFieldOptionsButton" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
|||||||
@@ -29,19 +29,24 @@
|
|||||||
Text="{Binding Field.Name, Mode=OneWay}"
|
Text="{Binding Field.Name, Mode=OneWay}"
|
||||||
StyleClass="box-label"
|
StyleClass="box-label"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0" />
|
Grid.Column="0"
|
||||||
|
AutomationId="TextCustomFieldNameLabel" />
|
||||||
<Label
|
<Label
|
||||||
Text="{Binding ValueText, Mode=OneWay}"
|
Text="{Binding ValueText, Mode=OneWay}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||||
|
AutomationId="TextCustomFieldValueLabel" />
|
||||||
<Entry
|
<Entry
|
||||||
Text="{Binding Field.Value}"
|
Text="{Binding Field.Value}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
IsVisible="{Binding IsEditing}" />
|
IsVisible="{Binding IsEditing}"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{Binding Field.Name}"
|
||||||
|
AutomationId="TextCustomFieldValueEntry" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
@@ -51,7 +56,8 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Copy}" />
|
AutomationProperties.Name="{u:I18n Copy}"
|
||||||
|
AutomationId="TextCustomFieldCopyValue" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||||
@@ -61,7 +67,8 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
AutomationProperties.Name="{u:I18n Options}"
|
||||||
|
AutomationId="TextCustomFieldOptionsButton" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Text;
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
@@ -73,13 +70,13 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _policy, value);
|
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 PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
public string MasterPassword { get; set; }
|
public string MasterPassword { get; set; }
|
||||||
public string ConfirmMasterPassword { get; set; }
|
public string ConfirmMasterPassword { get; set; }
|
||||||
public string Hint { get; set; }
|
public string Hint { get; set; }
|
||||||
|
|
||||||
public async Task InitAsync(bool forceSync = false)
|
public virtual async Task InitAsync(bool forceSync = false)
|
||||||
{
|
{
|
||||||
if (forceSync)
|
if (forceSync)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
Placeholder="ex. https://bitwarden.company.com"
|
Placeholder="ex. https://bitwarden.company.com"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
ReturnType="Go"
|
ReturnType="Go"
|
||||||
ReturnCommand="{Binding SubmitCommand}" />
|
ReturnCommand="{Binding SubmitCommand}"
|
||||||
|
AutomationId="ServerUrlEntry"/>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n SelfHostedEnvironmentFooter}"
|
Text="{u:I18n SelfHostedEnvironmentFooter}"
|
||||||
@@ -53,7 +54,8 @@
|
|||||||
x:Name="_webVaultEntry"
|
x:Name="_webVaultEntry"
|
||||||
Text="{Binding WebVaultUrl}"
|
Text="{Binding WebVaultUrl}"
|
||||||
Keyboard="Url"
|
Keyboard="Url"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value"
|
||||||
|
AutomationId="WebVaultUrlEntry"/>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box-row">
|
<StackLayout StyleClass="box-row">
|
||||||
<Label
|
<Label
|
||||||
@@ -63,7 +65,8 @@
|
|||||||
x:Name="_apiEntry"
|
x:Name="_apiEntry"
|
||||||
Text="{Binding ApiUrl}"
|
Text="{Binding ApiUrl}"
|
||||||
Keyboard="Url"
|
Keyboard="Url"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value"
|
||||||
|
AutomationId="ApiUrlEntry"/>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box-row">
|
<StackLayout StyleClass="box-row">
|
||||||
<Label
|
<Label
|
||||||
@@ -73,7 +76,8 @@
|
|||||||
x:Name="_identityEntry"
|
x:Name="_identityEntry"
|
||||||
Text="{Binding IdentityUrl}"
|
Text="{Binding IdentityUrl}"
|
||||||
Keyboard="Url"
|
Keyboard="Url"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value"
|
||||||
|
AutomationId="IdentityUrlEntry"/>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box-row">
|
<StackLayout StyleClass="box-row">
|
||||||
<Label
|
<Label
|
||||||
@@ -85,7 +89,8 @@
|
|||||||
Keyboard="Url"
|
Keyboard="Url"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
ReturnType="Go"
|
ReturnType="Go"
|
||||||
ReturnCommand="{Binding SubmitCommand}" />
|
ReturnCommand="{Binding SubmitCommand}"
|
||||||
|
AutomationId="IconsUrlEntry"/>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n CustomEnvironmentFooter}"
|
Text="{u:I18n CustomEnvironmentFooter}"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
|||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
|
||||||
@@ -18,13 +19,25 @@ namespace Bit.App.Pages
|
|||||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||||
|
|
||||||
PageTitle = AppResources.Settings;
|
PageTitle = AppResources.Settings;
|
||||||
|
SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
|
||||||
|
Init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Init()
|
||||||
|
{
|
||||||
|
if (_environmentService.SelectedRegion != Region.SelfHosted ||
|
||||||
|
_environmentService.BaseUrl == Region.US.BaseUrl() ||
|
||||||
|
_environmentService.BaseUrl == Region.EU.BaseUrl())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
BaseUrl = _environmentService.BaseUrl;
|
BaseUrl = _environmentService.BaseUrl;
|
||||||
WebVaultUrl = _environmentService.WebVaultUrl;
|
WebVaultUrl = _environmentService.WebVaultUrl;
|
||||||
ApiUrl = _environmentService.ApiUrl;
|
ApiUrl = _environmentService.ApiUrl;
|
||||||
IdentityUrl = _environmentService.IdentityUrl;
|
IdentityUrl = _environmentService.IdentityUrl;
|
||||||
IconsUrl = _environmentService.IconsUrl;
|
IconsUrl = _environmentService.IconsUrl;
|
||||||
NotificationsUrls = _environmentService.NotificationsUrl;
|
NotificationsUrls = _environmentService.NotificationsUrl;
|
||||||
SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ICommand SubmitCommand { get; }
|
public ICommand SubmitCommand { get; }
|
||||||
@@ -44,8 +57,7 @@ namespace Bit.App.Pages
|
|||||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok);
|
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var urls = new Core.Models.Data.EnvironmentUrlData
|
||||||
var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
|
|
||||||
{
|
{
|
||||||
Base = BaseUrl,
|
Base = BaseUrl,
|
||||||
Api = ApiUrl,
|
Api = ApiUrl,
|
||||||
@@ -53,7 +65,8 @@ namespace Bit.App.Pages
|
|||||||
WebVault = WebVaultUrl,
|
WebVault = WebVaultUrl,
|
||||||
Icons = IconsUrl,
|
Icons = IconsUrl,
|
||||||
Notifications = NotificationsUrls
|
Notifications = NotificationsUrls
|
||||||
});
|
};
|
||||||
|
var resUrls = await _environmentService.SetRegionAsync(urls.Region, urls);
|
||||||
|
|
||||||
// re-set urls since service can change them, ex: prefixing https://
|
// re-set urls since service can change them, ex: prefixing https://
|
||||||
BaseUrl = resUrls.Base;
|
BaseUrl = resUrls.Base;
|
||||||
|
|||||||
@@ -23,12 +23,9 @@
|
|||||||
Priority="-1"
|
Priority="-1"
|
||||||
UseOriginalImage="True"
|
UseOriginalImage="True"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Account}" />
|
AutomationProperties.Name="{u:I18n Account}"
|
||||||
|
AutomationId="AccountIconButton" />
|
||||||
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
|
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
|
||||||
<ToolbarItem
|
|
||||||
Icon="cog_environment.png" Clicked="Environment_Clicked" Order="Primary"
|
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ContentPage.Resources>
|
<ContentPage.Resources>
|
||||||
@@ -53,7 +50,9 @@
|
|||||||
Keyboard="Email"
|
Keyboard="Email"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
ReturnType="Go"
|
ReturnType="Go"
|
||||||
ReturnCommand="{Binding ContinueCommand}">
|
ReturnCommand="{Binding ContinueCommand}"
|
||||||
|
AutomationId="EmailAddressEntry"
|
||||||
|
>
|
||||||
<VisualStateManager.VisualStateGroups>
|
<VisualStateManager.VisualStateGroups>
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
<VisualState x:Name="Disabled">
|
<VisualState x:Name="Disabled">
|
||||||
@@ -66,7 +65,28 @@
|
|||||||
</Entry>
|
</Entry>
|
||||||
<StackLayout
|
<StackLayout
|
||||||
Orientation="Horizontal"
|
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>
|
<StackLayout.GestureRecognizers>
|
||||||
<TapGestureRecognizer
|
<TapGestureRecognizer
|
||||||
Command="{Binding RememberEmailCommand}" />
|
Command="{Binding RememberEmailCommand}" />
|
||||||
@@ -76,21 +96,27 @@
|
|||||||
StyleClass="text-sm"
|
StyleClass="text-sm"
|
||||||
HorizontalOptions="FillAndExpand"
|
HorizontalOptions="FillAndExpand"
|
||||||
VerticalOptions="Center"
|
VerticalOptions="Center"
|
||||||
VerticalTextAlignment="Center"/>
|
VerticalTextAlignment="Center"
|
||||||
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
Scale="0.8"
|
Scale="0.8"
|
||||||
IsToggled="{Binding RememberEmail}"
|
IsToggled="{Binding RememberEmail}"
|
||||||
VerticalOptions="Center"/>
|
VerticalOptions="Center"
|
||||||
|
AutomationId="RememberMeSwitch"
|
||||||
|
/>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Button Text="{u:I18n Continue}"
|
<Button Text="{u:I18n Continue}"
|
||||||
StyleClass="btn-primary"
|
StyleClass="btn-primary"
|
||||||
IsEnabled="{Binding CanContinue}"
|
IsEnabled="{Binding CanContinue}"
|
||||||
Command="{Binding ContinueCommand}" />
|
Command="{Binding ContinueCommand}"
|
||||||
|
AutomationId="ContinueButton"
|
||||||
|
/>
|
||||||
|
|
||||||
<Label FormattedText="{Binding CreateAccountText}"
|
<Label FormattedText="{Binding CreateAccountText}"
|
||||||
Margin="0, 10"
|
Margin="0, 10"
|
||||||
StyleClass="box-footer-label">
|
StyleClass="box-footer-label"
|
||||||
|
AutomationId="CreateAccountLabel">
|
||||||
<Label.GestureRecognizers>
|
<Label.GestureRecognizers>
|
||||||
<TapGestureRecognizer Command="{Binding CreateAccountCommand}" />
|
<TapGestureRecognizer Command="{Binding CreateAccountCommand}" />
|
||||||
</Label.GestureRecognizers>
|
</Label.GestureRecognizers>
|
||||||
@@ -116,5 +142,4 @@
|
|||||||
MainPage="{Binding Source={x:Reference _page}}"
|
MainPage="{Binding Source={x:Reference _page}}"
|
||||||
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
|
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
|
||||||
</AbsoluteLayout>
|
</AbsoluteLayout>
|
||||||
|
|
||||||
</pages:BaseContentPage>
|
</pages:BaseContentPage>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ namespace Bit.App.Pages
|
|||||||
private readonly AppOptions _appOptions;
|
private readonly AppOptions _appOptions;
|
||||||
private IBroadcasterService _broadcasterService;
|
private IBroadcasterService _broadcasterService;
|
||||||
|
|
||||||
|
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||||
|
|
||||||
public HomePage(AppOptions appOptions = null)
|
public HomePage(AppOptions appOptions = null)
|
||||||
{
|
{
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
@@ -62,7 +64,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
|
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
|
||||||
{
|
{
|
||||||
if (message.Command == "updatedTheme")
|
if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
|
||||||
{
|
{
|
||||||
Device.BeginInvokeOnMainThread(() =>
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
{
|
{
|
||||||
@@ -70,6 +72,14 @@ namespace Bit.App.Pages
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _vm.UpdateEnvironmentAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Value?.Exception(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnBackButtonPressed()
|
protected override bool OnBackButtonPressed()
|
||||||
@@ -128,14 +138,6 @@ namespace Bit.App.Pages
|
|||||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Environment_Clicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (DoOnce())
|
|
||||||
{
|
|
||||||
_vm.StartEnvironmentAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartEnvironmentAsync()
|
private async Task StartEnvironmentAsync()
|
||||||
{
|
{
|
||||||
await _accountListOverlay.HideAsync();
|
await _accountListOverlay.HideAsync();
|
||||||
|
|||||||
@@ -3,30 +3,34 @@ using System.Threading.Tasks;
|
|||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
|
using Bit.App.Styles;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Essentials;
|
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
using BwRegion = Bit.Core.Enums.Region;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class HomeViewModel : BaseViewModel
|
public class HomeViewModel : BaseViewModel
|
||||||
{
|
{
|
||||||
|
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IMessagingService _messagingService;
|
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 _showCancelButton;
|
||||||
private bool _rememberEmail;
|
private bool _rememberEmail;
|
||||||
private string _email;
|
private string _email;
|
||||||
private bool _isEmailEnabled;
|
private string _selectedEnvironmentName;
|
||||||
private bool _canLogin;
|
private bool _displayEuEnvironment;
|
||||||
private IPlatformUtilsService _platformUtilsService;
|
|
||||||
private ILogger _logger;
|
|
||||||
private IEnvironmentService _environmentService;
|
|
||||||
private IAccountsManager _accountManager;
|
|
||||||
|
|
||||||
public HomeViewModel()
|
public HomeViewModel()
|
||||||
{
|
{
|
||||||
@@ -36,6 +40,7 @@ namespace Bit.App.Pages
|
|||||||
_logger = ServiceContainer.Resolve<ILogger>();
|
_logger = ServiceContainer.Resolve<ILogger>();
|
||||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||||
_accountManager = ServiceContainer.Resolve<IAccountsManager>();
|
_accountManager = ServiceContainer.Resolve<IAccountsManager>();
|
||||||
|
_configService = ServiceContainer.Resolve<IConfigService>();
|
||||||
|
|
||||||
PageTitle = AppResources.Bitwarden;
|
PageTitle = AppResources.Bitwarden;
|
||||||
|
|
||||||
@@ -49,6 +54,8 @@ namespace Bit.App.Pages
|
|||||||
onException: _logger.Exception, allowsMultipleExecutions: false);
|
onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||||
CloseCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(CloseAction),
|
CloseCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(CloseAction),
|
||||||
onException: _logger.Exception, allowsMultipleExecutions: false);
|
onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||||
|
ShowEnvironmentPickerCommand = new AsyncCommand(ShowEnvironmentPickerAsync,
|
||||||
|
onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||||
InitAsync().FireAndForget();
|
InitAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +78,13 @@ namespace Bit.App.Pages
|
|||||||
additionalPropertyNames: new[] { nameof(CanContinue) });
|
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 bool CanContinue => !string.IsNullOrEmpty(Email);
|
||||||
|
|
||||||
public FormattedString CreateAccountText
|
public FormattedString CreateAccountText
|
||||||
@@ -101,11 +115,13 @@ namespace Bit.App.Pages
|
|||||||
public AsyncCommand ContinueCommand { get; }
|
public AsyncCommand ContinueCommand { get; }
|
||||||
public AsyncCommand CloseCommand { get; }
|
public AsyncCommand CloseCommand { get; }
|
||||||
public AsyncCommand CreateAccountCommand { get; }
|
public AsyncCommand CreateAccountCommand { get; }
|
||||||
|
public AsyncCommand ShowEnvironmentPickerCommand { get; }
|
||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
Email = await _stateService.GetRememberedEmailAsync();
|
Email = await _stateService.GetRememberedEmailAsync();
|
||||||
RememberEmail = !string.IsNullOrEmpty(Email);
|
RememberEmail = !string.IsNullOrEmpty(Email);
|
||||||
|
_displayEuEnvironment = await _configService.GetFeatureFlagBoolAsync(Constants.DisplayEuEnvironmentFlag, forceRefresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ContinueToLoginStepAsync()
|
public async Task ContinueToLoginStepAsync()
|
||||||
@@ -144,5 +160,47 @@ namespace Bit.App.Pages
|
|||||||
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred, AppResources.Ok);
|
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ShowEnvironmentPickerAsync()
|
||||||
|
{
|
||||||
|
_displayEuEnvironment = await _configService.GetFeatureFlagBoolAsync(Constants.DisplayEuEnvironmentFlag);
|
||||||
|
var options = _displayEuEnvironment
|
||||||
|
? new string[] { BwRegion.US.Domain(), BwRegion.EU.Domain(), AppResources.SelfHosted }
|
||||||
|
: new string[] { BwRegion.US.Domain(), 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.SetRegionAsync(result == BwRegion.EU.Domain() ? BwRegion.EU : BwRegion.US);
|
||||||
|
await _configService.GetAsync(true);
|
||||||
|
SelectedEnvironmentName = result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateEnvironmentAsync()
|
||||||
|
{
|
||||||
|
var region = _environmentService.SelectedRegion;
|
||||||
|
if (region == BwRegion.SelfHosted)
|
||||||
|
{
|
||||||
|
SelectedEnvironmentName = AppResources.SelfHosted;
|
||||||
|
await _configService.GetAsync(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedEnvironmentName = region.Domain();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
Priority="-1"
|
Priority="-1"
|
||||||
UseOriginalImage="True"
|
UseOriginalImage="True"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Account}" />
|
AutomationProperties.Name="{u:I18n Account}"
|
||||||
|
AutomationId="AccountIconButton" />
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ContentPage.Resources>
|
<ContentPage.Resources>
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
<StackLayout StyleClass="box">
|
<StackLayout StyleClass="box">
|
||||||
<Grid
|
<Grid
|
||||||
StyleClass="box-row"
|
StyleClass="box-row"
|
||||||
IsVisible="{Binding PinLock}"
|
IsVisible="{Binding PinEnabled}"
|
||||||
Padding="0, 10, 0, 0">
|
Padding="0, 10, 0, 0">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -71,7 +72,8 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
ReturnType="Go"
|
ReturnType="Go"
|
||||||
ReturnCommand="{Binding SubmitCommand}" />
|
ReturnCommand="{Binding SubmitCommand}"
|
||||||
|
AutomationId="PinEntry" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
@@ -81,12 +83,13 @@
|
|||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||||
|
AutomationId="PinVisibilityToggle" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid
|
<Grid
|
||||||
x:Name="_passwordGrid"
|
x:Name="_passwordGrid"
|
||||||
StyleClass="box-row"
|
StyleClass="box-row"
|
||||||
IsVisible="{Binding PinLock, Converter={StaticResource inverseBool}}"
|
IsVisible="{Binding PinEnabled, Converter={StaticResource inverseBool}}"
|
||||||
Padding="0, 10, 0, 0">
|
Padding="0, 10, 0, 0">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -111,7 +114,8 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
ReturnType="Go"
|
ReturnType="Go"
|
||||||
ReturnCommand="{Binding SubmitCommand}" />
|
ReturnCommand="{Binding SubmitCommand}"
|
||||||
|
AutomationId="MasterPasswordEntry" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
@@ -121,7 +125,9 @@
|
|||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||||
|
AutomationId="PasswordVisibilityToggle"
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<StackLayout
|
<StackLayout
|
||||||
StyleClass="box-row"
|
StyleClass="box-row"
|
||||||
@@ -137,7 +143,7 @@
|
|||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout Padding="10, 0">
|
<StackLayout Padding="10, 0">
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n BiometricInvalidated}"
|
Text="{u:I18n AccountBiometricInvalidated}"
|
||||||
StyleClass="box-footer-label,text-danger,text-bold"
|
StyleClass="box-footer-label,text-danger,text-bold"
|
||||||
IsVisible="{Binding BiometricIntegrityValid, Converter={StaticResource inverseBool}}" />
|
IsVisible="{Binding BiometricIntegrityValid, Converter={StaticResource inverseBool}}" />
|
||||||
<Button Text="{Binding BiometricButtonText}" Clicked="Biometric_Clicked"
|
<Button Text="{Binding BiometricButtonText}" Clicked="Biometric_Clicked"
|
||||||
@@ -147,7 +153,8 @@
|
|||||||
x:Name="_unlockButton"
|
x:Name="_unlockButton"
|
||||||
Text="{u:I18n Unlock}"
|
Text="{u:I18n Unlock}"
|
||||||
StyleClass="btn-primary"
|
StyleClass="btn-primary"
|
||||||
Clicked="Unlock_Clicked" />
|
Clicked="Unlock_Clicked"
|
||||||
|
AutomationId="UnlockVaultButton" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ namespace Bit.App.Pages
|
|||||||
private bool _promptedAfterResume;
|
private bool _promptedAfterResume;
|
||||||
private bool _appeared;
|
private bool _appeared;
|
||||||
|
|
||||||
public LockPage(AppOptions appOptions = null, bool autoPromptBiometric = true)
|
public LockPage(AppOptions appOptions = null, bool autoPromptBiometric = true, bool checkPendingAuthRequests = true)
|
||||||
{
|
{
|
||||||
_appOptions = appOptions;
|
_appOptions = appOptions;
|
||||||
_autoPromptBiometric = autoPromptBiometric;
|
_autoPromptBiometric = autoPromptBiometric;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
||||||
_vm = BindingContext as LockPageViewModel;
|
_vm = BindingContext as LockPageViewModel;
|
||||||
|
_vm.CheckPendingAuthRequests = checkPendingAuthRequests;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_vm?.PinLock ?? false)
|
if (_vm?.PinEnabled ?? false)
|
||||||
{
|
{
|
||||||
return _pin;
|
return _pin;
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task PromptBiometricAfterResumeAsync()
|
public async Task PromptBiometricAfterResumeAsync()
|
||||||
{
|
{
|
||||||
if (_vm.BiometricLock)
|
if (_vm.BiometricEnabled)
|
||||||
{
|
{
|
||||||
await Task.Delay(500);
|
await Task.Delay(500);
|
||||||
if (!_promptedAfterResume)
|
if (!_promptedAfterResume)
|
||||||
@@ -91,13 +92,13 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
_vm.FocusSecretEntry += PerformFocusSecretEntry;
|
_vm.FocusSecretEntry += PerformFocusSecretEntry;
|
||||||
|
|
||||||
if (!_vm.BiometricLock)
|
if (!_vm.BiometricEnabled)
|
||||||
{
|
{
|
||||||
RequestFocus(SecretEntry);
|
RequestFocus(SecretEntry);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_vm.UsingKeyConnector && !_vm.PinLock)
|
if (!_vm.HasMasterPassword && !_vm.PinEnabled)
|
||||||
{
|
{
|
||||||
_passwordGrid.IsVisible = false;
|
_passwordGrid.IsVisible = false;
|
||||||
_unlockButton.IsVisible = false;
|
_unlockButton.IsVisible = false;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Bit.App.Utilities;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.Request;
|
using Bit.Core.Models.Request;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -27,25 +28,27 @@ namespace Bit.App.Pages
|
|||||||
private readonly IEnvironmentService _environmentService;
|
private readonly IEnvironmentService _environmentService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IBiometricService _biometricService;
|
private readonly IBiometricService _biometricService;
|
||||||
private readonly IKeyConnectorService _keyConnectorService;
|
private readonly IUserVerificationService _userVerificationService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IWatchDeviceService _watchDeviceService;
|
private readonly IWatchDeviceService _watchDeviceService;
|
||||||
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
|
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
|
||||||
|
private readonly IPolicyService _policyService;
|
||||||
|
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||||
|
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||||
|
private readonly ISyncService _syncService;
|
||||||
private string _email;
|
private string _email;
|
||||||
private string _masterPassword;
|
private string _masterPassword;
|
||||||
private string _pin;
|
private string _pin;
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
private bool _pinLock;
|
private PinLockType _pinStatus;
|
||||||
private bool _biometricLock;
|
private bool _pinEnabled;
|
||||||
|
private bool _biometricEnabled;
|
||||||
private bool _biometricIntegrityValid = true;
|
private bool _biometricIntegrityValid = true;
|
||||||
private bool _biometricButtonVisible;
|
private bool _biometricButtonVisible;
|
||||||
private bool _usingKeyConnector;
|
private bool _hasMasterPassword;
|
||||||
private string _biometricButtonText;
|
private string _biometricButtonText;
|
||||||
private string _loggedInAsText;
|
private string _loggedInAsText;
|
||||||
private string _lockedVerifyText;
|
private string _lockedVerifyText;
|
||||||
private bool _isPinProtected;
|
|
||||||
private bool _isPinProtectedWithKey;
|
|
||||||
|
|
||||||
public LockPageViewModel()
|
public LockPageViewModel()
|
||||||
{
|
{
|
||||||
@@ -58,19 +61,24 @@ namespace Bit.App.Pages
|
|||||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
||||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
||||||
|
_policyService = ServiceContainer.Resolve<IPolicyService>();
|
||||||
|
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
|
||||||
|
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||||
|
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||||
|
|
||||||
PageTitle = AppResources.VerifyMasterPassword;
|
PageTitle = AppResources.VerifyMasterPassword;
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||||
|
|
||||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
AccountSwitchingOverlayViewModel =
|
||||||
{
|
new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||||
AllowAddAccountRow = true,
|
{
|
||||||
AllowActiveAccountSelection = true
|
AllowAddAccountRow = true,
|
||||||
};
|
AllowActiveAccountSelection = true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public string MasterPassword
|
public string MasterPassword
|
||||||
@@ -96,21 +104,21 @@ namespace Bit.App.Pages
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool PinLock
|
public bool PinEnabled
|
||||||
{
|
{
|
||||||
get => _pinLock;
|
get => _pinEnabled;
|
||||||
set => SetProperty(ref _pinLock, value);
|
set => SetProperty(ref _pinEnabled, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool UsingKeyConnector
|
public bool HasMasterPassword
|
||||||
{
|
{
|
||||||
get => _usingKeyConnector;
|
get => _hasMasterPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool BiometricLock
|
public bool BiometricEnabled
|
||||||
{
|
{
|
||||||
get => _biometricLock;
|
get => _biometricEnabled;
|
||||||
set => SetProperty(ref _biometricLock, value);
|
set => SetProperty(ref _biometricEnabled, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool BiometricIntegrityValid
|
public bool BiometricIntegrityValid
|
||||||
@@ -143,12 +151,18 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _lockedVerifyText, value);
|
set => SetProperty(ref _lockedVerifyText, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool CheckPendingAuthRequests { get; set; }
|
||||||
|
|
||||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||||
|
|
||||||
public Command SubmitCommand { get; }
|
public Command SubmitCommand { get; }
|
||||||
public Command TogglePasswordCommand { get; }
|
public Command TogglePasswordCommand { get; }
|
||||||
|
|
||||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword
|
||||||
|
? AppResources.PasswordIsVisibleTapToHide
|
||||||
|
: AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
|
|
||||||
public Action UnlockedAction { get; set; }
|
public Action UnlockedAction { get; set; }
|
||||||
public event Action<int?> FocusSecretEntry
|
public event Action<int?> FocusSecretEntry
|
||||||
{
|
{
|
||||||
@@ -158,18 +172,33 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
(_isPinProtected, _isPinProtectedWithKey) = await _vaultTimeoutService.IsPinLockSetAsync();
|
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||||
PinLock = (_isPinProtected && await _stateService.GetPinProtectedKeyAsync() != null) ||
|
if (pendingRequest != null && CheckPendingAuthRequests)
|
||||||
_isPinProtectedWithKey;
|
|
||||||
BiometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _cryptoService.HasKeyAsync();
|
|
||||||
|
|
||||||
// Users with key connector and without biometric or pin has no MP to unlock with
|
|
||||||
_usingKeyConnector = await _keyConnectorService.GetUsesKeyConnector();
|
|
||||||
if (_usingKeyConnector && !(BiometricLock || PinLock))
|
|
||||||
{
|
{
|
||||||
await _vaultTimeoutService.LogOutAsync();
|
await _vaultTimeoutService.LogOutAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_pinStatus = await _vaultTimeoutService.GetPinLockTypeAsync();
|
||||||
|
|
||||||
|
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
|
||||||
|
?? await _stateService.GetPinProtectedKeyAsync();
|
||||||
|
PinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) ||
|
||||||
|
_pinStatus == PinLockType.Persistent;
|
||||||
|
|
||||||
|
BiometricEnabled = await IsBiometricsEnabledAsync();
|
||||||
|
|
||||||
|
// Users without MP and without biometric or pin has no MP to unlock with
|
||||||
|
_hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync();
|
||||||
|
if (await _stateService.IsAuthenticatedAsync()
|
||||||
|
&& !_hasMasterPassword
|
||||||
|
&& !BiometricEnabled
|
||||||
|
&& !PinEnabled)
|
||||||
|
{
|
||||||
|
await _vaultTimeoutService.LogOutAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_email = await _stateService.GetEmailAsync();
|
_email = await _stateService.GetEmailAsync();
|
||||||
if (string.IsNullOrWhiteSpace(_email))
|
if (string.IsNullOrWhiteSpace(_email))
|
||||||
{
|
{
|
||||||
@@ -177,35 +206,24 @@ namespace Bit.App.Pages
|
|||||||
_logger.Exception(new NullReferenceException("Email not found in storage"));
|
_logger.Exception(new NullReferenceException("Email not found in storage"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var webVault = _environmentService.GetWebVaultUrl(true);
|
|
||||||
if (string.IsNullOrWhiteSpace(webVault))
|
LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, _environmentService.GetCurrentDomain());
|
||||||
{
|
if (PinEnabled)
|
||||||
webVault = "https://bitwarden.com";
|
|
||||||
}
|
|
||||||
var webVaultHostname = CoreHelpers.GetHostname(webVault);
|
|
||||||
LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, webVaultHostname);
|
|
||||||
if (PinLock)
|
|
||||||
{
|
{
|
||||||
PageTitle = AppResources.VerifyPIN;
|
PageTitle = AppResources.VerifyPIN;
|
||||||
LockedVerifyText = AppResources.VaultLockedPIN;
|
LockedVerifyText = AppResources.VaultLockedPIN;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_usingKeyConnector)
|
PageTitle = _hasMasterPassword ? AppResources.VerifyMasterPassword : AppResources.UnlockVault;
|
||||||
{
|
LockedVerifyText = _hasMasterPassword
|
||||||
PageTitle = AppResources.UnlockVault;
|
? AppResources.VaultLockedMasterPassword
|
||||||
LockedVerifyText = AppResources.VaultLockedIdentity;
|
: AppResources.VaultLockedIdentity;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PageTitle = AppResources.VerifyMasterPassword;
|
|
||||||
LockedVerifyText = AppResources.VaultLockedMasterPassword;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BiometricLock)
|
if (BiometricEnabled)
|
||||||
{
|
{
|
||||||
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
|
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
||||||
if (!_biometricIntegrityValid)
|
if (!_biometricIntegrityValid)
|
||||||
{
|
{
|
||||||
BiometricButtonVisible = false;
|
BiometricButtonVisible = false;
|
||||||
@@ -219,20 +237,119 @@ namespace Bit.App.Pages
|
|||||||
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
|
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
|
||||||
AppResources.UseFingerprintToUnlock;
|
AppResources.UseFingerprintToUnlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SubmitAsync()
|
public async Task SubmitAsync()
|
||||||
{
|
{
|
||||||
if (PinLock && string.IsNullOrWhiteSpace(Pin))
|
ShowPassword = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||||
|
if (PinEnabled)
|
||||||
|
{
|
||||||
|
await UnlockWithPinAsync(kdfConfig);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UnlockWithMasterPasswordAsync(kdfConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (LegacyUserException)
|
||||||
|
{
|
||||||
|
await HandleLegacyUserAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UnlockWithPinAsync(KdfConfig kdfConfig)
|
||||||
|
{
|
||||||
|
if (PinEnabled && string.IsNullOrWhiteSpace(Pin))
|
||||||
{
|
{
|
||||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||||
string.Format(AppResources.ValidationFieldRequired, AppResources.PIN),
|
string.Format(AppResources.ValidationFieldRequired, AppResources.PIN),
|
||||||
AppResources.Ok);
|
AppResources.Ok);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!PinLock && string.IsNullOrWhiteSpace(MasterPassword))
|
|
||||||
|
var failed = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EncString userKeyPin;
|
||||||
|
EncString oldPinProtected;
|
||||||
|
switch (_pinStatus)
|
||||||
|
{
|
||||||
|
case PinLockType.Persistent:
|
||||||
|
{
|
||||||
|
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
|
||||||
|
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
|
||||||
|
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PinLockType.Transient:
|
||||||
|
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
|
||||||
|
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
|
||||||
|
break;
|
||||||
|
case PinLockType.Disabled:
|
||||||
|
default:
|
||||||
|
throw new Exception("Pin is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
UserKey userKey;
|
||||||
|
if (oldPinProtected != null)
|
||||||
|
{
|
||||||
|
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
|
||||||
|
_pinStatus == PinLockType.Transient,
|
||||||
|
Pin,
|
||||||
|
_email,
|
||||||
|
kdfConfig,
|
||||||
|
oldPinProtected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
|
||||||
|
Pin,
|
||||||
|
_email,
|
||||||
|
kdfConfig,
|
||||||
|
userKeyPin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var protectedPin = await _stateService.GetProtectedPinAsync();
|
||||||
|
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
|
||||||
|
failed = decryptedPin != Pin;
|
||||||
|
if (!failed)
|
||||||
|
{
|
||||||
|
Pin = string.Empty;
|
||||||
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
|
await SetUserKeyAndContinueAsync(userKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (LegacyUserException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
if (failed)
|
||||||
|
{
|
||||||
|
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||||
|
if (invalidUnlockAttempts >= 5)
|
||||||
|
{
|
||||||
|
_messagingService.Send("logout");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN,
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UnlockWithMasterPasswordAsync(KdfConfig kdfConfig)
|
||||||
|
{
|
||||||
|
if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword))
|
||||||
{
|
{
|
||||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||||
@@ -240,118 +357,112 @@ namespace Bit.App.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowPassword = false;
|
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig);
|
||||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
if (await _cryptoService.IsLegacyUserAsync(masterKey))
|
||||||
|
|
||||||
if (PinLock)
|
|
||||||
{
|
{
|
||||||
var failed = true;
|
throw new LegacyUserException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync();
|
||||||
|
var passwordValid = false;
|
||||||
|
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
|
||||||
|
|
||||||
|
if (storedKeyHash != null)
|
||||||
|
{
|
||||||
|
// Offline unlock possible
|
||||||
|
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Online unlock required
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||||
|
var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey,
|
||||||
|
HashPurpose.ServerAuthorization);
|
||||||
|
var request = new PasswordVerificationRequest();
|
||||||
|
request.MasterPasswordHash = keyHash;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_isPinProtected)
|
var response = await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||||
{
|
enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
|
||||||
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email,
|
passwordValid = true;
|
||||||
kdfConfig,
|
var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey,
|
||||||
await _stateService.GetPinProtectedKeyAsync());
|
HashPurpose.LocalAuthorization);
|
||||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
await _cryptoService.SetMasterKeyHashAsync(localKeyHash);
|
||||||
var protectedPin = await _stateService.GetProtectedPinAsync();
|
|
||||||
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
|
|
||||||
failed = decPin != Pin;
|
|
||||||
if (!failed)
|
|
||||||
{
|
|
||||||
Pin = string.Empty;
|
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
|
||||||
await SetKeyAndContinueAsync(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email, kdfConfig);
|
|
||||||
failed = false;
|
|
||||||
Pin = string.Empty;
|
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
|
||||||
await SetKeyAndContinueAsync(key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
failed = true;
|
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
||||||
}
|
}
|
||||||
if (failed)
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordValid)
|
||||||
|
{
|
||||||
|
if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions))
|
||||||
{
|
{
|
||||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
// Save the ForcePasswordResetReason to force a password reset after unlock
|
||||||
if (invalidUnlockAttempts >= 5)
|
await _stateService.SetForcePasswordResetReasonAsync(
|
||||||
{
|
ForcePasswordResetReason.WeakMasterPasswordOnLogin);
|
||||||
_messagingService.Send("logout");
|
}
|
||||||
return;
|
|
||||||
}
|
MasterPassword = string.Empty;
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN,
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
AppResources.AnErrorHasOccurred);
|
|
||||||
|
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
||||||
|
await _cryptoService.SetMasterKeyAsync(masterKey);
|
||||||
|
await SetUserKeyAndContinueAsync(userKey);
|
||||||
|
|
||||||
|
// Re-enable biometrics
|
||||||
|
if (BiometricEnabled & !BiometricIntegrityValid)
|
||||||
|
{
|
||||||
|
await _biometricService.SetupBiometricAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdfConfig);
|
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
if (invalidUnlockAttempts >= 5)
|
||||||
var passwordValid = false;
|
|
||||||
|
|
||||||
if (storedKeyHash != null)
|
|
||||||
{
|
{
|
||||||
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, key);
|
_messagingService.Send("logout");
|
||||||
}
|
return;
|
||||||
else
|
|
||||||
{
|
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
|
||||||
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
|
|
||||||
var request = new PasswordVerificationRequest();
|
|
||||||
request.MasterPasswordHash = keyHash;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _apiService.PostAccountVerifyPasswordAsync(request);
|
|
||||||
passwordValid = true;
|
|
||||||
var localKeyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
|
|
||||||
await _cryptoService.SetKeyHashAsync(localKeyHash);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
|
||||||
}
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
|
||||||
}
|
|
||||||
if (passwordValid)
|
|
||||||
{
|
|
||||||
if (_isPinProtected)
|
|
||||||
{
|
|
||||||
var protectedPin = await _stateService.GetProtectedPinAsync();
|
|
||||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
|
||||||
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
|
|
||||||
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, _email, kdfConfig);
|
|
||||||
await _stateService.SetPinProtectedKeyAsync(await _cryptoService.EncryptAsync(key.Key, pinKey));
|
|
||||||
}
|
|
||||||
MasterPassword = string.Empty;
|
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
|
||||||
await SetKeyAndContinueAsync(key);
|
|
||||||
|
|
||||||
// Re-enable biometrics
|
|
||||||
if (BiometricLock & !BiometricIntegrityValid)
|
|
||||||
{
|
|
||||||
await _biometricService.SetupBiometricAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
|
||||||
if (invalidUnlockAttempts >= 5)
|
|
||||||
{
|
|
||||||
_messagingService.Send("logout");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword,
|
|
||||||
AppResources.AnErrorHasOccurred);
|
|
||||||
}
|
}
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword,
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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()
|
public async Task LogOutAsync()
|
||||||
{
|
{
|
||||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation,
|
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation,
|
||||||
@@ -379,43 +490,82 @@ namespace Bit.App.Pages
|
|||||||
public void TogglePassword()
|
public void TogglePassword()
|
||||||
{
|
{
|
||||||
ShowPassword = !ShowPassword;
|
ShowPassword = !ShowPassword;
|
||||||
var secret = PinLock ? Pin : MasterPassword;
|
var secret = PinEnabled ? Pin : MasterPassword;
|
||||||
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
|
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length,
|
||||||
|
nameof(FocusSecretEntry));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PromptBiometricAsync()
|
public async Task PromptBiometricAsync()
|
||||||
{
|
{
|
||||||
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
|
try
|
||||||
if (!BiometricLock || !BiometricIntegrityValid)
|
|
||||||
{
|
{
|
||||||
return;
|
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
||||||
|
BiometricButtonVisible = BiometricIntegrityValid;
|
||||||
|
if (!BiometricEnabled || !BiometricIntegrityValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||||
|
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||||
|
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)),
|
||||||
|
!PinEnabled && !HasMasterPassword);
|
||||||
|
|
||||||
|
await _stateService.SetBiometricLockedAsync(!success);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
|
||||||
|
await SetUserKeyAndContinueAsync(userKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
catch (LegacyUserException)
|
||||||
PinLock ? AppResources.PIN : AppResources.MasterPassword,
|
|
||||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
|
|
||||||
await _stateService.SetBiometricLockedAsync(!success);
|
|
||||||
if (success)
|
|
||||||
{
|
{
|
||||||
await DoContinueAsync();
|
await HandleLegacyUserAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key)
|
private async Task SetUserKeyAndContinueAsync(UserKey key)
|
||||||
{
|
{
|
||||||
var hasKey = await _cryptoService.HasKeyAsync();
|
var hasKey = await _cryptoService.HasUserKeyAsync();
|
||||||
if (!hasKey)
|
if (!hasKey)
|
||||||
{
|
{
|
||||||
await _cryptoService.SetKeyAsync(key);
|
await _cryptoService.SetUserKeyAsync(key);
|
||||||
}
|
}
|
||||||
|
await _deviceTrustCryptoService.TrustDeviceIfNeededAsync();
|
||||||
await DoContinueAsync();
|
await DoContinueAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DoContinueAsync()
|
private async Task DoContinueAsync()
|
||||||
{
|
{
|
||||||
|
_syncService.FullSyncAsync(false).FireAndForget();
|
||||||
await _stateService.SetBiometricLockedAsync(false);
|
await _stateService.SetBiometricLockedAsync(false);
|
||||||
_watchDeviceService.SyncDataToWatchAsync().FireAndForget();
|
_watchDeviceService.SyncDataToWatchAsync().FireAndForget();
|
||||||
_messagingService.Send("unlocked");
|
_messagingService.Send("unlocked");
|
||||||
UnlockedAction?.Invoke();
|
UnlockedAction?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsBiometricsEnabledAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _vaultTimeoutService.IsBiometricLockSetAsync() &&
|
||||||
|
await _biometricService.CanUseBiometricsUnlockAsync();
|
||||||
|
}
|
||||||
|
catch (LegacyUserException)
|
||||||
|
{
|
||||||
|
await HandleLegacyUserAsync();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleLegacyUserAsync()
|
||||||
|
{
|
||||||
|
// Legacy users must migrate on web vault.
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong,
|
||||||
|
AppResources.AnErrorHasOccurred,
|
||||||
|
AppResources.Ok);
|
||||||
|
await _vaultTimeoutService.LogOutAsync();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
src/App/Pages/Accounts/LoginApproveDevicePage.xaml
Normal file
80
src/App/Pages/Accounts/LoginApproveDevicePage.xaml
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<pages:BaseContentPage
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Pages.LoginApproveDevicePage"
|
||||||
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
x:DataType="pages:LoginApproveDeviceViewModel"
|
||||||
|
x:Name="_page"
|
||||||
|
Title="{Binding PageTitle}">
|
||||||
|
|
||||||
|
<ContentPage.BindingContext>
|
||||||
|
<pages:LoginApproveDeviceViewModel />
|
||||||
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
|
<StackLayout Padding="10, 10">
|
||||||
|
<StackLayout Padding="5, 10" Orientation="Horizontal">
|
||||||
|
<StackLayout HorizontalOptions="FillAndExpand">
|
||||||
|
<Label
|
||||||
|
StyleClass="text-md"
|
||||||
|
Text="{u:I18n RememberThisDevice}" />
|
||||||
|
<Label
|
||||||
|
StyleClass="box-sub-label"
|
||||||
|
Text="{u:I18n TurnOffUsingPublicDevice}" />
|
||||||
|
</StackLayout>
|
||||||
|
<Switch
|
||||||
|
Scale="0.8"
|
||||||
|
IsToggled="{Binding RememberThisDevice}"
|
||||||
|
AutomationId="RememberThisDeviceSwitch"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
</StackLayout>
|
||||||
|
<StackLayout Margin="0, 20, 0, 0">
|
||||||
|
<Button
|
||||||
|
x:Name="_continue"
|
||||||
|
Text="{u:I18n Continue}"
|
||||||
|
StyleClass="btn-primary"
|
||||||
|
Command="{Binding ContinueCommand}"
|
||||||
|
IsVisible="{Binding IsNewUser}"
|
||||||
|
AutomationId="ContinueButton" />
|
||||||
|
<Button
|
||||||
|
x:Name="_approveWithMyOtherDevice"
|
||||||
|
Text="{u:I18n ApproveWithMyOtherDevice}"
|
||||||
|
StyleClass="btn-primary"
|
||||||
|
Command="{Binding ApproveWithMyOtherDeviceCommand}"
|
||||||
|
IsVisible="{Binding ApproveWithMyOtherDeviceEnabled}"
|
||||||
|
AutomationId="ApproveWithMyOtherDeviceButton" />
|
||||||
|
<Button
|
||||||
|
x:Name="_requestAdminApproval"
|
||||||
|
Text="{u:I18n RequestAdminApproval}"
|
||||||
|
StyleClass="box-button-row"
|
||||||
|
Command="{Binding RequestAdminApprovalCommand}"
|
||||||
|
IsVisible="{Binding RequestAdminApprovalEnabled}"
|
||||||
|
AutomationId="RequestAdminApprovalButton" />
|
||||||
|
<Button
|
||||||
|
x:Name="_approveWithMasterPassword"
|
||||||
|
Text="{u:I18n ApproveWithMasterPassword}"
|
||||||
|
StyleClass="box-button-row"
|
||||||
|
Command="{Binding ApproveWithMasterPasswordCommand}"
|
||||||
|
IsVisible="{Binding ApproveWithMasterPasswordEnabled}"
|
||||||
|
AutomationId="ApproveWithMasterPasswordButton" />
|
||||||
|
<Label
|
||||||
|
Text="{Binding LoggingInAsText}"
|
||||||
|
StyleClass="text-sm"
|
||||||
|
Margin="0,40,0,0"
|
||||||
|
AutomationId="LoggingInAsLabel" />
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n NotYou}"
|
||||||
|
StyleClass="text-md"
|
||||||
|
HorizontalOptions="Start"
|
||||||
|
TextColor="{DynamicResource HyperlinkColor}"
|
||||||
|
AutomationId="NotYouLabel">
|
||||||
|
<Label.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding LogoutCommand}" />
|
||||||
|
</Label.GestureRecognizers>
|
||||||
|
</Label>
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
</pages:BaseContentPage>
|
||||||
|
|
||||||
|
|
||||||
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"
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
x:DataType="pages:LoginPageViewModel"
|
x:DataType="pages:LoginPageViewModel"
|
||||||
x:Name="_page"
|
x:Name="_page"
|
||||||
Title="{Binding PageTitle}">
|
Title="{Binding PageTitle}"
|
||||||
|
AutomationId="PageTitleLabel">
|
||||||
|
|
||||||
<ContentPage.BindingContext>
|
<ContentPage.BindingContext>
|
||||||
<pages:LoginPageViewModel />
|
<pages:LoginPageViewModel />
|
||||||
</ContentPage.BindingContext>
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<controls:ExtendedToolbarItem
|
<controls:ExtendedToolbarItem
|
||||||
x:Name="_accountAvatar"
|
x:Name="_accountAvatar"
|
||||||
x:Key="accountAvatar"
|
|
||||||
IconImageSource="{Binding AvatarImageSource}"
|
IconImageSource="{Binding AvatarImageSource}"
|
||||||
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
|
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
|
||||||
Order="Primary"
|
Order="Primary"
|
||||||
Priority="-1"
|
Priority="-1"
|
||||||
UseOriginalImage="True"
|
UseOriginalImage="True"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Account}" />
|
AutomationProperties.Name="{u:I18n Account}"
|
||||||
|
AutomationId="AccountIconButton" />
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ContentPage.Resources>
|
<ContentPage.Resources>
|
||||||
@@ -34,7 +34,8 @@
|
|||||||
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
||||||
x:Name="_moreItem" x:Key="moreItem"
|
x:Name="_moreItem" x:Key="moreItem"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
AutomationProperties.Name="{u:I18n Options}"
|
||||||
|
AutomationId="OptionsButton" />
|
||||||
<ToolbarItem Text="{u:I18n GetPasswordHint}"
|
<ToolbarItem Text="{u:I18n GetPasswordHint}"
|
||||||
x:Key="getPasswordHint"
|
x:Key="getPasswordHint"
|
||||||
x:Name="_getPasswordHint"
|
x:Name="_getPasswordHint"
|
||||||
@@ -75,7 +76,9 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
ReturnType="Go"
|
ReturnType="Go"
|
||||||
ReturnCommand="{Binding LogInCommand}" />
|
ReturnCommand="{Binding LogInCommand}"
|
||||||
|
AutomationId="MasterPasswordEntry"
|
||||||
|
/>
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
@@ -84,6 +87,7 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.RowSpan="1"
|
Grid.RowSpan="1"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationId="PasswordVisibilityToggle"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||||
<Label
|
<Label
|
||||||
@@ -93,7 +97,9 @@
|
|||||||
Padding="0,5,0,0"
|
Padding="0,5,0,0"
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.ColumnSpan="2">
|
Grid.ColumnSpan="2"
|
||||||
|
AutomationId="GetMasterPasswordHintLabel"
|
||||||
|
>
|
||||||
<Label.GestureRecognizers>
|
<Label.GestureRecognizers>
|
||||||
<TapGestureRecognizer Tapped="Hint_Clicked" />
|
<TapGestureRecognizer Tapped="Hint_Clicked" />
|
||||||
</Label.GestureRecognizers>
|
</Label.GestureRecognizers>
|
||||||
@@ -104,19 +110,24 @@
|
|||||||
<Button x:Name="_loginWithMasterPassword"
|
<Button x:Name="_loginWithMasterPassword"
|
||||||
Text="{u:I18n LogInWithMasterPassword}"
|
Text="{u:I18n LogInWithMasterPassword}"
|
||||||
StyleClass="btn-primary"
|
StyleClass="btn-primary"
|
||||||
Clicked="LogIn_Clicked" />
|
Clicked="LogIn_Clicked"
|
||||||
|
AutomationId="LogInWithMasterPasswordButton"
|
||||||
|
/>
|
||||||
<controls:IconLabelButton
|
<controls:IconLabelButton
|
||||||
HorizontalOptions="Fill"
|
HorizontalOptions="Fill"
|
||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
|
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
|
||||||
Label="{u:I18n LogInWithAnotherDevice}"
|
Label="{u:I18n LogInWithAnotherDevice}"
|
||||||
ButtonCommand="{Binding LogInWithDeviceCommand}"
|
ButtonCommand="{Binding LogInWithDeviceCommand}"
|
||||||
IsVisible="{Binding IsKnownDevice}"/>
|
IsVisible="{Binding IsKnownDevice}"
|
||||||
|
AutomationId="LogInWithAnotherDeviceButton"
|
||||||
|
/>
|
||||||
<controls:IconLabelButton
|
<controls:IconLabelButton
|
||||||
HorizontalOptions="Fill"
|
HorizontalOptions="Fill"
|
||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Suitcase}}"
|
Icon="{Binding Source={x:Static core:BitwardenIcons.Suitcase}}"
|
||||||
Label="{u:I18n LogInSso}">
|
Label="{u:I18n LogInSso}"
|
||||||
|
AutomationId="LogInWithSsoButton">
|
||||||
<controls:IconLabelButton.GestureRecognizers>
|
<controls:IconLabelButton.GestureRecognizers>
|
||||||
<TapGestureRecognizer Tapped="LogInSSO_Clicked" />
|
<TapGestureRecognizer Tapped="LogInSSO_Clicked" />
|
||||||
</controls:IconLabelButton.GestureRecognizers>
|
</controls:IconLabelButton.GestureRecognizers>
|
||||||
@@ -124,12 +135,15 @@
|
|||||||
<Label
|
<Label
|
||||||
Text="{Binding LoggingInAsText}"
|
Text="{Binding LoggingInAsText}"
|
||||||
StyleClass="text-sm"
|
StyleClass="text-sm"
|
||||||
Margin="0,40,0,0"/>
|
Margin="0,40,0,0"
|
||||||
|
AutomationId="LoggingInAsLabel"
|
||||||
|
/>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n NotYou}"
|
Text="{u:I18n NotYou}"
|
||||||
StyleClass="text-md"
|
StyleClass="text-md"
|
||||||
HorizontalOptions="Start"
|
HorizontalOptions="Start"
|
||||||
TextColor="{DynamicResource HyperlinkColor}">
|
TextColor="{DynamicResource HyperlinkColor}"
|
||||||
|
AutomationId="NotYouLabel">
|
||||||
<Label.GestureRecognizers>
|
<Label.GestureRecognizers>
|
||||||
<TapGestureRecognizer Tapped="Cancel_Clicked" />
|
<TapGestureRecognizer Tapped="Cancel_Clicked" />
|
||||||
</Label.GestureRecognizers>
|
</Label.GestureRecognizers>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Bit.App.Models;
|
|||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
@@ -135,7 +136,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private async Task StartLoginWithDeviceAsync()
|
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));
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace Bit.App.Pages
|
|||||||
private bool _isEmailEnabled;
|
private bool _isEmailEnabled;
|
||||||
private bool _isKnownDevice;
|
private bool _isKnownDevice;
|
||||||
private bool _isExecutingLogin;
|
private bool _isExecutingLogin;
|
||||||
|
private string _environmentHostName;
|
||||||
|
|
||||||
public LoginPageViewModel()
|
public LoginPageViewModel()
|
||||||
{
|
{
|
||||||
@@ -115,6 +116,16 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _isKnownDevice, value);
|
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 AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||||
public Command LogInCommand { get; }
|
public Command LogInCommand { get; }
|
||||||
public Command TogglePasswordCommand { get; }
|
public Command TogglePasswordCommand { get; }
|
||||||
@@ -122,7 +133,7 @@ namespace Bit.App.Pages
|
|||||||
public ICommand LogInWithDeviceCommand { get; }
|
public ICommand LogInWithDeviceCommand { get; }
|
||||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
|
public string LoggingInAsText => string.Format(AppResources.LoggingInAsXOnY, Email, EnvironmentDomainName);
|
||||||
public bool IsIosExtension { get; set; }
|
public bool IsIosExtension { get; set; }
|
||||||
public bool CanRemoveAccount { get; set; }
|
public bool CanRemoveAccount { get; set; }
|
||||||
public Action StartTwoFactorAction { get; set; }
|
public Action StartTwoFactorAction { get; set; }
|
||||||
@@ -151,6 +162,7 @@ namespace Bit.App.Pages
|
|||||||
Email = await _stateService.GetRememberedEmailAsync();
|
Email = await _stateService.GetRememberedEmailAsync();
|
||||||
}
|
}
|
||||||
CanRemoveAccount = await _stateService.GetActiveUserEmailAsync() != Email;
|
CanRemoveAccount = await _stateService.GetActiveUserEmailAsync() != Email;
|
||||||
|
EnvironmentDomainName = _environmentService.GetCurrentDomain();
|
||||||
IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, await _appIdService.GetAppIdAsync());
|
IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, await _appIdService.GetAppIdAsync());
|
||||||
}
|
}
|
||||||
catch (ApiException apiEx) when (apiEx.Error.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
catch (ApiException apiEx) when (apiEx.Error.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
@@ -236,6 +248,14 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
|
||||||
|
if (response.RequiresEncryptionKeyMigration)
|
||||||
|
{
|
||||||
|
// Legacy users must migrate on web vault.
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong, AppResources.AnErrorHasOccurred,
|
||||||
|
AppResources.Ok);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.TwoFactor)
|
if (response.TwoFactor)
|
||||||
{
|
{
|
||||||
StartTwoFactorAction?.Invoke();
|
StartTwoFactorAction?.Invoke();
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
<Label
|
<Label
|
||||||
Text="{Binding LogInAttemptByLabel}"
|
Text="{Binding LogInAttemptByLabel}"
|
||||||
FontSize="Small"
|
FontSize="Small"
|
||||||
Margin="0,0,0,24"/>
|
Margin="0,0,0,24"
|
||||||
|
AutomationId="LogInAttemptByLabel" />
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n FingerprintPhrase}"
|
Text="{u:I18n FingerprintPhrase}"
|
||||||
FontSize="Small"
|
FontSize="Small"
|
||||||
@@ -41,7 +42,8 @@
|
|||||||
FormattedText="{Binding LoginRequest.FingerprintPhrase}"
|
FormattedText="{Binding LoginRequest.FingerprintPhrase}"
|
||||||
FontSize="Medium"
|
FontSize="Medium"
|
||||||
TextColor="{DynamicResource FingerprintPhrase}"
|
TextColor="{DynamicResource FingerprintPhrase}"
|
||||||
Margin="0,0,0,27"/>
|
Margin="0,0,0,27"
|
||||||
|
AutomationId="FingerprintValueLabel" />
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n DeviceType}"
|
Text="{u:I18n DeviceType}"
|
||||||
FontSize="Small"
|
FontSize="Small"
|
||||||
@@ -49,7 +51,8 @@
|
|||||||
<Label
|
<Label
|
||||||
Text="{Binding LoginRequest.DeviceType}"
|
Text="{Binding LoginRequest.DeviceType}"
|
||||||
FontSize="Small"
|
FontSize="Small"
|
||||||
Margin="0,0,0,21"/>
|
Margin="0,0,0,21"
|
||||||
|
AutomationId="DeviceTypeValueLabel" />
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n IpAddress}"
|
Text="{u:I18n IpAddress}"
|
||||||
IsVisible="{Binding ShowIpAddress}"
|
IsVisible="{Binding ShowIpAddress}"
|
||||||
@@ -59,7 +62,8 @@
|
|||||||
Text="{Binding LoginRequest.IpAddress}"
|
Text="{Binding LoginRequest.IpAddress}"
|
||||||
IsVisible="{Binding ShowIpAddress}"
|
IsVisible="{Binding ShowIpAddress}"
|
||||||
FontSize="Small"
|
FontSize="Small"
|
||||||
Margin="0,0,0,21"/>
|
Margin="0,0,0,21"
|
||||||
|
AutomationId="IpAddressValueLabel" />
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n Time}"
|
Text="{u:I18n Time}"
|
||||||
FontSize="Small"
|
FontSize="Small"
|
||||||
@@ -67,7 +71,8 @@
|
|||||||
<Label
|
<Label
|
||||||
Text="{Binding TimeOfRequestText}"
|
Text="{Binding TimeOfRequestText}"
|
||||||
FontSize="Small"
|
FontSize="Small"
|
||||||
Margin="0,0,0,57"/>
|
Margin="0,0,0,57"
|
||||||
|
AutomationId="TimeOfRequestValueLabel" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
@@ -75,11 +80,13 @@
|
|||||||
Text="{u:I18n ConfirmLogIn}"
|
Text="{u:I18n ConfirmLogIn}"
|
||||||
Command="{Binding AcceptRequestCommand}"
|
Command="{Binding AcceptRequestCommand}"
|
||||||
Margin="0,0,0,17"
|
Margin="0,0,0,17"
|
||||||
StyleClass="btn-primary"/>
|
StyleClass="btn-primary"
|
||||||
|
AutomationId="ConfirmLoginButton" />
|
||||||
<Button
|
<Button
|
||||||
Text="{u:I18n DenyLogIn}"
|
Text="{u:I18n DenyLogIn}"
|
||||||
Command="{Binding RejectRequestCommand}"
|
Command="{Binding RejectRequestCommand}"
|
||||||
StyleClass="btn-secundary"/>
|
StyleClass="btn-secundary"
|
||||||
|
AutomationId="DenyLoginButton" />
|
||||||
|
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</pages:BaseContentPage>
|
</pages:BaseContentPage>
|
||||||
|
|||||||
@@ -14,63 +14,67 @@
|
|||||||
</ContentPage.BindingContext>
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<ToolbarItem Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1" x:Name="_closeItem"/>
|
<ToolbarItem Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1" x:Name="_closeItem" />
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<StackLayout
|
<StackLayout
|
||||||
Padding="7, 0, 7, 20">
|
Padding="7, 0, 7, 20">
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n LogInInitiated}"
|
Text="{Binding Title}"
|
||||||
FontSize="Title"
|
FontSize="Title"
|
||||||
FontAttributes="Bold"
|
FontAttributes="Bold"
|
||||||
Margin="0,14,0,21"/>
|
Margin="0,14,0,21"
|
||||||
|
AutomationId="LogInInitiatedLabel" />
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n ANotificationHasBeenSentToYourDevice}"
|
Text="{Binding SubTitle}"
|
||||||
FontSize="Small"
|
FontSize="Small"
|
||||||
Margin="0,0,0,10"/>
|
Margin="0,0,0,10"
|
||||||
|
AutomationId="SubTitleLabel" />
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice}"
|
Text="{Binding Description}"
|
||||||
FontSize="Small"
|
FontSize="Small"
|
||||||
Margin="0,0,0,24"/>
|
Margin="0,0,0,24"
|
||||||
|
AutomationId="DescriptionLabel" />
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n FingerprintPhrase}"
|
Text="{u:I18n FingerprintPhrase}"
|
||||||
FontSize="Small"
|
FontSize="Small"
|
||||||
FontAttributes="Bold"/>
|
FontAttributes="Bold" />
|
||||||
<controls:MonoLabel
|
<controls:MonoLabel
|
||||||
FormattedText="{Binding FingerprintPhrase}"
|
FormattedText="{Binding FingerprintPhrase}"
|
||||||
FontSize="Medium"
|
FontSize="Small"
|
||||||
TextColor="{DynamicResource FingerprintPhrase}"/>
|
TextColor="{DynamicResource FingerprintPhrase}"
|
||||||
|
AutomationId="FingerprintPhraseValue" />
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n ResendNotification}"
|
Text="{u:I18n ResendNotification}"
|
||||||
StyleClass="text-md"
|
IsVisible="{Binding ResendNotificationVisible}"
|
||||||
|
StyleClass="text-sm"
|
||||||
|
FontAttributes="Bold"
|
||||||
HorizontalOptions="Start"
|
HorizontalOptions="Start"
|
||||||
Margin="0,40,0,0"
|
Margin="0,24,0,0"
|
||||||
TextColor="{DynamicResource HyperlinkColor}">
|
TextColor="{DynamicResource HyperlinkColor}"
|
||||||
|
AutomationId="ResendNotificationButton">
|
||||||
<Label.GestureRecognizers>
|
<Label.GestureRecognizers>
|
||||||
<TapGestureRecognizer Command="{Binding CreatePasswordlessLoginCommand}" />
|
<TapGestureRecognizer Command="{Binding CreatePasswordlessLoginCommand}" />
|
||||||
</Label.GestureRecognizers>
|
</Label.GestureRecognizers>
|
||||||
</Label>
|
</Label>
|
||||||
<StackLayout
|
<BoxView
|
||||||
Orientation="Horizontal"
|
HeightRequest="1"
|
||||||
Margin="0,30,0,0">
|
Margin="0,24,0,24"
|
||||||
<Label
|
Color="{DynamicResource DisabledIconColor}" />
|
||||||
Text="{u:I18n NeedAnotherOption}"
|
<Label
|
||||||
FontSize="Small"
|
Text="{Binding OtherOptions}"
|
||||||
VerticalTextAlignment="End"/>
|
FontSize="Small" />
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n ViewAllLoginOptions}"
|
Text="{u:I18n ViewAllLoginOptions}"
|
||||||
StyleClass="text-md"
|
StyleClass="text-sm"
|
||||||
VerticalTextAlignment="End"
|
FontAttributes="Bold"
|
||||||
VerticalOptions="CenterAndExpand"
|
TextColor="{DynamicResource HyperlinkColor}"
|
||||||
Margin="5, 0"
|
AutomationId="ViewAllLoginOptionsButton">
|
||||||
TextColor="{DynamicResource HyperlinkColor}">
|
<Label.GestureRecognizers>
|
||||||
<Label.GestureRecognizers>
|
<TapGestureRecognizer Command="{Binding CloseCommand}" />
|
||||||
<TapGestureRecognizer Command="{Binding CloseCommand}" />
|
</Label.GestureRecognizers>
|
||||||
</Label.GestureRecognizers>
|
</Label>
|
||||||
</Label>
|
|
||||||
</StackLayout>
|
|
||||||
|
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</pages:BaseContentPage>
|
</pages:BaseContentPage>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
@@ -12,13 +13,15 @@ namespace Bit.App.Pages
|
|||||||
private LoginPasswordlessRequestViewModel _vm;
|
private LoginPasswordlessRequestViewModel _vm;
|
||||||
private readonly AppOptions _appOptions;
|
private readonly AppOptions _appOptions;
|
||||||
|
|
||||||
public LoginPasswordlessRequestPage(string email, AppOptions appOptions = null)
|
public LoginPasswordlessRequestPage(string email, AuthRequestType authRequestType, AppOptions appOptions = null, bool authingWithSso = false)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_appOptions = appOptions;
|
_appOptions = appOptions;
|
||||||
_vm = BindingContext as LoginPasswordlessRequestViewModel;
|
_vm = BindingContext as LoginPasswordlessRequestViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.Email = email;
|
_vm.Email = email;
|
||||||
|
_vm.AuthRequestType = authRequestType;
|
||||||
|
_vm.AuthingWithSso = authingWithSso;
|
||||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||||
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -11,7 +12,9 @@ using Bit.App.Utilities;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Models.Response;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
@@ -32,6 +35,9 @@ namespace Bit.App.Pages
|
|||||||
private IPlatformUtilsService _platformUtilsService;
|
private IPlatformUtilsService _platformUtilsService;
|
||||||
private IEnvironmentService _environmentService;
|
private IEnvironmentService _environmentService;
|
||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
|
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||||
|
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||||
|
private readonly ICryptoService _cryptoService;
|
||||||
|
|
||||||
protected override II18nService i18nService => _i18nService;
|
protected override II18nService i18nService => _i18nService;
|
||||||
protected override IEnvironmentService environmentService => _environmentService;
|
protected override IEnvironmentService environmentService => _environmentService;
|
||||||
@@ -44,6 +50,7 @@ namespace Bit.App.Pages
|
|||||||
private string _email;
|
private string _email;
|
||||||
private string _requestId;
|
private string _requestId;
|
||||||
private string _requestAccessCode;
|
private string _requestAccessCode;
|
||||||
|
private AuthRequestType _authRequestType;
|
||||||
// Item1 publicKey, Item2 privateKey
|
// Item1 publicKey, Item2 privateKey
|
||||||
private Tuple<byte[], byte[]> _requestKeyPair;
|
private Tuple<byte[], byte[]> _requestKeyPair;
|
||||||
|
|
||||||
@@ -57,8 +64,9 @@ namespace Bit.App.Pages
|
|||||||
_i18nService = ServiceContainer.Resolve<II18nService>();
|
_i18nService = ServiceContainer.Resolve<II18nService>();
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||||
_logger = ServiceContainer.Resolve<ILogger>();
|
_logger = ServiceContainer.Resolve<ILogger>();
|
||||||
|
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||||
PageTitle = AppResources.LogInWithAnotherDevice;
|
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>();
|
||||||
|
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
||||||
|
|
||||||
CreatePasswordlessLoginCommand = new AsyncCommand(CreatePasswordlessLoginAsync,
|
CreatePasswordlessLoginCommand = new AsyncCommand(CreatePasswordlessLoginAsync,
|
||||||
onException: ex => HandleException(ex),
|
onException: ex => HandleException(ex),
|
||||||
@@ -73,10 +81,91 @@ namespace Bit.App.Pages
|
|||||||
public Action LogInSuccessAction { get; set; }
|
public Action LogInSuccessAction { get; set; }
|
||||||
public Action UpdateTempPasswordAction { get; set; }
|
public Action UpdateTempPasswordAction { get; set; }
|
||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
|
public bool AuthingWithSso { get; set; }
|
||||||
|
|
||||||
public ICommand CreatePasswordlessLoginCommand { get; }
|
public ICommand CreatePasswordlessLoginCommand { get; }
|
||||||
public ICommand CloseCommand { get; }
|
public ICommand CloseCommand { get; }
|
||||||
|
|
||||||
|
public string HeaderTitle
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
switch (_authRequestType)
|
||||||
|
{
|
||||||
|
case AuthRequestType.AuthenticateAndUnlock:
|
||||||
|
return AppResources.LogInWithDevice;
|
||||||
|
case AuthRequestType.AdminApproval:
|
||||||
|
return AppResources.LogInInitiated;
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
switch (_authRequestType)
|
||||||
|
{
|
||||||
|
case AuthRequestType.AuthenticateAndUnlock:
|
||||||
|
return AppResources.LogInInitiated;
|
||||||
|
case AuthRequestType.AdminApproval:
|
||||||
|
return AppResources.AdminApprovalRequested;
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SubTitle
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
switch (_authRequestType)
|
||||||
|
{
|
||||||
|
case AuthRequestType.AuthenticateAndUnlock:
|
||||||
|
return AppResources.ANotificationHasBeenSentToYourDevice;
|
||||||
|
case AuthRequestType.AdminApproval:
|
||||||
|
return AppResources.YourRequestHasBeenSentToYourAdmin;
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Description
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
switch (_authRequestType)
|
||||||
|
{
|
||||||
|
case AuthRequestType.AuthenticateAndUnlock:
|
||||||
|
return AppResources.PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice;
|
||||||
|
case AuthRequestType.AdminApproval:
|
||||||
|
return AppResources.YouWillBeNotifiedOnceApproved;
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string OtherOptions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
switch (_authRequestType)
|
||||||
|
{
|
||||||
|
case AuthRequestType.AuthenticateAndUnlock:
|
||||||
|
return AppResources.LogInWithDeviceMustBeSetUpInTheSettingsOfTheBitwardenAppNeedAnotherOption;
|
||||||
|
case AuthRequestType.AdminApproval:
|
||||||
|
return AppResources.TroubleLoggingIn;
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string FingerprintPhrase
|
public string FingerprintPhrase
|
||||||
{
|
{
|
||||||
get => _fingerprintPhrase;
|
get => _fingerprintPhrase;
|
||||||
@@ -89,6 +178,25 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _email, value);
|
set => SetProperty(ref _email, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthRequestType AuthRequestType
|
||||||
|
{
|
||||||
|
get => _authRequestType;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _authRequestType, value, additionalPropertyNames: new string[]
|
||||||
|
{
|
||||||
|
nameof(Title),
|
||||||
|
nameof(SubTitle),
|
||||||
|
nameof(Description),
|
||||||
|
nameof(OtherOptions),
|
||||||
|
nameof(ResendNotificationVisible)
|
||||||
|
});
|
||||||
|
PageTitle = HeaderTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ResendNotificationVisible => AuthRequestType == AuthRequestType.AuthenticateAndUnlock;
|
||||||
|
|
||||||
public void StartCheckLoginRequestStatus()
|
public void StartCheckLoginRequestStatus()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -119,25 +227,39 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private async Task CheckLoginRequestStatus()
|
private async Task CheckLoginRequestStatus()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_requestAccessCode))
|
if (string.IsNullOrEmpty(_requestId))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StopCheckLoginRequestStatus();
|
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();
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
|
|
||||||
|
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await HandleLoginCompleteAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus))
|
if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -153,10 +275,13 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_syncService.FullSyncAsync(true).FireAndForget();
|
await HandleLoginCompleteAsync();
|
||||||
LogInSuccessAction?.Invoke();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (ApiException ex) when (ex.Error?.StatusCode == System.Net.HttpStatusCode.BadRequest)
|
||||||
|
{
|
||||||
|
HandleException(ex);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
StartCheckLoginRequestStatus();
|
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()
|
private async Task CreatePasswordlessLoginAsync()
|
||||||
{
|
{
|
||||||
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
|
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
|
||||||
|
|
||||||
var response = await _authService.PasswordlessCreateLoginRequestAsync(_email);
|
PasswordlessLoginResponse response = null;
|
||||||
if (response != null)
|
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||||
|
if (pendingRequest != null && _authRequestType == AuthRequestType.AdminApproval)
|
||||||
{
|
{
|
||||||
FingerprintPhrase = response.FingerprintPhrase;
|
response = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
|
||||||
_requestId = response.Id;
|
if (response == null || (response.IsAnswered && !response.RequestApproved.Value))
|
||||||
_requestAccessCode = response.RequestAccessCode;
|
{
|
||||||
_requestKeyPair = response.RequestKeyPair;
|
// 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();
|
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();
|
throw new ArgumentNullException(nameof(response));
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
|
}
|
||||||
}).FireAndForget();
|
|
||||||
_logger.Exception(ex);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<StackLayout StyleClass="box">
|
<StackLayout StyleClass="box">
|
||||||
<Label Text="{u:I18n LogInSsoSummary}"
|
<Label Text="{u:I18n LogInSsoSummary}"
|
||||||
StyleClass="text-md"
|
StyleClass="text-md"
|
||||||
HorizontalTextAlignment="Start"></Label>
|
HorizontalTextAlignment="Start" />
|
||||||
<StackLayout StyleClass="box-row">
|
<StackLayout StyleClass="box-row">
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n OrgIdentifier}"
|
Text="{u:I18n OrgIdentifier}"
|
||||||
@@ -32,13 +32,15 @@
|
|||||||
Keyboard="Default"
|
Keyboard="Default"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
ReturnType="Go"
|
ReturnType="Go"
|
||||||
ReturnCommand="{Binding LogInCommand}" />
|
ReturnCommand="{Binding LogInCommand}"
|
||||||
|
AutomationId="OrgIdentifierEntry" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout Padding="10, 0">
|
<StackLayout Padding="10, 0">
|
||||||
<Button Text="{u:I18n LogIn}"
|
<Button Text="{u:I18n LogIn}"
|
||||||
StyleClass="btn-primary"
|
StyleClass="btn-primary"
|
||||||
Clicked="LogIn_Clicked"></Button>
|
Clicked="LogIn_Clicked"
|
||||||
|
AutomationId="LogInButton" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ namespace Bit.App.Pages
|
|||||||
_vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync());
|
_vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync());
|
||||||
_vm.UpdateTempPasswordAction =
|
_vm.UpdateTempPasswordAction =
|
||||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||||
|
_vm.StartDeviceApprovalOptionsAction =
|
||||||
|
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
|
||||||
_vm.CloseAction = async () =>
|
_vm.CloseAction = async () =>
|
||||||
{
|
{
|
||||||
await Navigation.PopModalAsync();
|
await Navigation.PopModalAsync();
|
||||||
@@ -106,10 +108,17 @@ namespace Bit.App.Pages
|
|||||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task StartDeviceApprovalOptionsAsync()
|
||||||
|
{
|
||||||
|
var page = new LoginApproveDevicePage();
|
||||||
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SsoAuthSuccessAsync()
|
private async Task SsoAuthSuccessAsync()
|
||||||
{
|
{
|
||||||
RestoreAppOptionsFromCopy();
|
RestoreAppOptionsFromCopy();
|
||||||
await AppHelpers.ClearPreviousPage();
|
await AppHelpers.ClearPreviousPage();
|
||||||
|
|
||||||
if (await _vaultTimeoutService.IsLockedAsync())
|
if (await _vaultTimeoutService.IsLockedAsync())
|
||||||
{
|
{
|
||||||
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Bit.Core.Abstractions;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Essentials;
|
using Xamarin.Essentials;
|
||||||
@@ -29,8 +30,11 @@ namespace Bit.App.Pages
|
|||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||||
|
private readonly ICryptoService _cryptoService;
|
||||||
|
|
||||||
private string _orgIdentifier;
|
private string _orgIdentifier;
|
||||||
|
private bool _useEphemeralWebBrowserSession;
|
||||||
|
|
||||||
public LoginSsoPageViewModel()
|
public LoginSsoPageViewModel()
|
||||||
{
|
{
|
||||||
@@ -45,7 +49,8 @@ namespace Bit.App.Pages
|
|||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
_organizationService = ServiceContainer.Resolve<IOrganizationService>();
|
_organizationService = ServiceContainer.Resolve<IOrganizationService>();
|
||||||
|
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||||
|
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
||||||
|
|
||||||
PageTitle = AppResources.Bitwarden;
|
PageTitle = AppResources.Bitwarden;
|
||||||
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
|
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
|
||||||
@@ -61,6 +66,7 @@ namespace Bit.App.Pages
|
|||||||
public Action StartTwoFactorAction { get; set; }
|
public Action StartTwoFactorAction { get; set; }
|
||||||
public Action StartSetPasswordAction { get; set; }
|
public Action StartSetPasswordAction { get; set; }
|
||||||
public Action SsoAuthSuccessAction { get; set; }
|
public Action SsoAuthSuccessAction { get; set; }
|
||||||
|
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
public Action UpdateTempPasswordAction { get; set; }
|
public Action UpdateTempPasswordAction { get; set; }
|
||||||
|
|
||||||
@@ -109,7 +115,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
||||||
|
|
||||||
var response = await _apiService.PreValidateSso(OrgIdentifier);
|
var response = await _apiService.PreValidateSsoAsync(OrgIdentifier);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(response?.Token))
|
if (string.IsNullOrWhiteSpace(response?.Token))
|
||||||
{
|
{
|
||||||
@@ -121,9 +127,8 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
var ssoToken = response.Token;
|
var ssoToken = response.Token;
|
||||||
|
|
||||||
|
var passwordOptions = PasswordGenerationOptions.CreateDefault
|
||||||
var passwordOptions = new PasswordGenerationOptions(true);
|
.WithLength(64);
|
||||||
passwordOptions.Length = 64;
|
|
||||||
|
|
||||||
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
||||||
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
|
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
|
||||||
@@ -141,10 +146,12 @@ namespace Bit.App.Pages
|
|||||||
"ssoToken=" + Uri.EscapeDataString(ssoToken);
|
"ssoToken=" + Uri.EscapeDataString(ssoToken);
|
||||||
|
|
||||||
WebAuthenticatorResult authResult = null;
|
WebAuthenticatorResult authResult = null;
|
||||||
|
authResult = await WebAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions()
|
||||||
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
|
{
|
||||||
new Uri(REDIRECT_URI));
|
CallbackUrl = new Uri(REDIRECT_URI),
|
||||||
|
Url = new Uri(url),
|
||||||
|
PrefersEphemeralWebBrowserSession = _useEphemeralWebBrowserSession,
|
||||||
|
});
|
||||||
|
|
||||||
var code = GetResultCode(authResult, state);
|
var code = GetResultCode(authResult, state);
|
||||||
if (!string.IsNullOrEmpty(code))
|
if (!string.IsNullOrEmpty(code))
|
||||||
@@ -169,6 +176,8 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
// user canceled
|
// user canceled
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
// Workaroung for cached expired sso token PM-3551
|
||||||
|
_useEphemeralWebBrowserSession = true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -198,28 +207,92 @@ namespace Bit.App.Pages
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
|
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
|
||||||
|
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
|
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
if (response.TwoFactor)
|
if (response.TwoFactor)
|
||||||
{
|
{
|
||||||
StartTwoFactorAction?.Invoke();
|
StartTwoFactorAction?.Invoke();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else if (response.ResetMasterPassword)
|
|
||||||
|
// Trusted device option is sent regardless if this is a trusted device or not
|
||||||
|
// If it is trusted, it will have the necessary keys
|
||||||
|
if (decryptOptions?.TrustedDeviceOption != null)
|
||||||
{
|
{
|
||||||
|
if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
|
||||||
|
{
|
||||||
|
// If we have a device key but no keys on server, we need to remove the device key
|
||||||
|
if (decryptOptions.TrustedDeviceOption.EncryptedPrivateKey == null && decryptOptions.TrustedDeviceOption.EncryptedUserKey == null)
|
||||||
|
{
|
||||||
|
await _deviceTrustCryptoService.RemoveTrustedDeviceAsync();
|
||||||
|
StartDeviceApprovalOptionsAction?.Invoke();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Update temp password only if the device is trusted and therefore has a decrypted User Key set
|
||||||
|
if (response.ForcePasswordReset)
|
||||||
|
{
|
||||||
|
UpdateTempPasswordAction?.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)
|
||||||
|
{
|
||||||
|
await _stateService.SetForcePasswordResetReasonAsync(ForcePasswordResetReason.TdeUserWithoutPasswordHasPasswordResetPermission);
|
||||||
|
}
|
||||||
|
// 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();
|
StartSetPasswordAction?.Invoke();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else if (response.ForcePasswordReset)
|
|
||||||
{
|
_syncService.FullSyncAsync(true).FireAndForget();
|
||||||
UpdateTempPasswordAction?.Invoke();
|
SsoAuthSuccessAction?.Invoke();
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
|
||||||
SsoAuthSuccessAction?.Invoke();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||||
|
|||||||
@@ -35,7 +35,8 @@
|
|||||||
x:Name="_email"
|
x:Name="_email"
|
||||||
Text="{Binding Email}"
|
Text="{Binding Email}"
|
||||||
Keyboard="Email"
|
Keyboard="Email"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value"
|
||||||
|
AutomationId="EmailAddressEntry"/>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Grid StyleClass="box-row">
|
<Grid StyleClass="box-row">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -59,7 +60,8 @@
|
|||||||
IsTextPredictionEnabled="False"
|
IsTextPredictionEnabled="False"
|
||||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0" />
|
Grid.Column="0"
|
||||||
|
AutomationId="MasterPasswordEntry"/>
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
@@ -69,7 +71,8 @@
|
|||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||||
|
AutomationId="PasswordVisibilityToggle"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Label
|
<Label
|
||||||
StyleClass="box-sub-label"
|
StyleClass="box-sub-label"
|
||||||
@@ -109,7 +112,8 @@
|
|||||||
IsTextPredictionEnabled="False"
|
IsTextPredictionEnabled="False"
|
||||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0" />
|
Grid.Column="0"
|
||||||
|
AutomationId="ConfirmMasterPasswordEntry"/>
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
@@ -118,6 +122,7 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationId="ConfirmPasswordVisibilityToggle"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -130,7 +135,8 @@
|
|||||||
Text="{Binding Hint}"
|
Text="{Binding Hint}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
ReturnType="Go"
|
ReturnType="Go"
|
||||||
ReturnCommand="{Binding SubmitCommand}" />
|
ReturnCommand="{Binding SubmitCommand}"
|
||||||
|
AutomationId="MasterPasswordHintLabel" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n MasterPasswordHintDescription}"
|
Text="{u:I18n MasterPasswordHintDescription}"
|
||||||
@@ -142,7 +148,8 @@
|
|||||||
IsToggled="{Binding CheckExposedMasterPassword}"
|
IsToggled="{Binding CheckExposedMasterPassword}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="Start"
|
HorizontalOptions="Start"
|
||||||
Margin="0, 0, 10, 0"/>
|
Margin="0, 0, 10, 0"
|
||||||
|
AutomationId="CheckExposedMasterPasswordToggle"/>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n CheckKnownDataBreachesForThisPassword}"
|
Text="{u:I18n CheckKnownDataBreachesForThisPassword}"
|
||||||
StyleClass="box-footer-label"
|
StyleClass="box-footer-label"
|
||||||
@@ -154,7 +161,8 @@
|
|||||||
IsToggled="{Binding AcceptPolicies}"
|
IsToggled="{Binding AcceptPolicies}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="Start"
|
HorizontalOptions="Start"
|
||||||
Margin="0, 0, 10, 0"/>
|
Margin="0, 0, 10, 0"
|
||||||
|
AutomationId="AcceptPoliciesToggle"/>
|
||||||
<Label StyleClass="box-footer-label"
|
<Label StyleClass="box-footer-label"
|
||||||
HorizontalOptions="Fill">
|
HorizontalOptions="Fill">
|
||||||
<Label.FormattedText>
|
<Label.FormattedText>
|
||||||
|
|||||||
@@ -177,25 +177,28 @@ namespace Bit.App.Pages
|
|||||||
Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
|
Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
|
||||||
Email = Email.Trim().ToLower();
|
Email = Email.Trim().ToLower();
|
||||||
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
|
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
|
||||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, Email, kdfConfig);
|
var newMasterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, Email, kdfConfig);
|
||||||
var encKey = await _cryptoService.MakeEncKeyAsync(key);
|
var (newUserKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(
|
||||||
var hashedPassword = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
newMasterKey,
|
||||||
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1);
|
await _cryptoService.MakeUserKeyAsync()
|
||||||
|
);
|
||||||
|
var hashedPassword = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey);
|
||||||
|
var (newPublicKey, newProtectedPrivateKey) = await _cryptoService.MakeKeyPairAsync(newUserKey);
|
||||||
var request = new RegisterRequest
|
var request = new RegisterRequest
|
||||||
{
|
{
|
||||||
Email = Email,
|
Email = Email,
|
||||||
Name = Name,
|
Name = Name,
|
||||||
MasterPasswordHash = hashedPassword,
|
MasterPasswordHash = hashedPassword,
|
||||||
MasterPasswordHint = Hint,
|
MasterPasswordHint = Hint,
|
||||||
Key = encKey.Item2.EncryptedString,
|
Key = newProtectedUserKey.EncryptedString,
|
||||||
Kdf = kdfConfig.Type,
|
Kdf = kdfConfig.Type,
|
||||||
KdfIterations = kdfConfig.Iterations,
|
KdfIterations = kdfConfig.Iterations,
|
||||||
KdfMemory = kdfConfig.Memory,
|
KdfMemory = kdfConfig.Memory,
|
||||||
KdfParallelism = kdfConfig.Parallelism,
|
KdfParallelism = kdfConfig.Parallelism,
|
||||||
Keys = new KeysRequest
|
Keys = new KeysRequest
|
||||||
{
|
{
|
||||||
PublicKey = keys.Item1,
|
PublicKey = newPublicKey,
|
||||||
EncryptedPrivateKey = keys.Item2.EncryptedString
|
EncryptedPrivateKey = newProtectedPrivateKey.EncryptedString
|
||||||
},
|
},
|
||||||
CaptchaResponse = _captchaToken,
|
CaptchaResponse = _captchaToken,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task Init()
|
public async Task Init()
|
||||||
{
|
{
|
||||||
Organization = await _keyConnectorService.GetManagingOrganization();
|
Organization = await _keyConnectorService.GetManagingOrganizationAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task MigrateAccount()
|
public async Task MigrateAccount()
|
||||||
{
|
{
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||||
|
|
||||||
await _keyConnectorService.MigrateUser();
|
await _keyConnectorService.MigrateUserAsync();
|
||||||
await _syncService.FullSyncAsync(true);
|
await _syncService.FullSyncAsync(true);
|
||||||
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
@@ -47,7 +47,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||||
|
|
||||||
await _apiService.PostLeaveOrganization(Organization.Id);
|
await _apiService.PostLeaveOrganizationAsync(Organization.Id);
|
||||||
await _syncService.FullSyncAsync(true);
|
await _syncService.FullSyncAsync(true);
|
||||||
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<StackLayout Spacing="20">
|
<StackLayout Spacing="20">
|
||||||
<StackLayout StyleClass="box">
|
<StackLayout StyleClass="box">
|
||||||
<StackLayout StyleClass="box-row">
|
<StackLayout StyleClass="box-row">
|
||||||
<Label Text="{u:I18n SetMasterPasswordSummary}"
|
<Label Text="{Binding SetMasterPasswordSummary}"
|
||||||
StyleClass="text-md"
|
StyleClass="text-md"
|
||||||
HorizontalTextAlignment="Start"></Label>
|
HorizontalTextAlignment="Start"></Label>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
@@ -51,7 +51,8 @@
|
|||||||
<Label
|
<Label
|
||||||
Text="{u:I18n ResetPasswordAutoEnrollInviteWarning}"
|
Text="{u:I18n ResetPasswordAutoEnrollInviteWarning}"
|
||||||
StyleClass="text-muted, text-sm, text-bold"
|
StyleClass="text-muted, text-sm, text-bold"
|
||||||
HorizontalTextAlignment="Start" />
|
HorizontalTextAlignment="Start"
|
||||||
|
AutomationId="ResetPasswordAutoEnrollInviteWarningLabel" />
|
||||||
</Frame>
|
</Frame>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||||
@@ -73,7 +74,8 @@
|
|||||||
<Label
|
<Label
|
||||||
Text="{Binding PolicySummary}"
|
Text="{Binding PolicySummary}"
|
||||||
StyleClass="text-muted, text-sm, text-bold"
|
StyleClass="text-muted, text-sm, text-bold"
|
||||||
HorizontalTextAlignment="Start" />
|
HorizontalTextAlignment="Start"
|
||||||
|
AutomationId="PolicyInEffectLabel" />
|
||||||
</Frame>
|
</Frame>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid StyleClass="box-row">
|
<Grid StyleClass="box-row">
|
||||||
@@ -98,7 +100,8 @@
|
|||||||
IsTextPredictionEnabled="False"
|
IsTextPredictionEnabled="False"
|
||||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0" />
|
Grid.Column="0"
|
||||||
|
AutomationId="MasterPasswordField" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
@@ -108,7 +111,8 @@
|
|||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||||
|
AutomationId="ToggleMasterPasswordVisibilityButton" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n MasterPasswordDescription}"
|
Text="{u:I18n MasterPasswordDescription}"
|
||||||
@@ -137,7 +141,8 @@
|
|||||||
IsTextPredictionEnabled="False"
|
IsTextPredictionEnabled="False"
|
||||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0" />
|
Grid.Column="0"
|
||||||
|
AutomationId="RetypePasswordField" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
@@ -147,7 +152,8 @@
|
|||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||||
|
AutomationId="ToggleRetypePasswordVisibilityButton" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<StackLayout StyleClass="box-row">
|
<StackLayout StyleClass="box-row">
|
||||||
<Label
|
<Label
|
||||||
@@ -158,7 +164,8 @@
|
|||||||
Text="{Binding Hint}"
|
Text="{Binding Hint}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
ReturnType="Go"
|
ReturnType="Go"
|
||||||
ReturnCommand="{Binding SubmitCommand}" />
|
ReturnCommand="{Binding SubmitCommand}"
|
||||||
|
AutomationId="MasterPasswordHintLabel" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n MasterPasswordHintDescription}"
|
Text="{u:I18n MasterPasswordHintDescription}"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using System.Text.RegularExpressions;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@@ -28,6 +27,7 @@ namespace Bit.App.Pages
|
|||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
|
private readonly ISyncService _syncService;
|
||||||
|
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
private bool _isPolicyInEffect;
|
private bool _isPolicyInEffect;
|
||||||
@@ -46,6 +46,7 @@ namespace Bit.App.Pages
|
|||||||
_passwordGenerationService =
|
_passwordGenerationService =
|
||||||
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
|
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
|
||||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||||
|
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||||
|
|
||||||
PageTitle = AppResources.SetMasterPassword;
|
PageTitle = AppResources.SetMasterPassword;
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
@@ -100,11 +101,17 @@ namespace Bit.App.Pages
|
|||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
public string OrgIdentifier { get; set; }
|
public string OrgIdentifier { get; set; }
|
||||||
public string OrgId { get; set; }
|
public string OrgId { get; set; }
|
||||||
|
public ForcePasswordResetReason? ForceSetPasswordReason { get; private set; }
|
||||||
|
|
||||||
|
public string SetMasterPasswordSummary => ForceSetPasswordReason == ForcePasswordResetReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||||
|
? AppResources.YourOrganizationPermissionsWereUpdatedRequeringYouToSetAMasterPassword
|
||||||
|
: AppResources.YourOrganizationRequiresYouToSetAMasterPassword;
|
||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
await CheckPasswordPolicy();
|
await CheckPasswordPolicy();
|
||||||
|
ForceSetPasswordReason = await _stateService.GetForcePasswordResetReasonAsync();
|
||||||
|
TriggerPropertyChanged(nameof(SetMasterPasswordSummary));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await _apiService.GetOrganizationAutoEnrollStatusAsync(OrgIdentifier);
|
var response = await _apiService.GetOrganizationAutoEnrollStatusAsync(OrgIdentifier);
|
||||||
@@ -165,58 +172,52 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
|
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
|
||||||
var email = await _stateService.GetEmailAsync();
|
var email = await _stateService.GetEmailAsync();
|
||||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig);
|
var newMasterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, email, kdfConfig);
|
||||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
|
var masterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey, HashPurpose.ServerAuthorization);
|
||||||
var localMasterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
|
var localMasterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey, HashPurpose.LocalAuthorization);
|
||||||
|
|
||||||
Tuple<SymmetricCryptoKey, EncString> encKey;
|
var (newUserKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(newMasterKey,
|
||||||
var existingEncKey = await _cryptoService.GetEncKeyAsync();
|
await _cryptoService.GetUserKeyAsync() ?? await _cryptoService.MakeUserKeyAsync());
|
||||||
if (existingEncKey == null)
|
var keysRequest = await GetKeysForSetPasswordRequestAsync(newUserKey);
|
||||||
{
|
|
||||||
encKey = await _cryptoService.MakeEncKeyAsync(key);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
encKey = await _cryptoService.RemakeEncKeyAsync(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1);
|
|
||||||
var request = new SetPasswordRequest
|
var request = new SetPasswordRequest
|
||||||
{
|
{
|
||||||
MasterPasswordHash = masterPasswordHash,
|
MasterPasswordHash = masterPasswordHash,
|
||||||
Key = encKey.Item2.EncryptedString,
|
Key = newProtectedUserKey.EncryptedString,
|
||||||
MasterPasswordHint = Hint,
|
MasterPasswordHint = Hint,
|
||||||
Kdf = kdfConfig.Type.GetValueOrDefault(KdfType.PBKDF2_SHA256),
|
Kdf = kdfConfig.Type.GetValueOrDefault(KdfType.PBKDF2_SHA256),
|
||||||
KdfIterations = kdfConfig.Iterations.GetValueOrDefault(Constants.Pbkdf2Iterations),
|
KdfIterations = kdfConfig.Iterations.GetValueOrDefault(Constants.Pbkdf2Iterations),
|
||||||
KdfMemory = kdfConfig.Memory,
|
KdfMemory = kdfConfig.Memory,
|
||||||
KdfParallelism = kdfConfig.Parallelism,
|
KdfParallelism = kdfConfig.Parallelism,
|
||||||
OrgIdentifier = OrgIdentifier,
|
OrgIdentifier = OrgIdentifier,
|
||||||
Keys = new KeysRequest
|
Keys = keysRequest
|
||||||
{
|
|
||||||
PublicKey = keys.Item1,
|
|
||||||
EncryptedPrivateKey = keys.Item2.EncryptedString
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
|
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||||
// Set Password and relevant information
|
// Set Password and relevant information
|
||||||
await _apiService.SetPasswordAsync(request);
|
await _apiService.SetPasswordAsync(request);
|
||||||
await _stateService.SetKdfConfigurationAsync(kdfConfig);
|
await _stateService.SetKdfConfigurationAsync(kdfConfig);
|
||||||
await _cryptoService.SetKeyAsync(key);
|
await _cryptoService.SetUserKeyAsync(newUserKey);
|
||||||
await _cryptoService.SetKeyHashAsync(localMasterPasswordHash);
|
await _cryptoService.SetMasterKeyAsync(newMasterKey);
|
||||||
await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString);
|
await _cryptoService.SetMasterKeyHashAsync(localMasterPasswordHash);
|
||||||
await _cryptoService.SetEncPrivateKeyAsync(keys.Item2.EncryptedString);
|
await _cryptoService.SetMasterKeyEncryptedUserKeyAsync(newProtectedUserKey.EncryptedString);
|
||||||
|
|
||||||
|
// Set private key only for new JIT provisioned users in MP encryption orgs
|
||||||
|
// Existing TDE users will have private key set on sync or on login
|
||||||
|
if (keysRequest != null)
|
||||||
|
{
|
||||||
|
await _cryptoService.SetUserPrivateKeyAsync(keysRequest.EncryptedPrivateKey);
|
||||||
|
}
|
||||||
|
|
||||||
if (ResetPasswordAutoEnroll)
|
if (ResetPasswordAutoEnroll)
|
||||||
{
|
{
|
||||||
// Grab Organization Keys
|
// Grab Organization Keys
|
||||||
var response = await _apiService.GetOrganizationKeysAsync(OrgId);
|
var response = await _apiService.GetOrganizationKeysAsync(OrgId);
|
||||||
var publicKey = CoreHelpers.Base64UrlDecode(response.PublicKey);
|
var publicKey = CoreHelpers.Base64UrlDecode(response.PublicKey);
|
||||||
// Grab user's Encryption Key and encrypt with Org Public Key
|
// Grab User Key and encrypt with Org Public Key
|
||||||
var userEncKey = await _cryptoService.GetEncKeyAsync();
|
var userKey = await _cryptoService.GetUserKeyAsync();
|
||||||
var encryptedKey = await _cryptoService.RsaEncryptAsync(userEncKey.Key, publicKey);
|
var encryptedKey = await _cryptoService.RsaEncryptAsync(userKey.Key, publicKey);
|
||||||
// Request
|
// Request
|
||||||
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
|
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
|
||||||
{
|
{
|
||||||
@@ -228,6 +229,9 @@ namespace Bit.App.Pages
|
|||||||
await _apiService.PutOrganizationUserResetPasswordEnrollmentAsync(OrgId, userId, resetRequest);
|
await _apiService.PutOrganizationUserResetPasswordEnrollmentAsync(OrgId, userId, resetRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _stateService.SetForcePasswordResetReasonAsync(null);
|
||||||
|
await _stateService.SetUserHasMasterPasswordAsync(true);
|
||||||
|
await _syncService.FullSyncAsync(true);
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
SetPasswordSuccessAction?.Invoke();
|
SetPasswordSuccessAction?.Invoke();
|
||||||
}
|
}
|
||||||
@@ -242,6 +246,21 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<KeysRequest> GetKeysForSetPasswordRequestAsync(UserKey newUserKey)
|
||||||
|
{
|
||||||
|
if (ForceSetPasswordReason == ForcePasswordResetReason.TdeUserWithoutPasswordHasPasswordResetPermission)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (newPublicKey, newProtectedPrivateKey) = await _cryptoService.MakeKeyPairAsync(newUserKey);
|
||||||
|
return new KeysRequest
|
||||||
|
{
|
||||||
|
PublicKey = newPublicKey,
|
||||||
|
EncryptedPrivateKey = newProtectedPrivateKey.EncryptedString
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public void TogglePassword()
|
public void TogglePassword()
|
||||||
{
|
{
|
||||||
ShowPassword = !ShowPassword;
|
ShowPassword = !ShowPassword;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Bit.App.Controls;
|
|||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
@@ -17,26 +18,29 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private TwoFactorPageViewModel _vm;
|
private TwoFactorPageViewModel _vm;
|
||||||
private bool _inited;
|
private bool _inited;
|
||||||
private bool _authingWithSso;
|
|
||||||
private string _orgIdentifier;
|
private string _orgIdentifier;
|
||||||
|
|
||||||
public TwoFactorPage(bool? authingWithSso = false, AppOptions appOptions = null, string orgIdentifier = null)
|
public TwoFactorPage(bool? authingWithSso = false, AppOptions appOptions = null, string orgIdentifier = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
SetActivityIndicator();
|
SetActivityIndicator();
|
||||||
_authingWithSso = authingWithSso ?? false;
|
|
||||||
_appOptions = appOptions;
|
_appOptions = appOptions;
|
||||||
_orgIdentifier = orgIdentifier;
|
_orgIdentifier = orgIdentifier;
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
_vm = BindingContext as TwoFactorPageViewModel;
|
_vm = BindingContext as TwoFactorPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
|
_vm.AuthingWithSso = authingWithSso ?? false;
|
||||||
_vm.StartSetPasswordAction = () =>
|
_vm.StartSetPasswordAction = () =>
|
||||||
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||||
_vm.TwoFactorAuthSuccessAction = () =>
|
_vm.TwoFactorAuthSuccessAction = () =>
|
||||||
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync());
|
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessToMainAsync());
|
||||||
|
_vm.LockAction = () =>
|
||||||
|
Device.BeginInvokeOnMainThread(TwoFactorAuthSuccessWithSSOLocked);
|
||||||
_vm.UpdateTempPasswordAction =
|
_vm.UpdateTempPasswordAction =
|
||||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||||
|
_vm.StartDeviceApprovalOptionsAction =
|
||||||
|
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
|
||||||
_vm.CloseAction = async () => await Navigation.PopModalAsync();
|
_vm.CloseAction = async () => await Navigation.PopModalAsync();
|
||||||
DuoWebView = _duoWebView;
|
DuoWebView = _duoWebView;
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
@@ -180,21 +184,25 @@ namespace Bit.App.Pages
|
|||||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
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));
|
return;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
|
||||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
|
||||||
}
|
}
|
||||||
|
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||||
|
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Token_TextChanged(object sender, TextChangedEventArgs e)
|
private void Token_TextChanged(object sender, TextChangedEventArgs e)
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ using Bit.App.Utilities;
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.Request;
|
using Bit.Core.Models.Request;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
@@ -32,12 +34,12 @@ namespace Bit.App.Pages
|
|||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
private readonly IAppIdService _appIdService;
|
private readonly IAppIdService _appIdService;
|
||||||
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||||
private TwoFactorProviderType? _selectedProviderType;
|
private TwoFactorProviderType? _selectedProviderType;
|
||||||
private string _totpInstruction;
|
private string _totpInstruction;
|
||||||
private string _webVaultUrl = "https://vault.bitwarden.com";
|
private string _webVaultUrl = "https://vault.bitwarden.com";
|
||||||
private bool _authingWithSso = false;
|
|
||||||
private bool _enableContinue = false;
|
private bool _enableContinue = false;
|
||||||
private bool _showContinue = true;
|
private bool _showContinue = true;
|
||||||
|
|
||||||
@@ -54,7 +56,9 @@ namespace Bit.App.Pages
|
|||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||||
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||||
_logger = ServiceContainer.Resolve<ILogger>();
|
_logger = ServiceContainer.Resolve<ILogger>();
|
||||||
|
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||||
|
|
||||||
PageTitle = AppResources.TwoStepLogin;
|
PageTitle = AppResources.TwoStepLogin;
|
||||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||||
@@ -69,6 +73,8 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public bool Remember { get; set; }
|
public bool Remember { get; set; }
|
||||||
|
|
||||||
|
public bool AuthingWithSso { get; set; }
|
||||||
|
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
|
|
||||||
public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo ||
|
public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo ||
|
||||||
@@ -118,6 +124,8 @@ namespace Bit.App.Pages
|
|||||||
public Command SubmitCommand { get; }
|
public Command SubmitCommand { get; }
|
||||||
public ICommand MoreCommand { get; }
|
public ICommand MoreCommand { get; }
|
||||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||||
|
public Action LockAction { get; set; }
|
||||||
|
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||||
public Action StartSetPasswordAction { get; set; }
|
public Action StartSetPasswordAction { get; set; }
|
||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
public Action UpdateTempPasswordAction { get; set; }
|
public Action UpdateTempPasswordAction { get; set; }
|
||||||
@@ -136,8 +144,6 @@ namespace Bit.App.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_authingWithSso = _authService.AuthingWithSso();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl))
|
if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl))
|
||||||
{
|
{
|
||||||
_webVaultUrl = _environmentService.BaseUrl;
|
_webVaultUrl = _environmentService.BaseUrl;
|
||||||
@@ -315,21 +321,82 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
var task = Task.Run(() => _syncService.FullSyncAsync(true));
|
var task = Task.Run(() => _syncService.FullSyncAsync(true));
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
|
||||||
_messagingService.Send("listenYubiKeyOTP", false);
|
_messagingService.Send("listenYubiKeyOTP", false);
|
||||||
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
|
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
|
||||||
|
|
||||||
if (_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;
|
||||||
|
}
|
||||||
|
// Update temp password only if the device is trusted and therefore has a decrypted User Key set
|
||||||
|
if (result.ForcePasswordReset)
|
||||||
|
{
|
||||||
|
UpdateTempPasswordAction?.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)
|
||||||
|
{
|
||||||
|
await _stateService.SetForcePasswordResetReasonAsync(ForcePasswordResetReason.TdeUserWithoutPasswordHasPasswordResetPermission);
|
||||||
|
}
|
||||||
|
// 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();
|
StartSetPasswordAction?.Invoke();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else if (result.ForcePasswordReset)
|
|
||||||
{
|
_syncService.FullSyncAsync(true).FireAndForget();
|
||||||
UpdateTempPasswordAction?.Invoke();
|
await TwoFactorAuthSuccessAsync();
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TwoFactorAuthSuccessAction?.Invoke();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (ApiException e)
|
catch (ApiException e)
|
||||||
{
|
{
|
||||||
@@ -398,7 +465,8 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
Email = _authService.Email,
|
Email = _authService.Email,
|
||||||
MasterPasswordHash = _authService.MasterPasswordHash,
|
MasterPasswordHash = _authService.MasterPasswordHash,
|
||||||
DeviceIdentifier = await _appIdService.GetAppIdAsync()
|
DeviceIdentifier = await _appIdService.GetAppIdAsync(),
|
||||||
|
SsoEmail2FaSessionToken = _authService.SsoEmail2FaSessionToken
|
||||||
};
|
};
|
||||||
await _apiService.PostTwoFactorEmailAsync(request);
|
await _apiService.PostTwoFactorEmailAsync(request);
|
||||||
if (showLoading)
|
if (showLoading)
|
||||||
@@ -422,5 +490,17 @@ namespace Bit.App.Pages
|
|||||||
return false;
|
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
|
<pages:BaseContentPage
|
||||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
@@ -46,9 +46,10 @@
|
|||||||
BackgroundColor="Transparent"
|
BackgroundColor="Transparent"
|
||||||
BorderColor="{DynamicResource PrimaryColor}">
|
BorderColor="{DynamicResource PrimaryColor}">
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n UpdateMasterPasswordWarning}"
|
Text="{Binding UpdateMasterPasswordWarningText }"
|
||||||
StyleClass="text-muted, text-sm, text-bold"
|
StyleClass="text-muted, text-sm, text-bold"
|
||||||
HorizontalTextAlignment="Center" />
|
HorizontalTextAlignment="Center"
|
||||||
|
AutomationId="UpdatePasswordWarningLabel" />
|
||||||
</Frame>
|
</Frame>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||||
@@ -71,9 +72,46 @@
|
|||||||
<Label
|
<Label
|
||||||
Text="{Binding PolicySummary}"
|
Text="{Binding PolicySummary}"
|
||||||
StyleClass="text-muted, text-sm, text-bold"
|
StyleClass="text-muted, text-sm, text-bold"
|
||||||
HorizontalTextAlignment="Start" />
|
HorizontalTextAlignment="Start"
|
||||||
|
AutomationId="PolicySummaryLabel" />
|
||||||
</Frame>
|
</Frame>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid StyleClass="box-row" IsVisible="{Binding RequireCurrentPassword }">
|
||||||
|
<Grid.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 StyleClass="box-row">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -96,7 +134,8 @@
|
|||||||
IsTextPredictionEnabled="False"
|
IsTextPredictionEnabled="False"
|
||||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0" />
|
Grid.Column="0"
|
||||||
|
AutomationId="NewPasswordField" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
@@ -106,7 +145,8 @@
|
|||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||||
|
AutomationId="NewPasswordVisibilityButton" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box">
|
<StackLayout StyleClass="box">
|
||||||
@@ -132,7 +172,8 @@
|
|||||||
IsTextPredictionEnabled="False"
|
IsTextPredictionEnabled="False"
|
||||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="0" />
|
Grid.Column="0"
|
||||||
|
AutomationId="RetypePasswordField" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
@@ -142,7 +183,8 @@
|
|||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||||
|
AutomationId="ToggleRetypePasswordVisibilityButton" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<StackLayout StyleClass="box-row">
|
<StackLayout StyleClass="box-row">
|
||||||
<Label
|
<Label
|
||||||
@@ -153,7 +195,8 @@
|
|||||||
Text="{Binding Hint}"
|
Text="{Binding Hint}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
ReturnType="Go"
|
ReturnType="Go"
|
||||||
ReturnCommand="{Binding SubmitCommand}" />
|
ReturnCommand="{Binding SubmitCommand}"
|
||||||
|
AutomationId="MasterPasswordHintLabel" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n MasterPasswordHintDescription}"
|
Text="{u:I18n MasterPasswordHintDescription}"
|
||||||
|
|||||||
@@ -1,27 +1,68 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.Request;
|
using Bit.Core.Models.Request;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class UpdateTempPasswordPageViewModel : BaseChangePasswordViewModel
|
public class UpdateTempPasswordPageViewModel : BaseChangePasswordViewModel
|
||||||
{
|
{
|
||||||
|
private readonly IUserVerificationService _userVerificationService;
|
||||||
|
|
||||||
|
private ForcePasswordResetReason _reason = ForcePasswordResetReason.AdminForcePasswordReset;
|
||||||
|
|
||||||
public UpdateTempPasswordPageViewModel()
|
public UpdateTempPasswordPageViewModel()
|
||||||
{
|
{
|
||||||
PageTitle = AppResources.UpdateMasterPassword;
|
PageTitle = AppResources.UpdateMasterPassword;
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
|
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 TogglePasswordCommand { get; }
|
||||||
public Command ToggleConfirmPasswordCommand { get; }
|
public Command ToggleConfirmPasswordCommand { get; }
|
||||||
public Action UpdateTempPasswordSuccessAction { get; set; }
|
public Action UpdateTempPasswordSuccessAction { get; set; }
|
||||||
public Action LogOutAction { 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()
|
public void TogglePassword()
|
||||||
{
|
{
|
||||||
@@ -42,32 +83,46 @@ namespace Bit.App.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RequireCurrentPassword &&
|
||||||
|
!await _userVerificationService.VerifyUser(CurrentMasterPassword, VerificationType.MasterPassword))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve details for key generation
|
// Retrieve details for key generation
|
||||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||||
var email = await _stateService.GetEmailAsync();
|
var email = await _stateService.GetEmailAsync();
|
||||||
|
|
||||||
// Create new key and hash new password
|
// Create new master key and hash new password
|
||||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig);
|
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, email, kdfConfig);
|
||||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
var masterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey);
|
||||||
|
|
||||||
// Create new encKey for the User
|
// Encrypt user key with new master key
|
||||||
var newEncKey = await _cryptoService.RemakeEncKeyAsync(key);
|
var (userKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(masterKey);
|
||||||
|
|
||||||
// Create request
|
|
||||||
var request = new UpdateTempPasswordRequest
|
|
||||||
{
|
|
||||||
Key = newEncKey.Item2.EncryptedString,
|
|
||||||
NewMasterPasswordHash = masterPasswordHash,
|
|
||||||
MasterPasswordHint = Hint
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initiate API action
|
// Initiate API action
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.UpdatingPassword);
|
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();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
|
||||||
|
// Clear the force reset password reason
|
||||||
|
await _stateService.SetForcePasswordResetReasonAsync(null);
|
||||||
|
|
||||||
|
_platformUtilsService.ShowToast(null, null, AppResources.UpdatedMasterPassword);
|
||||||
|
|
||||||
UpdateTempPasswordSuccessAction?.Invoke();
|
UpdateTempPasswordSuccessAction?.Invoke();
|
||||||
}
|
}
|
||||||
catch (ApiException e)
|
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()
|
private async Task SaveActivityAsync()
|
||||||
{
|
{
|
||||||
SetServices();
|
SetServices();
|
||||||
|
if (await _stateService.GetActiveUserIdAsync() == null)
|
||||||
|
{
|
||||||
|
// Fresh install and/or all users logged out won't have an active user, skip saving last active time
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime());
|
await _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/App/Pages/BaseModalContentPage.cs
Normal file
18
src/App/Pages/BaseModalContentPage.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class BaseModalContentPage : BaseContentPage
|
||||||
|
{
|
||||||
|
public BaseModalContentPage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PopModal_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
using Xamarin.Essentials;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
@@ -47,5 +50,24 @@ namespace Bit.App.Pages
|
|||||||
_logger.Value.Exception(ex);
|
_logger.Value.Exception(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected AsyncCommand CreateDefaultAsyncCommnad(Func<Task> execute, Func<object, bool> canExecute = null)
|
||||||
|
{
|
||||||
|
return new AsyncCommand(execute,
|
||||||
|
canExecute,
|
||||||
|
ex => HandleException(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task<bool> HasConnectivityAsync()
|
||||||
|
{
|
||||||
|
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.Value.ShowDialogAsync(
|
||||||
|
AppResources.InternetConnectionRequiredMessage,
|
||||||
|
AppResources.InternetConnectionRequiredTitle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
Clicked="Clear_Clicked"
|
Clicked="Clear_Clicked"
|
||||||
Order="Secondary"
|
Order="Secondary"
|
||||||
x:Name="_clearItem"
|
x:Name="_clearItem"
|
||||||
x:Key="clearItem" />
|
x:Key="clearItem"
|
||||||
|
AutomationId="ClearPasswordList" />
|
||||||
<ToolbarItem Icon="more_vert.png"
|
<ToolbarItem Icon="more_vert.png"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Options}"
|
AutomationProperties.Name="{u:I18n Options}"
|
||||||
@@ -43,7 +44,8 @@
|
|||||||
Margin="20, 0"
|
Margin="20, 0"
|
||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
HorizontalOptions="CenterAndExpand"
|
HorizontalOptions="CenterAndExpand"
|
||||||
HorizontalTextAlignment="Center"></Label>
|
HorizontalTextAlignment="Center"
|
||||||
|
AutomationId="NoPasswordsDisplayedLabel"></Label>
|
||||||
<controls:ExtendedCollectionView
|
<controls:ExtendedCollectionView
|
||||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||||
ItemsSource="{Binding History}"
|
ItemsSource="{Binding History}"
|
||||||
@@ -56,7 +58,8 @@
|
|||||||
StyleClass="list-row, list-row-platform"
|
StyleClass="list-row, list-row-platform"
|
||||||
Padding="10"
|
Padding="10"
|
||||||
RowSpacing="0"
|
RowSpacing="0"
|
||||||
ColumnSpacing="10">
|
ColumnSpacing="10"
|
||||||
|
AutomationId="GeneratedPasswordRow">
|
||||||
|
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -71,12 +74,14 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
StyleClass="list-title, list-title-platform, text-html"
|
StyleClass="list-title, list-title-platform, text-html"
|
||||||
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}" />
|
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}"
|
||||||
|
AutomationId="GeneratedPasswordValue" />
|
||||||
<Label LineBreakMode="TailTruncation"
|
<Label LineBreakMode="TailTruncation"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
StyleClass="list-subtitle, list-subtitle-platform"
|
StyleClass="list-subtitle, list-subtitle-platform"
|
||||||
Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}" />
|
Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}"
|
||||||
|
AutomationId="GeneratedPasswordDateLabel" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="list-row-button, list-row-button-platform"
|
StyleClass="list-row-button, list-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Paste}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Paste}}"
|
||||||
@@ -86,7 +91,8 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
AutomationProperties.Name="{u:I18n CopyPassword}"
|
||||||
|
AutomationId="CopyPasswordValueButton" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</CollectionView.ItemTemplate>
|
</CollectionView.ItemTemplate>
|
||||||
|
|||||||
@@ -71,7 +71,8 @@
|
|||||||
<Label
|
<Label
|
||||||
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
|
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
|
||||||
StyleClass="text-muted, text-sm, text-bold"
|
StyleClass="text-muted, text-sm, text-bold"
|
||||||
HorizontalTextAlignment="Center" />
|
HorizontalTextAlignment="Center"
|
||||||
|
AutomationId="PasswordGeneratorPolicyInEffectLabel" />
|
||||||
</Frame>
|
</Frame>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}"
|
<Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}"
|
||||||
@@ -82,21 +83,24 @@
|
|||||||
x:Name="lblPassword"
|
x:Name="lblPassword"
|
||||||
StyleClass="text-lg, text-html"
|
StyleClass="text-lg, text-html"
|
||||||
Text="{Binding ColoredPassword, Mode=OneWay}"
|
Text="{Binding ColoredPassword, Mode=OneWay}"
|
||||||
Margin="0, 20" />
|
Margin="0, 20"
|
||||||
|
AutomationId="GeneratedPasswordLabel" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
Command="{Binding CopyCommand}"
|
Command="{Binding CopyCommand}"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
AutomationProperties.Name="{u:I18n CopyPassword}"
|
||||||
|
AutomationId="CopyValueButton" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
||||||
Command="{Binding RegenerateCommand}"
|
Command="{Binding RegenerateCommand}"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n GeneratePassword}" />
|
AutomationProperties.Name="{u:I18n GeneratePassword}"
|
||||||
|
AutomationId="RegenerateValueButton" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid IsVisible="{Binding IsUsername}"
|
<Grid IsVisible="{Binding IsUsername}"
|
||||||
StyleClass="box-row"
|
StyleClass="box-row"
|
||||||
@@ -107,21 +111,24 @@
|
|||||||
StyleClass="text-lg, text-html"
|
StyleClass="text-lg, text-html"
|
||||||
Text="{Binding ColoredUsername, Mode=OneWay}"
|
Text="{Binding ColoredUsername, Mode=OneWay}"
|
||||||
Margin="0, 20"
|
Margin="0, 20"
|
||||||
HorizontalOptions="Start" />
|
HorizontalOptions="Start"
|
||||||
|
AutomationId="GeneratedPasswordLabel" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
Command="{Binding CopyCommand}"
|
Command="{Binding CopyCommand}"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n CopyUsername}" />
|
AutomationProperties.Name="{u:I18n CopyUsername}"
|
||||||
|
AutomationId="CopyValueButton" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
||||||
Command="{Binding RegenerateUsernameCommand}"
|
Command="{Binding RegenerateUsernameCommand}"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n GenerateUsername}" />
|
AutomationProperties.Name="{u:I18n GenerateUsername}"
|
||||||
|
AutomationId="RegenerateValueButton" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<BoxView StyleClass="box-row-separator"/>
|
<BoxView StyleClass="box-row-separator"/>
|
||||||
<StackLayout StyleClass="box"
|
<StackLayout StyleClass="box"
|
||||||
@@ -135,7 +142,8 @@
|
|||||||
ItemsSource="{Binding GeneratorTypeOptions, Mode=OneTime}"
|
ItemsSource="{Binding GeneratorTypeOptions, Mode=OneTime}"
|
||||||
SelectedItem="{Binding GeneratorTypeSelected}"
|
SelectedItem="{Binding GeneratorTypeSelected}"
|
||||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value"
|
||||||
|
AutomationId="GeneratorTypePicker" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Label Text="{u:I18n Options, Header=True}"
|
<Label Text="{u:I18n Options, Header=True}"
|
||||||
StyleClass="box-header, box-header-platform"
|
StyleClass="box-header, box-header-platform"
|
||||||
@@ -161,7 +169,8 @@
|
|||||||
ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}"
|
ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}"
|
||||||
SelectedItem="{Binding UsernameTypeSelected}"
|
SelectedItem="{Binding UsernameTypeSelected}"
|
||||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value"
|
||||||
|
AutomationId="UsernameTypePicker" />
|
||||||
<Label
|
<Label
|
||||||
StyleClass="box-footer-label"
|
StyleClass="box-footer-label"
|
||||||
Text="{Binding UsernameTypeDescriptionLabel}" />
|
Text="{Binding UsernameTypeDescriptionLabel}" />
|
||||||
@@ -172,7 +181,8 @@
|
|||||||
StyleClass="box-label" />
|
StyleClass="box-label" />
|
||||||
<Entry x:Name="_plusAddressedEmailEntry"
|
<Entry x:Name="_plusAddressedEmailEntry"
|
||||||
Text="{Binding PlusAddressedEmail}"
|
Text="{Binding PlusAddressedEmail}"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value"
|
||||||
|
AutomationId="PlusAddressedEmailEntry" />
|
||||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||||
Text="{u:I18n EmailType}"
|
Text="{u:I18n EmailType}"
|
||||||
StyleClass="box-label"
|
StyleClass="box-label"
|
||||||
@@ -203,7 +213,8 @@
|
|||||||
<Entry
|
<Entry
|
||||||
x:Name="_catchAllEmailDomainNameEntry"
|
x:Name="_catchAllEmailDomainNameEntry"
|
||||||
Text="{Binding CatchAllEmailDomain}"
|
Text="{Binding CatchAllEmailDomain}"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value"
|
||||||
|
AutomationId="CatchAllEmailDomainEntry" />
|
||||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||||
Text="{u:I18n EmailType}"
|
Text="{u:I18n EmailType}"
|
||||||
StyleClass="box-label"
|
StyleClass="box-label"
|
||||||
@@ -236,26 +247,27 @@
|
|||||||
ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}"
|
ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}"
|
||||||
SelectedItem="{Binding ForwardedEmailServiceSelected}"
|
SelectedItem="{Binding ForwardedEmailServiceSelected}"
|
||||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value"
|
||||||
<!--ANONADDY OPTIONS-->
|
AutomationId="ServiceTypePicker" />
|
||||||
<Grid IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
<Grid
|
||||||
Grid.RowDefinitions="Auto,*"
|
Grid.RowDefinitions="Auto,*"
|
||||||
Grid.ColumnDefinitions="*,Auto">
|
Grid.ColumnDefinitions="*,Auto">
|
||||||
<Label
|
<Label
|
||||||
Margin="0,10,0,0"
|
Margin="0,10,0,0"
|
||||||
Text="{u:I18n APIAccessToken}"
|
Text="{Binding ForwardedEmailApiSecretLabel}"
|
||||||
StyleClass="box-label"/>
|
StyleClass="box-label"/>
|
||||||
<Entry
|
<Entry
|
||||||
x:Name="_anonAddyApiAccessTokenEntry"
|
Text="{Binding ForwardedEmailApiSecret}"
|
||||||
Text="{Binding AnonAddyApiAccessToken}"
|
IsPassword="{Binding ShowForwardedEmailApiSecret, Converter={StaticResource inverseBool}}"
|
||||||
IsPassword="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool}}"
|
Grid.Row="1"
|
||||||
Grid.Row="1"/>
|
AutomationId="ForwardedEmailApiSecretEntry" />
|
||||||
<controls:IconButton
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
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}"
|
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="1"/>
|
Grid.Column="1"
|
||||||
|
AutomationId="ShowForwardedEmailApiSecretButton" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||||
Text="{u:I18n DomainNameRequiredParenthesis}"
|
Text="{u:I18n DomainNameRequiredParenthesis}"
|
||||||
@@ -263,92 +275,18 @@
|
|||||||
Margin="0,10,0,0"/>
|
Margin="0,10,0,0"/>
|
||||||
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||||
x:Name="_anonAddyDomainNameEntry"
|
x:Name="_anonAddyDomainNameEntry"
|
||||||
Text="{Binding AnonAddyDomainName}"
|
Text="{Binding AddyIoDomainName}"
|
||||||
StyleClass="box-value"/>
|
StyleClass="box-value"
|
||||||
<!--FIREFOX RELAY OPTIONS-->
|
AutomationId="AnonAddyDomainNameEntry" />
|
||||||
<Grid StyleClass="box-row, box-row-input"
|
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.ForwardEmail}}"
|
||||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.FirefoxRelay}}"
|
Text="{u:I18n DomainNameRequiredParenthesis}"
|
||||||
Grid.RowDefinitions="Auto,*"
|
StyleClass="box-label"
|
||||||
Grid.ColumnDefinitions="*,Auto">
|
Margin="0,10,0,0"/>
|
||||||
<Label
|
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.ForwardEmail}}"
|
||||||
Text="{u:I18n APIAccessToken}"
|
x:Name="_forwardEmailDomainNameEntry"
|
||||||
StyleClass="box-label"/>
|
Text="{Binding ForwardEmailDomainName}"
|
||||||
<Entry
|
StyleClass="box-value"
|
||||||
x:Name="_firefoxRelayApiAccessTokenEntry"
|
AutomationId="ForwardEmailDomainNameEntry" />
|
||||||
Text="{Binding FirefoxRelayApiAccessToken}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
Grid.Row="1"
|
|
||||||
IsPassword="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool}}"/>
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
|
||||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="1"/>
|
|
||||||
</Grid>
|
|
||||||
<!--SIMPLELOGIN OPTIONS-->
|
|
||||||
<Grid StyleClass="box-row, box-row-input"
|
|
||||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.SimpleLogin}}"
|
|
||||||
Grid.RowDefinitions="Auto,*"
|
|
||||||
Grid.ColumnDefinitions="*,Auto">
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n APIKeyRequiredParenthesis}"
|
|
||||||
StyleClass="box-label"/>
|
|
||||||
<Entry
|
|
||||||
x:Name="_simpleLoginApiKeyEntry"
|
|
||||||
Text="{Binding SimpleLoginApiKey}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
Grid.Row="1"
|
|
||||||
IsPassword="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool}}"/>
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
|
||||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="1"/>
|
|
||||||
</Grid>
|
|
||||||
<!--DUCKDUCKGO OPTIONS-->
|
|
||||||
<Grid StyleClass="box-row, box-row-input"
|
|
||||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.DuckDuckGo}}"
|
|
||||||
Grid.RowDefinitions="Auto,*"
|
|
||||||
Grid.ColumnDefinitions="*,Auto">
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n APIKeyRequiredParenthesis}"
|
|
||||||
StyleClass="box-label"/>
|
|
||||||
<Entry
|
|
||||||
x:Name="_duckDuckGoApiAccessTokenEntry"
|
|
||||||
Text="{Binding DuckDuckGoApiKey}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
Grid.Row="1"
|
|
||||||
IsPassword="{Binding ShowDuckDuckGoApiKey, Converter={StaticResource inverseBool}}"/>
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding ShowDuckDuckGoApiKey, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
|
||||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="1"/>
|
|
||||||
</Grid>
|
|
||||||
<!--FASTMAIL OPTIONS-->
|
|
||||||
<Grid StyleClass="box-row, box-row-input"
|
|
||||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.Fastmail}}"
|
|
||||||
Grid.RowDefinitions="Auto,*"
|
|
||||||
Grid.ColumnDefinitions="*,Auto">
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n APIKeyRequiredParenthesis}"
|
|
||||||
StyleClass="box-label"/>
|
|
||||||
<Entry
|
|
||||||
x:Name="_fastmailApiAccessTokenEntry"
|
|
||||||
Text="{Binding FastmailApiKey}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
Grid.Row="1"
|
|
||||||
IsPassword="{Binding ShowFastmailApiKey, Converter={StaticResource inverseBool}}"/>
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding ShowFastmailApiKey, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
|
||||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="1"/>
|
|
||||||
</Grid>
|
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<!--RANDOM WORD OPTIONS-->
|
<!--RANDOM WORD OPTIONS-->
|
||||||
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
|
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
|
||||||
@@ -359,7 +297,8 @@
|
|||||||
<Switch
|
<Switch
|
||||||
IsToggled="{Binding CapitalizeRandomWordUsername}"
|
IsToggled="{Binding CapitalizeRandomWordUsername}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End" />
|
HorizontalOptions="End"
|
||||||
|
AutomationId="CapitalizeRandomWordUsernameToggle" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
||||||
StyleClass="box-row-separator" />
|
StyleClass="box-row-separator" />
|
||||||
@@ -371,7 +310,8 @@
|
|||||||
<Switch
|
<Switch
|
||||||
IsToggled="{Binding IncludeNumberRandomWordUsername}"
|
IsToggled="{Binding IncludeNumberRandomWordUsername}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End" />
|
HorizontalOptions="End"
|
||||||
|
AutomationId="IncludeNumberRandomWordUsernameToggle" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
||||||
StyleClass="box-row-separator" />
|
StyleClass="box-row-separator" />
|
||||||
@@ -386,7 +326,8 @@
|
|||||||
x:Name="_passwordTypePicker"
|
x:Name="_passwordTypePicker"
|
||||||
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
|
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
|
||||||
SelectedIndex="{Binding PasswordTypeSelectedIndex}"
|
SelectedIndex="{Binding PasswordTypeSelectedIndex}"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value"
|
||||||
|
AutomationId="PasswordTypePicker" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout Spacing="0"
|
<StackLayout Spacing="0"
|
||||||
Padding="0"
|
Padding="0"
|
||||||
@@ -403,12 +344,14 @@
|
|||||||
HorizontalOptions="FillAndExpand"
|
HorizontalOptions="FillAndExpand"
|
||||||
HorizontalTextAlignment="End"
|
HorizontalTextAlignment="End"
|
||||||
VerticalOptions="FillAndExpand"
|
VerticalOptions="FillAndExpand"
|
||||||
VerticalTextAlignment="Center" />
|
VerticalTextAlignment="Center"
|
||||||
|
AutomationId="NumberOfWordsLabel" />
|
||||||
<controls:ExtendedStepper
|
<controls:ExtendedStepper
|
||||||
Value="{Binding NumWords}"
|
Value="{Binding NumWords}"
|
||||||
Maximum="20"
|
Maximum="20"
|
||||||
Minimum="3"
|
Minimum="3"
|
||||||
Increment="1" />
|
Increment="1"
|
||||||
|
AutomationId="NumberOfWordsStepper" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<BoxView StyleClass="box-row-separator" />
|
<BoxView StyleClass="box-row-separator" />
|
||||||
<StackLayout StyleClass="box-row, box-row-input">
|
<StackLayout StyleClass="box-row, box-row-input">
|
||||||
@@ -419,7 +362,8 @@
|
|||||||
Text="{Binding WordSeparator}"
|
Text="{Binding WordSeparator}"
|
||||||
IsSpellCheckEnabled="False"
|
IsSpellCheckEnabled="False"
|
||||||
IsTextPredictionEnabled="False"
|
IsTextPredictionEnabled="False"
|
||||||
StyleClass="box-value">
|
StyleClass="box-value"
|
||||||
|
AutomationId="WordSeparatorEntry">
|
||||||
<Entry.Effects>
|
<Entry.Effects>
|
||||||
<effects:NoEmojiKeyboardEffect />
|
<effects:NoEmojiKeyboardEffect />
|
||||||
</Entry.Effects>
|
</Entry.Effects>
|
||||||
@@ -435,7 +379,8 @@
|
|||||||
IsEnabled="{Binding EnforcedPolicyOptions.Capitalize,
|
IsEnabled="{Binding EnforcedPolicyOptions.Capitalize,
|
||||||
Converter={StaticResource inverseBool}}"
|
Converter={StaticResource inverseBool}}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End" />
|
HorizontalOptions="End"
|
||||||
|
AutomationId="CapitalizePassphraseToggle" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<BoxView StyleClass="box-row-separator" />
|
<BoxView StyleClass="box-row-separator" />
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
@@ -448,7 +393,8 @@
|
|||||||
IsEnabled="{Binding EnforcedPolicyOptions.IncludeNumber,
|
IsEnabled="{Binding EnforcedPolicyOptions.IncludeNumber,
|
||||||
Converter={StaticResource inverseBool}}"
|
Converter={StaticResource inverseBool}}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End" />
|
HorizontalOptions="End"
|
||||||
|
AutomationId="IncludeNumbersToggle" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding IsPassword}">
|
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding IsPassword}">
|
||||||
@@ -462,7 +408,8 @@
|
|||||||
StyleClass="box-sub-label"
|
StyleClass="box-sub-label"
|
||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
HorizontalTextAlignment="End"
|
HorizontalTextAlignment="End"
|
||||||
WidthRequest="50" />
|
WidthRequest="50"
|
||||||
|
AutomationId="PasswordLengthLabel" />
|
||||||
<controls:ExtendedSlider
|
<controls:ExtendedSlider
|
||||||
DragCompleted="LengthSlider_DragCompleted"
|
DragCompleted="LengthSlider_DragCompleted"
|
||||||
Value="{Binding Length}"
|
Value="{Binding Length}"
|
||||||
@@ -471,7 +418,8 @@
|
|||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
HorizontalOptions="FillAndExpand"
|
HorizontalOptions="FillAndExpand"
|
||||||
Maximum="128"
|
Maximum="128"
|
||||||
Minimum="5" />
|
Minimum="5"
|
||||||
|
AutomationId="PasswordLengthSlider" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<BoxView StyleClass="box-row-separator" />
|
<BoxView StyleClass="box-row-separator" />
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
@@ -488,7 +436,8 @@
|
|||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End"
|
HorizontalOptions="End"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n UppercaseAtoZ}"/>
|
AutomationProperties.Name="{u:I18n UppercaseAtoZ}"
|
||||||
|
AutomationId="UppercaseAtoZToggle" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<BoxView StyleClass="box-row-separator" />
|
<BoxView StyleClass="box-row-separator" />
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
@@ -505,7 +454,8 @@
|
|||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End"
|
HorizontalOptions="End"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n LowercaseAtoZ}"/>
|
AutomationProperties.Name="{u:I18n LowercaseAtoZ}"
|
||||||
|
AutomationId="LowercaseAtoZToggle" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<BoxView StyleClass="box-row-separator" />
|
<BoxView StyleClass="box-row-separator" />
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
@@ -522,7 +472,8 @@
|
|||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End"
|
HorizontalOptions="End"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n NumbersZeroToNine}"/>
|
AutomationProperties.Name="{u:I18n NumbersZeroToNine}"
|
||||||
|
AutomationId="NumbersZeroToNineToggle" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<BoxView StyleClass="box-row-separator" />
|
<BoxView StyleClass="box-row-separator" />
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
@@ -539,7 +490,8 @@
|
|||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End"
|
HorizontalOptions="End"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n SpecialCharacters}"/>
|
AutomationProperties.Name="{u:I18n SpecialCharacters}"
|
||||||
|
AutomationId="SpecialCharactersToggle" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<BoxView StyleClass="box-row-separator" />
|
<BoxView StyleClass="box-row-separator" />
|
||||||
<StackLayout StyleClass="box-row, box-row-stepper">
|
<StackLayout StyleClass="box-row, box-row-stepper">
|
||||||
@@ -554,12 +506,14 @@
|
|||||||
HorizontalOptions="FillAndExpand"
|
HorizontalOptions="FillAndExpand"
|
||||||
HorizontalTextAlignment="End"
|
HorizontalTextAlignment="End"
|
||||||
VerticalOptions="FillAndExpand"
|
VerticalOptions="FillAndExpand"
|
||||||
VerticalTextAlignment="Center" />
|
VerticalTextAlignment="Center"
|
||||||
|
AutomationId="MinNumberValueLabel" />
|
||||||
<controls:ExtendedStepper
|
<controls:ExtendedStepper
|
||||||
Value="{Binding MinNumber}"
|
Value="{Binding MinNumber}"
|
||||||
Maximum="5"
|
Maximum="5"
|
||||||
Minimum="0"
|
Minimum="0"
|
||||||
Increment="1" />
|
Increment="1"
|
||||||
|
AutomationId="MinNumberStepper" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<BoxView StyleClass="box-row-separator" />
|
<BoxView StyleClass="box-row-separator" />
|
||||||
<StackLayout StyleClass="box-row, box-row-stepper">
|
<StackLayout StyleClass="box-row, box-row-stepper">
|
||||||
@@ -574,12 +528,14 @@
|
|||||||
HorizontalOptions="FillAndExpand"
|
HorizontalOptions="FillAndExpand"
|
||||||
HorizontalTextAlignment="End"
|
HorizontalTextAlignment="End"
|
||||||
VerticalOptions="FillAndExpand"
|
VerticalOptions="FillAndExpand"
|
||||||
VerticalTextAlignment="Center" />
|
VerticalTextAlignment="Center"
|
||||||
|
AutomationId="MinSpecialValueLabel" />
|
||||||
<controls:ExtendedStepper
|
<controls:ExtendedStepper
|
||||||
Value="{Binding MinSpecial}"
|
Value="{Binding MinSpecial}"
|
||||||
Maximum="5"
|
Maximum="5"
|
||||||
Minimum="0"
|
Minimum="0"
|
||||||
Increment="1" />
|
Increment="1"
|
||||||
|
AutomationId="MinSpecialStepper" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<BoxView StyleClass="box-row-separator" />
|
<BoxView StyleClass="box-row-separator" />
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
@@ -590,7 +546,8 @@
|
|||||||
<Switch
|
<Switch
|
||||||
IsToggled="{Binding AvoidAmbiguousChars}"
|
IsToggled="{Binding AvoidAmbiguousChars}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="End" />
|
HorizontalOptions="End"
|
||||||
|
AutomationId="AvoidAmbiguousCharsToggle" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<BoxView StyleClass="box-row-separator" />
|
<BoxView StyleClass="box-row-separator" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
|||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Styles;
|
using Bit.App.Styles;
|
||||||
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
@@ -79,7 +80,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
_broadcasterService.Subscribe(nameof(GeneratorPage), (message) =>
|
_broadcasterService.Subscribe(nameof(GeneratorPage), (message) =>
|
||||||
{
|
{
|
||||||
if (message.Command == "updatedTheme")
|
if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
|
||||||
{
|
{
|
||||||
Device.BeginInvokeOnMainThread(() => _vm.RedrawPassword());
|
Device.BeginInvokeOnMainThread(() => _vm.RedrawPassword());
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user