mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
125 Commits
v2022.05.0
...
feature/to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcc260183e | ||
|
|
2f2b90acee | ||
|
|
1b82249d44 | ||
|
|
5e61fb0a14 | ||
|
|
cf222bd0c3 | ||
|
|
cb0c52fb26 | ||
|
|
c07c305384 | ||
|
|
d2fbf5bdea | ||
|
|
2d2a883b96 | ||
|
|
1f2fb3f796 | ||
|
|
8f3a4b98a5 | ||
|
|
70cf7431f7 | ||
|
|
f2ba86a62b | ||
|
|
f432a58e9f | ||
|
|
7e6935f21c | ||
|
|
7cf34b845e | ||
|
|
292908f53f | ||
|
|
d621a5d2f3 | ||
|
|
75e8276784 | ||
|
|
67f49a0591 | ||
|
|
cceded2a0f | ||
|
|
846d3a85a2 | ||
|
|
8ed909eb91 | ||
|
|
a57dc50a50 | ||
|
|
7802da2b9c | ||
|
|
cd56a124d5 | ||
|
|
9074f533f9 | ||
|
|
9499fa0fb3 | ||
|
|
58a3662d0f | ||
|
|
69e47b651d | ||
|
|
dbe72c68a8 | ||
|
|
3822373b98 | ||
|
|
6c7413e38c | ||
|
|
547e61a66b | ||
|
|
d246d1dece | ||
|
|
e2502e2e0c | ||
|
|
448cce38e1 | ||
|
|
dbc1e5ea3e | ||
|
|
a6ddc2496f | ||
|
|
d9a818279f | ||
|
|
a51c2efdae | ||
|
|
0ce01ddd84 | ||
|
|
6e2e613fee | ||
|
|
6b33266721 | ||
|
|
6f61f1f4d8 | ||
|
|
d3d935fff6 | ||
|
|
a97abc61d5 | ||
|
|
6e30ec2dc7 | ||
|
|
472f251714 | ||
|
|
a6069ef37b | ||
|
|
6aeec4a89a | ||
|
|
9ddbe400f8 | ||
|
|
9bc8e6912d | ||
|
|
64beb23239 | ||
|
|
94f2c5a4f8 | ||
|
|
3522953b8c | ||
|
|
3a91bc7eb1 | ||
|
|
277dd5942f | ||
|
|
109aeb49e4 | ||
|
|
46c73dafbe | ||
|
|
c564a34317 | ||
|
|
e606ff88a4 | ||
|
|
67fbd55ae9 | ||
|
|
c67229d235 | ||
|
|
e8d31b8a22 | ||
|
|
3b763e454c | ||
|
|
c9c3e6d98d | ||
|
|
751fdf2db6 | ||
|
|
c892e9fa57 | ||
|
|
b2500557e7 | ||
|
|
7c311fbb55 | ||
|
|
6ae7d6dd8d | ||
|
|
f24388c1b5 | ||
|
|
3aef86bd34 | ||
|
|
c53a85cd50 | ||
|
|
448758a697 | ||
|
|
e51233bf9b | ||
|
|
f9cbe43627 | ||
|
|
5579817f9f | ||
|
|
51a5f58258 | ||
|
|
a3218aed26 | ||
|
|
388ad4e840 | ||
|
|
48a8d9ae35 | ||
|
|
dd6003bd4f | ||
|
|
fba407f3b6 | ||
|
|
88b406544b | ||
|
|
3438ed94ce | ||
|
|
ec71b21264 | ||
|
|
77c8156515 | ||
|
|
1c0375ef05 | ||
|
|
b3f2730c71 | ||
|
|
5e2142fba7 | ||
|
|
b223f5f16e | ||
|
|
0a64e4c918 | ||
|
|
9b41db962e | ||
|
|
43d3c7b5d7 | ||
|
|
c91277bc43 | ||
|
|
a484ca633c | ||
|
|
31d1a2e083 | ||
|
|
655b51b6a5 | ||
|
|
8168089591 | ||
|
|
6b55fc3032 | ||
|
|
87ab42b155 | ||
|
|
98130e89de | ||
|
|
121f0e3628 | ||
|
|
8a3d88b3ce | ||
|
|
b8b41fe847 | ||
|
|
5bbef3ee16 | ||
|
|
9a2b6c8ec9 | ||
|
|
5272c99643 | ||
|
|
43e9515a03 | ||
|
|
7e9b7398c8 | ||
|
|
58d7b001a5 | ||
|
|
a259560d29 | ||
|
|
22c746543a | ||
|
|
bcbc2738ca | ||
|
|
604e3b6892 | ||
|
|
0b626cedc7 | ||
|
|
3ac2580742 | ||
|
|
008ed8eb56 | ||
|
|
26e0e43bb4 | ||
|
|
0ad992faec | ||
|
|
98dd8298ea | ||
|
|
bb37bac620 | ||
|
|
b02c58e362 |
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -21,12 +21,8 @@
|
||||
|
||||
|
||||
|
||||
## Testing requirements
|
||||
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
|
||||
|
||||
|
||||
|
||||
## Before you submit
|
||||
- [ ] I have checked for formatting errors (`dotnet tool run dotnet-format --check`) (required)
|
||||
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
|
||||
- [ ] This change requires a **documentation update** (notify the documentation team)
|
||||
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
|
||||
|
||||
64
.github/workflows/automatic-issue-responses.yml
vendored
Normal file
64
.github/workflows/automatic-issue-responses.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: Automatic responses
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
jobs:
|
||||
close-issue:
|
||||
name: 'Close issue with automatic response'
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
# Feature request
|
||||
- if: github.event.label.name == 'feature-request'
|
||||
name: Feature request
|
||||
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||
with:
|
||||
comment: |
|
||||
We use GitHub issues as a place to track bugs and other development related issues. The [Bitwarden Community Forums](https://community.bitwarden.com/) has a [Feature Requests](https://community.bitwarden.com/c/feature-requests) section for submitting, voting for, and discussing requests like this one.
|
||||
|
||||
Please [sign up on our forums](https://community.bitwarden.com/signup) and search to see if this request already exists. If so, you can vote for it and contribute to any discussions about it. If not, you can re-create the request there so that it can be properly tracked.
|
||||
|
||||
This issue will now be closed. Thanks!
|
||||
# Intended behavior
|
||||
- if: github.event.label.name == 'intended-behavior'
|
||||
name: Intended behaviour
|
||||
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||
with:
|
||||
comment: |
|
||||
Your issue appears to be describing the intended behavior of the software. If you want this to be changed, it would be a feature request.
|
||||
|
||||
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.
|
||||
|
||||
Please [sign up on our forums](https://community.bitwarden.com/signup) and search to see if this request already exists. If so, you can vote for it and contribute to any discussions about it. If not, you can re-create the request there so that it can be properly tracked.
|
||||
|
||||
This issue will now be closed. Thanks!
|
||||
# Customer support request
|
||||
- if: github.event.label.name == 'customer-support'
|
||||
name: Customer Support request
|
||||
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||
with:
|
||||
comment: |
|
||||
We use GitHub issues as a place to track bugs and other development related issues. Your issue appears to be a support request, or would otherwise be better handled by our dedicated Customer Success team.
|
||||
|
||||
Please contact us using our [Contact page](https://bitwarden.com/contact). You can include a link to this issue in the message content.
|
||||
|
||||
Alternatively, you can also search for an answer in our [help documentation](https://bitwarden.com/help/) or get help from other Bitwarden users on our [community forums](https://community.bitwarden.com/c/support/). The issue here will be closed.
|
||||
# Resolved
|
||||
- if: github.event.label.name == 'resolved'
|
||||
name: Resolved
|
||||
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||
with:
|
||||
comment: |
|
||||
We’ve closed this issue, as it appears the original problem has been resolved. If this happens again or continues to be an problem, please respond to this issue with any additional detail to assist with reproduction and root cause analysis.
|
||||
# Stale
|
||||
- if: github.event.label.name == 'stale'
|
||||
name: Stale
|
||||
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||
with:
|
||||
comment: |
|
||||
As we haven’t heard from you about this problem in some time, this issue will now be closed.
|
||||
|
||||
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.
|
||||
27
.github/workflows/build.yml
vendored
27
.github/workflows/build.yml
vendored
@@ -60,6 +60,11 @@ jobs:
|
||||
runs-on: windows-2019
|
||||
needs: setup
|
||||
steps:
|
||||
- name: Setup NuGet
|
||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||
with:
|
||||
nuget-version: 5.9.0
|
||||
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||
|
||||
@@ -209,6 +214,11 @@ jobs:
|
||||
name: F-Droid Build
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- name: Setup NuGet
|
||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||
with:
|
||||
nuget-version: 5.9.0
|
||||
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||
|
||||
@@ -304,18 +314,6 @@ jobs:
|
||||
|
||||
$xml.Save($androidPath);
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Uninstall from App.csproj"
|
||||
Write-Output "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($appPath);
|
||||
|
||||
$appCenterNode=$xml.SelectSingleNode("/Project/ItemGroup/PackageReference[@Include='Microsoft.AppCenter.Crashes']");
|
||||
$appCenterNode.ParentNode.RemoveChild($appCenterNode);
|
||||
|
||||
$xml.Save($appPath);
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Uninstall from Core.csproj"
|
||||
Write-Output "########################################"
|
||||
@@ -380,6 +378,11 @@ jobs:
|
||||
runs-on: macos-11
|
||||
needs: setup
|
||||
steps:
|
||||
- name: Setup NuGet
|
||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||
with:
|
||||
nuget-version: 5.9.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
nuget help | grep Version
|
||||
|
||||
40
.github/workflows/release.yml
vendored
40
.github/workflows/release.yml
vendored
@@ -13,6 +13,11 @@ on:
|
||||
- Initial Release
|
||||
- Redeploy
|
||||
- Dry Run
|
||||
fdroid_publish:
|
||||
description: 'Publish to f-droid store'
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -34,29 +39,13 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||
|
||||
- name: Retrieve Mobile release version
|
||||
id: retrieve-mobile-version
|
||||
run: |
|
||||
ver=$(sed -E -n '/^<manifest/s/^.*[ ]android:versionName="([^"]+)".*$/\1/p' \
|
||||
./src/Android/Properties/AndroidManifest.xml | tr -d '"')
|
||||
echo "::set-output name=mobile_version::${ver}"
|
||||
shell: bash
|
||||
|
||||
- name: Check to make sure Mobile release version has been bumped
|
||||
if: ${{ github.event.inputs.release_type == 'Initial Release' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
latest_ver=$(hub release -L 1 -f '%T')
|
||||
latest_ver=${latest_ver:1}
|
||||
echo "Latest version: $latest_ver"
|
||||
ver=${{ steps.retrieve-mobile-version.outputs.mobile_version }}
|
||||
echo "Version: $ver"
|
||||
if [ "$latest_ver" = "$ver" ]; then
|
||||
echo "Version has not been bumped!"
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@8f055ef543c7433c967a1b9b04a0f230923233bb
|
||||
with:
|
||||
release-type: ${{ github.event.inputs.release_type }}
|
||||
project-type: xamarin
|
||||
file: src/Android/Properties/AndroidManifest.xml
|
||||
|
||||
- name: Get branch name
|
||||
id: branch
|
||||
@@ -83,8 +72,8 @@ jobs:
|
||||
./com.x8bit.bitwarden-fdroid.apk/com.x8bit.bitwarden-fdroid.apk,
|
||||
./Bitwarden iOS.zip"
|
||||
commit: ${{ github.sha }}
|
||||
tag: v${{ steps.retrieve-mobile-version.outputs.mobile_version }}
|
||||
name: Version ${{ steps.retrieve-mobile-version.outputs.mobile_version }}
|
||||
tag: v${{ steps.version.outputs.version }}
|
||||
name: Version ${{ steps.version.outputs.version }}
|
||||
body: "<insert release notes here>"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
@@ -94,6 +83,7 @@ jobs:
|
||||
name: F-Droid Release
|
||||
runs-on: ubuntu-20.04
|
||||
needs: release
|
||||
if: inputs.fdroid_publish
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||
|
||||
30
.github/workflows/stale-bot.yml
vendored
Normal file
30
.github/workflows/stale-bot.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule: # Run once a day at 5.23am (arbitrary but should avoid peak loads on the hour)
|
||||
- cron: '23 5 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
name: 'Check for stale issues and PRs'
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: 'Run stale action'
|
||||
uses: actions/stale@3cc123766321e9f15a6676375c154ccffb12a358 # v5.0.0
|
||||
with:
|
||||
stale-issue-label: 'needs-reply'
|
||||
stale-pr-label: 'needs-changes'
|
||||
days-before-stale: -1 # Do not apply the stale labels automatically, this is a manual process
|
||||
days-before-issue-close: 14 # Close issue if no further activity after X days
|
||||
days-before-pr-close: 21 # Close PR if no further activity after X days
|
||||
close-issue-message: |
|
||||
We need more information before we can help you with your problem. As we haven’t heard from you recently, this issue will be closed.
|
||||
|
||||
If this happens again or continues to be an problem, please respond to this issue with the information we’ve requested and anything else relevant.
|
||||
close-pr-message: |
|
||||
We can’t merge your pull request until you make the changes we’ve requested. As we haven’t heard from you recently, this pull request will be closed.
|
||||
|
||||
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.
|
||||
24
.github/workflows/version-bump.yml
vendored
24
.github/workflows/version-bump.yml
vendored
@@ -19,12 +19,6 @@ jobs:
|
||||
- name: Create Version Branch
|
||||
run: |
|
||||
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||
git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Checkout Version Branch
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
with:
|
||||
ref: version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Bump Version - Android XML
|
||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||
@@ -56,16 +50,32 @@ jobs:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./src/iOS/Info.plist"
|
||||
|
||||
- name: Commit files
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
|
||||
- name: Check if version changed
|
||||
id: version-changed
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "::set-output name=changes_to_commit::TRUE"
|
||||
else
|
||||
echo "::set-output name=changes_to_commit::FALSE"
|
||||
echo "No changes to commit!";
|
||||
fi
|
||||
|
||||
- name: Commit files
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
run: |
|
||||
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
||||
|
||||
- name: Push changes
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||
|
||||
- name: Create Version PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
@@ -1,40 +1,3 @@
|
||||
# How to Contribute
|
||||
|
||||
Contributions of all kinds are welcome!
|
||||
|
||||
Please visit our [Community Forums](https://community.bitwarden.com/) for general community discussion and the development roadmap.
|
||||
|
||||
Here is how you can get involved:
|
||||
|
||||
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
|
||||
|
||||
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
|
||||
|
||||
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
|
||||
|
||||
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
|
||||
|
||||
* **Help other users:** Go to the [Ask the Bitwarden Community category](https://community.bitwarden.com/c/support/) on the Community Forums
|
||||
|
||||
* **Translate:** See the localization (i10n) section below
|
||||
|
||||
## Contributor Agreement
|
||||
|
||||
Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/mobile) if you intend on contributing to any Github repository. Pull requests cannot be accepted and merged unless the author has signed the Contributor Agreement.
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
* commit any pull requests against the `master` branch
|
||||
* include a link to your Community Forums post
|
||||
|
||||
# Localization (l10n)
|
||||
|
||||
[](https://crowdin.com/project/bitwarden-mobile)
|
||||
|
||||
We use a translation tool called [Crowdin](https://crowdin.com) to help manage our localization efforts across many different languages.
|
||||
|
||||
If you are interested in helping translate the Bitwarden mobile app into another language (or make a translation correction), please register an account at Crowdin and join our project here: https://crowdin.com/project/bitwarden-mobile
|
||||
|
||||
If the language that you are interested in translating is not already listed, create a new account on Crowdin, join the project, and contact the project owner (https://crowdin.com/profile/dwbit).
|
||||
|
||||
You can read Crowdin's getting started guide for translators here: https://support.crowdin.com/crowdin-intro/
|
||||
Our [Contributing Guidelines](https://contributing.bitwarden.com/contributing/) are located in our [Contributing Documentation](https://contributing.bitwarden.com/). The documentation also includes recommended tooling, code style tips, and lots of other great information to get you started.
|
||||
|
||||
22
README.md
22
README.md
@@ -12,16 +12,7 @@ The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin
|
||||
|
||||
# Build/Run
|
||||
|
||||
**Requirements**
|
||||
|
||||
- [Visual Studio](https://visualstudio.microsoft.com/)
|
||||
- [Xamarin](https://docs.microsoft.com/en-us/xamarin/get-started/installation/?pivots=windows)
|
||||
|
||||
**Run the app**
|
||||
|
||||
- Open the solution file in Visual Studio.
|
||||
- Restore the nuget packages.
|
||||
- Build and run the app.
|
||||
Please refer to the [Mobile section](https://contributing.bitwarden.com/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
|
||||
|
||||
# We're Hiring!
|
||||
|
||||
@@ -29,8 +20,7 @@ Interested in contributing in a big way? Consider joining our team! We're hiring
|
||||
|
||||
# Contribute
|
||||
|
||||
Code contributions are welcome! Visual Studio with Xamarin is required to work on this project. Please commit any pull requests against the `master` branch.
|
||||
Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
|
||||
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [Contributing Guidelines](https://contributing.bitwarden.com/contributing/). Check out the [Contributing Documentation](https://contributing.bitwarden.com/) for how to get started with your first contribution.
|
||||
|
||||
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
|
||||
|
||||
@@ -45,11 +35,3 @@ We recently migrated to using dotnet-format as code formatter. All previous bran
|
||||
5. Commit
|
||||
6. Run `git merge -Xours 04539af2a66668b6e85476d5cf318c9150ec4357`
|
||||
7. Push
|
||||
|
||||
#### Git blame
|
||||
|
||||
We also recommend that you configure git to ignore the prettier revision using:
|
||||
|
||||
```bash
|
||||
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
```
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("com.google.android.apps.chrome", "url_bar"),
|
||||
new Browser("com.google.android.apps.chrome_dev", "url_bar"),
|
||||
// Rem. for "com.google.android.captiveportallogin": URL displayed in ActionBar subtitle without viewId.
|
||||
new Browser("com.iode.firefox", "mozac_browser_toolbar_url_view"),
|
||||
new Browser("com.jamal2367.styx", "search"),
|
||||
new Browser("com.kiwibrowser.browser", "url_bar"),
|
||||
new Browser("com.kiwibrowser.browser.dev", "url_bar"),
|
||||
@@ -67,6 +68,7 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("com.naver.whale", "url_bar"),
|
||||
new Browser("com.opera.browser", "url_field"),
|
||||
new Browser("com.opera.browser.beta", "url_field"),
|
||||
new Browser("com.opera.gx", "addressbarEdit"),
|
||||
new Browser("com.opera.mini.native", "url_field"),
|
||||
new Browser("com.opera.mini.native.beta", "url_field"),
|
||||
new Browser("com.opera.touch", "addressbarEdit"),
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.2.5.2" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.8" />
|
||||
<PackageReference Include="Xamarin.Essentials">
|
||||
<Version>1.7.2</Version>
|
||||
<Version>1.7.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||
<Version>122.0.0</Version>
|
||||
@@ -145,12 +145,12 @@
|
||||
<Compile Include="Tiles\GeneratorTileService.cs" />
|
||||
<Compile Include="Tiles\MyVaultTileService.cs" />
|
||||
<Compile Include="Utilities\AndroidHelpers.cs" />
|
||||
<Compile Include="Utilities\AppCenterHelper.cs" />
|
||||
<Compile Include="Utilities\ThemeHelpers.cs" />
|
||||
<Compile Include="WebAuthCallbackActivity.cs" />
|
||||
<Compile Include="Renderers\SelectableLabelRenderer.cs" />
|
||||
<Compile Include="Services\ClipboardService.cs" />
|
||||
<Compile Include="Utilities\IntentExtensions.cs" />
|
||||
<Compile Include="Renderers\CustomPageRenderer.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
||||
|
||||
@@ -73,6 +73,7 @@ namespace Bit.Droid.Autofill
|
||||
"com.google.android.apps.chrome",
|
||||
"com.google.android.apps.chrome_dev",
|
||||
"com.google.android.captiveportallogin",
|
||||
"com.iode.firefox",
|
||||
"com.jamal2367.styx",
|
||||
"com.kiwibrowser.browser",
|
||||
"com.kiwibrowser.browser.dev",
|
||||
@@ -86,6 +87,7 @@ namespace Bit.Droid.Autofill
|
||||
"com.naver.whale",
|
||||
"com.opera.browser",
|
||||
"com.opera.browser.beta",
|
||||
"com.opera.gx",
|
||||
"com.opera.mini.native",
|
||||
"com.opera.mini.native.beta",
|
||||
"com.opera.touch",
|
||||
|
||||
@@ -64,15 +64,13 @@ namespace Bit.Droid
|
||||
Intent?.Validate();
|
||||
|
||||
base.OnCreate(savedInstanceState);
|
||||
if (!CoreHelpers.InDebugMode())
|
||||
|
||||
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget(_ =>
|
||||
{
|
||||
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
|
||||
}
|
||||
});
|
||||
|
||||
#if !DEBUG && !FDROID
|
||||
var appCenterHelper = new AppCenterHelper(_appIdService, _stateService);
|
||||
var appCenterTask = appCenterHelper.InitAsync();
|
||||
#endif
|
||||
ServiceContainer.Resolve<ILogger>("logger").InitAsync();
|
||||
|
||||
var toplayout = Window?.DecorView?.RootView;
|
||||
if (toplayout != null)
|
||||
@@ -85,6 +83,7 @@ namespace Bit.Droid
|
||||
_appOptions = GetOptions();
|
||||
LoadApplication(new App.App(_appOptions));
|
||||
|
||||
|
||||
_broadcasterService.Subscribe(_activityKey, (message) =>
|
||||
{
|
||||
if (message.Command == "startEventTimer")
|
||||
|
||||
@@ -12,7 +12,6 @@ using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Droid.Services;
|
||||
using Bit.Droid.Utilities;
|
||||
using Plugin.CurrentActivity;
|
||||
using Plugin.Fingerprint;
|
||||
using Xamarin.Android.Net;
|
||||
@@ -20,6 +19,8 @@ using System.Net.Http;
|
||||
using System.Net;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.App.Pages;
|
||||
using Bit.App.Utilities.AccountManagement;
|
||||
using Bit.App.Controls;
|
||||
#if !FDROID
|
||||
using Android.Gms.Security;
|
||||
#endif
|
||||
@@ -62,6 +63,16 @@ namespace Bit.Droid
|
||||
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
|
||||
ServiceContainer.Resolve<ICryptoService>("cryptoService"));
|
||||
ServiceContainer.Register<IVerificationActionsFlowHelper>("verificationActionsFlowHelper", verificationActionsFlowHelper);
|
||||
|
||||
var accountsManager = new AccountsManager(
|
||||
ServiceContainer.Resolve<IBroadcasterService>("broadcasterService"),
|
||||
ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService"),
|
||||
ServiceContainer.Resolve<IStorageService>("secureStorageService"),
|
||||
ServiceContainer.Resolve<IStateService>("stateService"),
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
||||
ServiceContainer.Resolve<IAuthService>("authService"),
|
||||
ServiceContainer.Resolve<ILogger>("logger"));
|
||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||
}
|
||||
#if !FDROID
|
||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||
@@ -90,12 +101,13 @@ namespace Bit.Droid
|
||||
{
|
||||
ServiceContainer.Register<INativeLogService>("nativeLogService", new AndroidLogService());
|
||||
#if FDROID
|
||||
ServiceContainer.Register<ILogger>("logger", new StubLogger());
|
||||
var logger = new StubLogger();
|
||||
#elif DEBUG
|
||||
ServiceContainer.Register<ILogger>("logger", DebugLogger.Instance);
|
||||
var logger = DebugLogger.Instance;
|
||||
#else
|
||||
ServiceContainer.Register<ILogger>("logger", Logger.Instance);
|
||||
var logger = Logger.Instance;
|
||||
#endif
|
||||
ServiceContainer.Register("logger", logger);
|
||||
|
||||
// Note: This might cause a race condition. Investigate more.
|
||||
Task.Run(() =>
|
||||
@@ -115,19 +127,20 @@ namespace Bit.Droid
|
||||
var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
|
||||
var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db"));
|
||||
var localizeService = new LocalizeService();
|
||||
var broadcasterService = new BroadcasterService();
|
||||
var broadcasterService = new BroadcasterService(logger);
|
||||
var messagingService = new MobileBroadcasterMessagingService(broadcasterService);
|
||||
var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo());
|
||||
var secureStorageService = new SecureStorageService();
|
||||
var cryptoPrimitiveService = new CryptoPrimitiveService();
|
||||
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
|
||||
var stateService = new StateService(mobileStorageService, secureStorageService);
|
||||
var stateService = new StateService(mobileStorageService, secureStorageService, messagingService);
|
||||
var stateMigrationService =
|
||||
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
||||
var deviceActionService = new DeviceActionService(stateService, messagingService,
|
||||
var clipboardService = new ClipboardService(stateService);
|
||||
var deviceActionService = new DeviceActionService(clipboardService, stateService, messagingService,
|
||||
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
|
||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
|
||||
broadcasterService);
|
||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
|
||||
messagingService, broadcasterService);
|
||||
var biometricService = new BiometricService();
|
||||
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
||||
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
|
||||
@@ -142,13 +155,14 @@ namespace Bit.Droid
|
||||
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
|
||||
ServiceContainer.Register<IStateService>("stateService", stateService);
|
||||
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
|
||||
ServiceContainer.Register<IClipboardService>("clipboardService", new ClipboardService(stateService));
|
||||
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
|
||||
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
||||
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
||||
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
||||
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
|
||||
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
||||
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
||||
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
||||
|
||||
// Push
|
||||
#if FDROID
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.05.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="2022.6.2" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30"/>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
||||
|
||||
|
||||
31
src/Android/Renderers/CustomPageRenderer.cs
Normal file
31
src/Android/Renderers/CustomPageRenderer.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using AndroidX.AppCompat.Widget;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Droid.Renderers;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
|
||||
[assembly: ExportRenderer(typeof(ContentPage), typeof(CustomPageRenderer))]
|
||||
namespace Bit.Droid.Renderers
|
||||
{
|
||||
public class CustomPageRenderer : PageRenderer
|
||||
{
|
||||
public CustomPageRenderer(Context context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
|
||||
{
|
||||
base.OnElementChanged(e);
|
||||
|
||||
Activity context = (Activity)this.Context;
|
||||
var toolbar = context.FindViewById<Toolbar>(Resource.Id.toolbar);
|
||||
if(toolbar != null)
|
||||
{
|
||||
toolbar.NavigationContentDescription = AppResources.TapToGoBack;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -77,6 +77,9 @@
|
||||
<compatibility-package
|
||||
android:name="com.google.android.captiveportallogin"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.iode.firefox"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.jamal2367.styx"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
@@ -116,6 +119,9 @@
|
||||
<compatibility-package
|
||||
android:name="com.opera.browser.beta"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.opera.gx"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.opera.mini.native"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
|
||||
@@ -3,11 +3,10 @@ using System.Threading.Tasks;
|
||||
using Android.OS;
|
||||
using Android.Security.Keystore;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Java.Security;
|
||||
using Javax.Crypto;
|
||||
#if !FDROID
|
||||
using Microsoft.AppCenter.Crashes;
|
||||
#endif
|
||||
|
||||
namespace Bit.Droid.Services
|
||||
{
|
||||
@@ -74,9 +73,7 @@ namespace Bit.Droid.Services
|
||||
catch (InvalidKeyException e)
|
||||
{
|
||||
// Fallback for old bitwarden users without a key
|
||||
#if !FDROID
|
||||
Crashes.TrackError(e);
|
||||
#endif
|
||||
LoggerHelper.LogEvenIfCantBeResolved(e);
|
||||
CreateKey();
|
||||
}
|
||||
|
||||
@@ -101,9 +98,7 @@ namespace Bit.Droid.Services
|
||||
{
|
||||
// Catch silently to allow biometrics to function on devices that are in a state where key generation
|
||||
// is not functioning
|
||||
#if !FDROID
|
||||
Crashes.TrackError(e);
|
||||
#endif
|
||||
LoggerHelper.LogEvenIfCantBeResolved(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Bit.Core;
|
||||
using Android.OS;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Droid.Receivers;
|
||||
using Plugin.CurrentActivity;
|
||||
@@ -26,11 +26,47 @@ namespace Bit.Droid.Services
|
||||
PendingIntentFlags.UpdateCurrent));
|
||||
}
|
||||
|
||||
public async Task CopyTextAsync(string text, int expiresInMs = -1)
|
||||
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
|
||||
{
|
||||
await Clipboard.SetTextAsync(text);
|
||||
try
|
||||
{
|
||||
// Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+
|
||||
if ((int)Build.VERSION.SdkInt < 33)
|
||||
{
|
||||
await Clipboard.SetTextAsync(text);
|
||||
}
|
||||
else
|
||||
{
|
||||
CopyToClipboard(text, isSensitive);
|
||||
}
|
||||
|
||||
await ClearClipboardAlarmAsync(expiresInMs);
|
||||
await ClearClipboardAlarmAsync(expiresInMs);
|
||||
}
|
||||
catch (Java.Lang.SecurityException ex) when (ex.Message.Contains("does not belong to"))
|
||||
{
|
||||
// #1962 Just ignore, the content is copied either way but there is some app interfiering in the process
|
||||
// that the OS catches and just throws this exception.
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCopyNotificationHandledByPlatform()
|
||||
{
|
||||
// Android 13+ provides built-in notification when text is copied to the clipboard
|
||||
return (int)Build.VERSION.SdkInt >= 33;
|
||||
}
|
||||
|
||||
private void CopyToClipboard(string text, bool isSensitive = true)
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var clipboardManager = activity.GetSystemService(
|
||||
Context.ClipboardService) as Android.Content.ClipboardManager;
|
||||
var clipData = ClipData.NewPlainText("bitwarden", text);
|
||||
if (isSensitive)
|
||||
{
|
||||
clipData.Description.Extras ??= new PersistableBundle();
|
||||
clipData.Description.Extras.PutBoolean("android.content.extra.IS_SENSITIVE", true);
|
||||
}
|
||||
clipboardManager.PrimaryClip = clipData;
|
||||
}
|
||||
|
||||
private async Task ClearClipboardAlarmAsync(int expiresInMs = -1)
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace Bit.Droid.Services
|
||||
{
|
||||
public class DeviceActionService : IDeviceActionService
|
||||
{
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
@@ -47,11 +48,13 @@ namespace Bit.Droid.Services
|
||||
private string _userAgent;
|
||||
|
||||
public DeviceActionService(
|
||||
IClipboardService clipboardService,
|
||||
IStateService stateService,
|
||||
IMessagingService messagingService,
|
||||
IBroadcasterService broadcasterService,
|
||||
Func<IEventService> eventServiceFunc)
|
||||
{
|
||||
_clipboardService = clipboardService;
|
||||
_stateService = stateService;
|
||||
_messagingService = messagingService;
|
||||
_broadcasterService = broadcasterService;
|
||||
@@ -929,20 +932,12 @@ namespace Bit.Droid.Services
|
||||
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
|
||||
if (totp != null)
|
||||
{
|
||||
CopyToClipboard(totp);
|
||||
await _clipboardService.CopyTextAsync(totp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyToClipboard(string text)
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var clipboardManager = activity.GetSystemService(
|
||||
Context.ClipboardService) as Android.Content.ClipboardManager;
|
||||
clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", text);
|
||||
}
|
||||
|
||||
public float GetSystemFontSizeScale()
|
||||
{
|
||||
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
|
||||
@@ -953,5 +948,21 @@ namespace Bit.Droid.Services
|
||||
{
|
||||
// for any Android-specific cleanup required after switching accounts
|
||||
}
|
||||
|
||||
public async Task SetScreenCaptureAllowedAsync()
|
||||
{
|
||||
if (CoreHelpers.ForceScreenCaptureEnabled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var activity = CrossCurrentActivity.Current?.Activity;
|
||||
if (await _stateService.GetScreenCaptureAllowedAsync())
|
||||
{
|
||||
activity.RunOnUiThread(() => activity.Window.ClearFlags(WindowManagerFlags.Secure));
|
||||
return;
|
||||
}
|
||||
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#if !FDROID
|
||||
using Bit.Core.Abstractions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AppCenter;
|
||||
using Microsoft.AppCenter.Crashes;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Droid.Utilities
|
||||
{
|
||||
public class AppCenterHelper
|
||||
{
|
||||
private const string AppSecret = "d3834185-b4a6-4347-9047-b86c65293d42";
|
||||
|
||||
private readonly IAppIdService _appIdService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private string _userId;
|
||||
private string _appId;
|
||||
|
||||
public AppCenterHelper(
|
||||
IAppIdService appIdService,
|
||||
IStateService stateService)
|
||||
{
|
||||
_appIdService = appIdService;
|
||||
_stateService = stateService;
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_userId = await _stateService.GetActiveUserIdAsync();
|
||||
_appId = await _appIdService.GetAppIdAsync();
|
||||
|
||||
AppCenter.Start(AppSecret, typeof(Crashes));
|
||||
AppCenter.SetUserId(_userId);
|
||||
|
||||
Crashes.GetErrorAttachments = (ErrorReport report) =>
|
||||
{
|
||||
return new ErrorAttachmentLog[]
|
||||
{
|
||||
ErrorAttachmentLog.AttachmentWithText(Description, "crshdesc.txt"),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
AppId = _appId,
|
||||
UserId = _userId
|
||||
}, Formatting.Indented);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -59,10 +59,10 @@ namespace Bit.Droid.Utilities
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(theme) && osDarkModeEnabled)
|
||||
{
|
||||
theme = "dark";
|
||||
theme = ThemeManager.Dark;
|
||||
}
|
||||
|
||||
if (theme == "dark" || theme == "black" || theme == "nord")
|
||||
if (theme == ThemeManager.Dark || theme == ThemeManager.Black || theme == ThemeManager.Nord)
|
||||
{
|
||||
LightTheme = false;
|
||||
}
|
||||
|
||||
13
src/App/Abstractions/IAccountsManager.cs
Normal file
13
src/App/Abstractions/IAccountsManager.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IAccountsManager
|
||||
{
|
||||
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
|
||||
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
|
||||
Task LogOutAsync(string userId, bool userInitiated, bool expired);
|
||||
}
|
||||
}
|
||||
14
src/App/Abstractions/IAccountsManagerHost.cs
Normal file
14
src/App/Abstractions/IAccountsManagerHost.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface INavigationParams { }
|
||||
|
||||
public interface IAccountsManagerHost
|
||||
{
|
||||
Task SetPreviousPageInfoAsync();
|
||||
void Navigate(NavigationTarget navTarget, INavigationParams navParams = null);
|
||||
Task UpdateThemeAsync();
|
||||
}
|
||||
}
|
||||
@@ -48,5 +48,6 @@ namespace Bit.App.Abstractions
|
||||
bool SupportsFido2();
|
||||
float GetSystemFontSizeScale();
|
||||
Task OnAccountSwitchCompleteAsync();
|
||||
Task SetScreenCaptureAllowedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="4.4.0" />
|
||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.4" />
|
||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
|
||||
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
|
||||
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.1" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.7.2" />
|
||||
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.2" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
|
||||
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2401" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2478" />
|
||||
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
|
||||
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
|
||||
</ItemGroup>
|
||||
@@ -129,12 +128,11 @@
|
||||
<Folder Include="Resources\" />
|
||||
<Folder Include="Behaviors\" />
|
||||
<Folder Include="Controls\AccountSwitchingOverlay\" />
|
||||
<Folder Include="Utilities\AccountManagement\" />
|
||||
<Folder Include="Controls\DateTime\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Controls\CipherViewCell\CipherViewCell.xaml">
|
||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Remove="Pages\Accounts\AccountsPopupPage.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -162,12 +160,6 @@
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Styles\Base.xaml">
|
||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Resources\AppResources.cs.Designer.cs">
|
||||
<DependentUpon>AppResources.cs.resx</DependentUpon>
|
||||
@@ -421,5 +413,7 @@
|
||||
<None Remove="Behaviors\" />
|
||||
<None Remove="Xamarin.CommunityToolkit" />
|
||||
<None Remove="Controls\AccountSwitchingOverlay\" />
|
||||
<None Remove="Utilities\AccountManagement\" />
|
||||
<None Remove="Controls\DateTime\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -6,9 +6,11 @@ using Bit.App.Pages;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Services;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.App.Utilities.AccountManagement;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Xaml;
|
||||
@@ -16,7 +18,7 @@ using Xamarin.Forms.Xaml;
|
||||
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
namespace Bit.App
|
||||
{
|
||||
public partial class App : Application
|
||||
public partial class App : Application, IAccountsManagerHost
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
@@ -27,6 +29,7 @@ namespace Bit.App
|
||||
private readonly IAuthService _authService;
|
||||
private readonly IStorageService _secureStorageService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAccountsManager _accountsManager;
|
||||
|
||||
private static bool _isResumed;
|
||||
|
||||
@@ -47,128 +50,101 @@ namespace Bit.App
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||
|
||||
_accountsManager.Init(() => Options, this);
|
||||
|
||||
Bootstrap();
|
||||
_broadcasterService.Subscribe(nameof(App), async (message) =>
|
||||
{
|
||||
if (message.Command == "showDialog")
|
||||
try
|
||||
{
|
||||
var details = message.Data as DialogDetails;
|
||||
var confirmed = true;
|
||||
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
||||
AppResources.Ok : details.ConfirmText;
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
if (message.Command == "showDialog")
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
||||
var details = message.Data as DialogDetails;
|
||||
var confirmed = true;
|
||||
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
||||
AppResources.Ok : details.ConfirmText;
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
||||
details.CancelText);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText);
|
||||
}
|
||||
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
||||
});
|
||||
}
|
||||
else if (message.Command == "locked")
|
||||
{
|
||||
var extras = message.Data as Tuple<string, bool>;
|
||||
var userId = extras?.Item1;
|
||||
var userInitiated = extras?.Item2 ?? false;
|
||||
Device.BeginInvokeOnMainThread(async () => await LockedAsync(userId, userInitiated));
|
||||
}
|
||||
else if (message.Command == "lockVault")
|
||||
{
|
||||
await _vaultTimeoutService.LockAsync(true);
|
||||
}
|
||||
else if (message.Command == "logout")
|
||||
{
|
||||
var extras = message.Data as Tuple<string, bool, bool>;
|
||||
var userId = extras?.Item1;
|
||||
var userInitiated = extras?.Item2 ?? true;
|
||||
var expired = extras?.Item3 ?? false;
|
||||
Device.BeginInvokeOnMainThread(async () => await LogOutAsync(userId, userInitiated, expired));
|
||||
}
|
||||
else if (message.Command == "loggedOut")
|
||||
{
|
||||
// Clean up old migrated key if they ever log out.
|
||||
await _secureStorageService.RemoveAsync("oldKey");
|
||||
}
|
||||
else if (message.Command == "resumed")
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
||||
{
|
||||
confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
||||
details.CancelText);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText);
|
||||
}
|
||||
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
||||
});
|
||||
}
|
||||
else if (message.Command == "resumed")
|
||||
{
|
||||
ResumedAsync().FireAndForget();
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ResumedAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
else if (message.Command == "slept")
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
await SleptAsync();
|
||||
}
|
||||
}
|
||||
else if (message.Command == "migrated")
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
await _accountsManager.NavigateOnAccountChangeAsync();
|
||||
}
|
||||
else if (message.Command == "popAllAndGoToTabGenerator" ||
|
||||
message.Command == "popAllAndGoToTabMyVault" ||
|
||||
message.Command == "popAllAndGoToTabSend" ||
|
||||
message.Command == "popAllAndGoToAutofillCiphers")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
if (Current.MainPage is TabsPage tabsPage)
|
||||
{
|
||||
while (tabsPage.Navigation.ModalStack.Count > 0)
|
||||
{
|
||||
await tabsPage.Navigation.PopModalAsync(false);
|
||||
}
|
||||
if (message.Command == "popAllAndGoToAutofillCiphers")
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||
}
|
||||
else if (message.Command == "popAllAndGoToTabMyVault")
|
||||
{
|
||||
Options.MyVaultTile = false;
|
||||
tabsPage.ResetToVaultPage();
|
||||
}
|
||||
else if (message.Command == "popAllAndGoToTabGenerator")
|
||||
{
|
||||
Options.GeneratorTile = false;
|
||||
tabsPage.ResetToGeneratorPage();
|
||||
}
|
||||
else if (message.Command == "popAllAndGoToTabSend")
|
||||
{
|
||||
tabsPage.ResetToSendPage();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (message.Command == "convertAccountToKeyConnector")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
await Application.Current.MainPage.Navigation.PushModalAsync(
|
||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (message.Command == "slept")
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
await SleptAsync();
|
||||
}
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
else if (message.Command == "addAccount")
|
||||
{
|
||||
await AddAccount();
|
||||
}
|
||||
else if (message.Command == "accountAdded")
|
||||
{
|
||||
await UpdateThemeAsync();
|
||||
}
|
||||
else if (message.Command == "switchedAccount")
|
||||
{
|
||||
await SwitchedAccountAsync();
|
||||
}
|
||||
else if (message.Command == "migrated")
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
await SetMainPageAsync();
|
||||
}
|
||||
else if (message.Command == "popAllAndGoToTabGenerator" ||
|
||||
message.Command == "popAllAndGoToTabMyVault" ||
|
||||
message.Command == "popAllAndGoToTabSend" ||
|
||||
message.Command == "popAllAndGoToAutofillCiphers")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
if (Current.MainPage is TabsPage tabsPage)
|
||||
{
|
||||
while (tabsPage.Navigation.ModalStack.Count > 0)
|
||||
{
|
||||
await tabsPage.Navigation.PopModalAsync(false);
|
||||
}
|
||||
if (message.Command == "popAllAndGoToAutofillCiphers")
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||
}
|
||||
else if (message.Command == "popAllAndGoToTabMyVault")
|
||||
{
|
||||
Options.MyVaultTile = false;
|
||||
tabsPage.ResetToVaultPage();
|
||||
}
|
||||
else if (message.Command == "popAllAndGoToTabGenerator")
|
||||
{
|
||||
Options.GeneratorTile = false;
|
||||
tabsPage.ResetToGeneratorPage();
|
||||
}
|
||||
else if (message.Command == "popAllAndGoToTabSend")
|
||||
{
|
||||
tabsPage.ResetToSendPage();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (message.Command == "convertAccountToKeyConnector")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
await Application.Current.MainPage.Navigation.PushModalAsync(
|
||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -234,6 +210,7 @@ namespace Bit.App
|
||||
|
||||
private async Task ResumedAsync()
|
||||
{
|
||||
await _stateService.CheckExtensionActiveUserAndSwitchIfNeededAsync();
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
_messagingService.Send("startEventTimer");
|
||||
await UpdateThemeAsync();
|
||||
@@ -263,102 +240,6 @@ namespace Bit.App
|
||||
new System.Globalization.UmAlQuraCalendar();
|
||||
}
|
||||
|
||||
private async Task LogOutAsync(string userId, bool userInitiated, bool expired)
|
||||
{
|
||||
await AppHelpers.LogOutAsync(userId, userInitiated);
|
||||
await SetMainPageAsync();
|
||||
_authService.LogOut(() =>
|
||||
{
|
||||
if (expired)
|
||||
{
|
||||
_platformUtilsService.ShowToast("warning", null, AppResources.LoginExpired);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task AddAccount()
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
Options.HideAccountSwitcher = false;
|
||||
Current.MainPage = new NavigationPage(new HomePage(Options));
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SwitchedAccountAsync()
|
||||
{
|
||||
await AppHelpers.OnAccountSwitchAsync();
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
if (await _vaultTimeoutService.ShouldTimeoutAsync())
|
||||
{
|
||||
await _vaultTimeoutService.ExecuteTimeoutActionAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await SetMainPageAsync();
|
||||
}
|
||||
await Task.Delay(50);
|
||||
await UpdateThemeAsync();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SetMainPageAsync()
|
||||
{
|
||||
var authed = await _stateService.IsAuthenticatedAsync();
|
||||
if (authed)
|
||||
{
|
||||
if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() ||
|
||||
await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
|
||||
{
|
||||
// TODO implement orgIdentifier flow to SSO Login page, same as email flow below
|
||||
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
|
||||
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null;
|
||||
Current.MainPage = new NavigationPage(new LoginPage(email, Options));
|
||||
}
|
||||
else if (await _vaultTimeoutService.IsLockedAsync() ||
|
||||
await _vaultTimeoutService.ShouldLockAsync())
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new LockPage(Options));
|
||||
}
|
||||
else if (Options.FromAutofillFramework && Options.SaveType.HasValue)
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options));
|
||||
}
|
||||
else if (Options.Uri != null)
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||
}
|
||||
else if (Options.CreateSend != null)
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
|
||||
}
|
||||
else
|
||||
{
|
||||
Current.MainPage = new TabsPage(Options);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null;
|
||||
if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() ||
|
||||
await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
|
||||
{
|
||||
// TODO implement orgIdentifier flow to SSO Login page, same as email flow below
|
||||
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
|
||||
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
Current.MainPage = new NavigationPage(new LoginPage(email, Options));
|
||||
}
|
||||
else
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new HomePage(Options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ClearCacheIfNeededAsync()
|
||||
{
|
||||
var lastClear = await _stateService.GetLastFileCacheClearAsync();
|
||||
@@ -420,7 +301,7 @@ namespace Bit.App
|
||||
UpdateThemeAsync();
|
||||
};
|
||||
Current.MainPage = new NavigationPage(new HomePage(Options));
|
||||
var mainPageTask = SetMainPageAsync();
|
||||
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
|
||||
}
|
||||
|
||||
@@ -441,23 +322,8 @@ namespace Bit.App
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LockedAsync(string userId, bool userInitiated)
|
||||
public async Task SetPreviousPageInfoAsync()
|
||||
{
|
||||
if (!await _stateService.IsActiveAccountAsync(userId))
|
||||
{
|
||||
_platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully);
|
||||
return;
|
||||
}
|
||||
|
||||
var autoPromptBiometric = !userInitiated;
|
||||
if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
var vaultTimeout = await _stateService.GetVaultTimeoutAsync();
|
||||
if (vaultTimeout == 0)
|
||||
{
|
||||
autoPromptBiometric = false;
|
||||
}
|
||||
}
|
||||
PreviousPageInfo lastPageBeforeLock = null;
|
||||
if (Current.MainPage is TabbedPage tabbedPage && tabbedPage.Navigation.ModalStack.Count > 0)
|
||||
{
|
||||
@@ -483,8 +349,44 @@ namespace Bit.App
|
||||
}
|
||||
}
|
||||
await _stateService.SetPreviousPageInfoAsync(lastPageBeforeLock);
|
||||
var lockPage = new LockPage(Options, autoPromptBiometric);
|
||||
Device.BeginInvokeOnMainThread(() => Current.MainPage = new NavigationPage(lockPage));
|
||||
}
|
||||
|
||||
public void Navigate(NavigationTarget navTarget, INavigationParams navParams)
|
||||
{
|
||||
switch (navTarget)
|
||||
{
|
||||
case NavigationTarget.HomeLogin:
|
||||
Current.MainPage = new NavigationPage(new HomePage(Options));
|
||||
break;
|
||||
case NavigationTarget.Login:
|
||||
if (navParams is LoginNavigationParams loginParams)
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new LoginPage(loginParams.Email, Options));
|
||||
}
|
||||
break;
|
||||
case NavigationTarget.Lock:
|
||||
if (navParams is LockNavigationParams lockParams)
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new LockPage(Options, lockParams.AutoPromptBiometric));
|
||||
}
|
||||
else
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new LockPage(Options));
|
||||
}
|
||||
break;
|
||||
case NavigationTarget.Home:
|
||||
Current.MainPage = new TabsPage(Options);
|
||||
break;
|
||||
case NavigationTarget.AddEditCipher:
|
||||
Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options));
|
||||
break;
|
||||
case NavigationTarget.AutofillCiphers:
|
||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||
break;
|
||||
case NavigationTarget.SendAddEdit:
|
||||
Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ namespace Bit.App.Controls
|
||||
|
||||
public int AccountListRowHeight => Device.RuntimePlatform == Device.Android ? 74 : 70;
|
||||
|
||||
public bool LongPressAccountEnabled { get; set; } = true;
|
||||
|
||||
public Action AfterHide { get; set; }
|
||||
|
||||
public async Task ToggleVisibilityAsync()
|
||||
{
|
||||
if (IsVisible)
|
||||
@@ -135,6 +139,8 @@ namespace Bit.App.Controls
|
||||
|
||||
// remove overlay
|
||||
IsVisible = false;
|
||||
|
||||
AfterHide?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,7 +173,7 @@ namespace Bit.App.Controls
|
||||
|
||||
private async Task LongPressAccountAsync(AccountViewCellViewModel item)
|
||||
{
|
||||
if (!item.IsAccount)
|
||||
if (!LongPressAccountEnabled || !item.IsAccount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,23 +45,28 @@ namespace Bit.App.Controls
|
||||
|
||||
public ICommand LongPressAccountCommand { get; }
|
||||
|
||||
public bool FromIOSExtension { get; set; }
|
||||
|
||||
private async Task SelectAccountAsync(AccountViewCellViewModel item)
|
||||
{
|
||||
if (item.AccountView.IsAccount)
|
||||
if (!item.AccountView.IsAccount)
|
||||
{
|
||||
if (!item.AccountView.IsActive)
|
||||
_messagingService.Send(AccountsManagerMessageCommands.ADD_ACCOUNT);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.AccountView.IsActive)
|
||||
{
|
||||
await _stateService.SetActiveUserAsync(item.AccountView.UserId);
|
||||
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||
if (FromIOSExtension)
|
||||
{
|
||||
await _stateService.SetActiveUserAsync(item.AccountView.UserId);
|
||||
_messagingService.Send("switchedAccount");
|
||||
}
|
||||
else if (AllowActiveAccountSelection)
|
||||
{
|
||||
_messagingService.Send("switchedAccount");
|
||||
await _stateService.SaveExtensionActiveUserIdToStorageAsync(item.AccountView.UserId);
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (AllowActiveAccountSelection)
|
||||
{
|
||||
_messagingService.Send("addAccount");
|
||||
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace Bit.App.Controls
|
||||
public AccountViewCellViewModel(AccountView accountView)
|
||||
{
|
||||
AccountView = accountView;
|
||||
AvatarImageSource = new AvatarImageSource(AccountView.Name, AccountView.Email);
|
||||
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
|
||||
?.GetOrCreateAvatar(AccountView.Name, AccountView.Email);
|
||||
}
|
||||
|
||||
public AccountView AccountView
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Controls.AuthenticatorViewCell"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
|
||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||
StyleClass="list-row, list-row-platform"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
x:DataType="pages:GroupingsPageTOTPListItem"
|
||||
ColumnDefinitions="40,*,40,Auto,40"
|
||||
RowSpacing="0"
|
||||
Padding="0,10,0,0"
|
||||
RowDefinitions="*,*">
|
||||
|
||||
<Grid.Resources>
|
||||
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</Grid.Resources>
|
||||
|
||||
<controls:IconLabel
|
||||
Grid.Column="0"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform"
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
|
||||
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
|
||||
<ff:CachedImage
|
||||
Grid.Column="0"
|
||||
BitmapOptimizations="True"
|
||||
ErrorPlaceholder="login.png"
|
||||
LoadingPlaceholder="login.png"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
WidthRequest="22"
|
||||
HeightRequest="22"
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding ShowIconImage}"
|
||||
Source="{Binding IconImageSource, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
VerticalTextAlignment="Center"
|
||||
VerticalOptions="Fill"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Cipher.Name}" />
|
||||
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
VerticalTextAlignment="Center"
|
||||
VerticalOptions="Fill"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Cipher.SubTitle}" />
|
||||
|
||||
<controls:CircularProgressbarView
|
||||
Progress="{Binding Progress}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="CenterAndExpand" />
|
||||
|
||||
<Label
|
||||
Text="{Binding TotpSec, Mode=OneWay}"
|
||||
Style="{DynamicResource textTotp}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
StyleClass="text-sm"
|
||||
HorizontalTextAlignment="Center"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalTextAlignment="Center"
|
||||
VerticalOptions="Fill" />
|
||||
|
||||
<StackLayout
|
||||
Grid.Row="0"
|
||||
Grid.Column="3"
|
||||
Margin="3,0,2,0"
|
||||
Spacing="5"
|
||||
Grid.RowSpan="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="Fill">
|
||||
|
||||
<controls:MonoLabel
|
||||
Text="{Binding TotpCodeFormattedStart, Mode=OneWay}"
|
||||
Style="{DynamicResource textTotp}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalTextAlignment="Center"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="FillAndExpand" />
|
||||
|
||||
<controls:MonoLabel
|
||||
Text="{Binding TotpCodeFormattedEnd, Mode=OneWay}"
|
||||
Style="{DynamicResource textTotp}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalTextAlignment="Center"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="FillAndExpand" />
|
||||
</StackLayout>
|
||||
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
CommandParameter="LoginTotp"
|
||||
Grid.Row="0"
|
||||
Grid.Column="4"
|
||||
Grid.RowSpan="2"
|
||||
Padding="0,0,1,0"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyTotp}" />
|
||||
</controls:ExtendedGrid>
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using Bit.App.Pages;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public partial class AuthenticatorViewCell : ExtendedGrid
|
||||
{
|
||||
public static readonly BindableProperty CipherProperty = BindableProperty.Create(
|
||||
nameof(Cipher), typeof(CipherView), typeof(AuthenticatorViewCell), default(CipherView), BindingMode.TwoWay);
|
||||
|
||||
public static readonly BindableProperty WebsiteIconsEnabledProperty = BindableProperty.Create(
|
||||
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(AuthenticatorViewCell));
|
||||
|
||||
public static readonly BindableProperty TotpSecProperty = BindableProperty.Create(
|
||||
nameof(TotpSec), typeof(long), typeof(AuthenticatorViewCell));
|
||||
|
||||
public AuthenticatorViewCell()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public Command CopyCommand { get; set; }
|
||||
|
||||
public CipherView Cipher
|
||||
{
|
||||
get => GetValue(CipherProperty) as CipherView;
|
||||
set => SetValue(CipherProperty, value);
|
||||
}
|
||||
|
||||
public bool? WebsiteIconsEnabled
|
||||
{
|
||||
get => (bool)GetValue(WebsiteIconsEnabledProperty);
|
||||
set => SetValue(WebsiteIconsEnabledProperty, value);
|
||||
}
|
||||
|
||||
public long TotpSec
|
||||
{
|
||||
get => (long)GetValue(TotpSecProperty);
|
||||
set => SetValue(TotpSecProperty, value);
|
||||
}
|
||||
|
||||
public bool ShowIconImage
|
||||
{
|
||||
get => WebsiteIconsEnabled ?? false
|
||||
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
|
||||
&& IconImageSource != null;
|
||||
}
|
||||
|
||||
private string _iconImageSource = string.Empty;
|
||||
public string IconImageSource
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_iconImageSource == string.Empty) // default value since icon source can return null
|
||||
{
|
||||
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
|
||||
}
|
||||
return _iconImageSource;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SkiaSharp;
|
||||
@@ -50,7 +51,7 @@ namespace Bit.App.Controls
|
||||
|
||||
private Stream Draw()
|
||||
{
|
||||
string chars = null;
|
||||
string chars;
|
||||
string upperData = null;
|
||||
|
||||
if (string.IsNullOrEmpty(_data))
|
||||
@@ -71,62 +72,83 @@ namespace Bit.App.Controls
|
||||
var textColor = Color.White;
|
||||
var size = 50;
|
||||
|
||||
var bitmap = new SKBitmap(
|
||||
size * 2,
|
||||
using (var bitmap = new SKBitmap(size * 2,
|
||||
size * 2,
|
||||
SKImageInfo.PlatformColorType,
|
||||
SKAlphaType.Premul);
|
||||
var canvas = new SKCanvas(bitmap);
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
|
||||
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
|
||||
var radius = midX - midX / 5;
|
||||
|
||||
var circlePaint = new SKPaint
|
||||
SKAlphaType.Premul))
|
||||
{
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Fill,
|
||||
StrokeJoin = SKStrokeJoin.Miter,
|
||||
Color = SKColor.Parse(bgColor.ToHex())
|
||||
};
|
||||
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
||||
using (var canvas = new SKCanvas(bitmap))
|
||||
{
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
using (var paint = new SKPaint
|
||||
{
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Fill,
|
||||
StrokeJoin = SKStrokeJoin.Miter,
|
||||
Color = SKColor.Parse(bgColor.ToHex())
|
||||
})
|
||||
{
|
||||
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
|
||||
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
|
||||
var radius = midX - midX / 5;
|
||||
|
||||
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
|
||||
var textSize = midX / 1.3f;
|
||||
var textPaint = new SKPaint
|
||||
{
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Fill,
|
||||
Color = SKColor.Parse(textColor.ToHex()),
|
||||
TextSize = textSize,
|
||||
TextAlign = SKTextAlign.Center,
|
||||
Typeface = typeface
|
||||
};
|
||||
var rect = new SKRect();
|
||||
textPaint.MeasureText(chars, ref rect);
|
||||
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
|
||||
using (var circlePaint = new SKPaint
|
||||
{
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Fill,
|
||||
StrokeJoin = SKStrokeJoin.Miter,
|
||||
Color = SKColor.Parse(bgColor.ToHex())
|
||||
})
|
||||
{
|
||||
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
||||
|
||||
return SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100).AsStream();
|
||||
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
|
||||
var textSize = midX / 1.3f;
|
||||
using (var textPaint = new SKPaint
|
||||
{
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Fill,
|
||||
Color = SKColor.Parse(textColor.ToHex()),
|
||||
TextSize = textSize,
|
||||
TextAlign = SKTextAlign.Center,
|
||||
Typeface = typeface
|
||||
})
|
||||
{
|
||||
var rect = new SKRect();
|
||||
textPaint.MeasureText(chars, ref rect);
|
||||
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
|
||||
|
||||
using (var img = SKImage.FromBitmap(bitmap))
|
||||
{
|
||||
var data = img.Encode(SKEncodedImageFormat.Png, 100);
|
||||
return data?.AsStream(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFirstLetters(string data, int charCount)
|
||||
{
|
||||
var parts = data.Split();
|
||||
var sanitizedData = data.Trim();
|
||||
var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length > 1 && charCount <= 2)
|
||||
{
|
||||
var text = "";
|
||||
for (int i = 0; i < charCount; i++)
|
||||
var text = string.Empty;
|
||||
for (var i = 0; i < charCount; i++)
|
||||
{
|
||||
text += parts[i].Substring(0, 1);
|
||||
text += parts[i][0];
|
||||
}
|
||||
return text;
|
||||
}
|
||||
if (data.Length > 2)
|
||||
if (sanitizedData.Length > 2)
|
||||
{
|
||||
return data.Substring(0, 2);
|
||||
return sanitizedData.Substring(0, 2);
|
||||
}
|
||||
return data;
|
||||
return sanitizedData;
|
||||
}
|
||||
|
||||
private Color StringToColor(string str)
|
||||
|
||||
33
src/App/Controls/AvatarImageSourcePool.cs
Normal file
33
src/App/Controls/AvatarImageSourcePool.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public interface IAvatarImageSourcePool
|
||||
{
|
||||
AvatarImageSource GetOrCreateAvatar(string name, string email);
|
||||
}
|
||||
|
||||
public class AvatarImageSourcePool : IAvatarImageSourcePool
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
|
||||
|
||||
public AvatarImageSource GetOrCreateAvatar(string name, string email)
|
||||
{
|
||||
var key = $"{name}{email}";
|
||||
if (!_cache.TryGetValue(key, out var avatar))
|
||||
{
|
||||
avatar = new AvatarImageSource(name, email);
|
||||
if (!_cache.TryAdd(key, avatar)
|
||||
&&
|
||||
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.
|
||||
{
|
||||
// if add and get after fails, then something wrong is going on with this method.
|
||||
throw new InvalidOperationException("Something is wrong creating the avatar image");
|
||||
}
|
||||
}
|
||||
return avatar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
<controls:MiButton
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Text=""
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ViewCellMenu}}"
|
||||
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
|
||||
Clicked="MoreButton_Clicked"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
|
||||
139
src/App/Controls/CircularProgressbarView.cs
Normal file
139
src/App/Controls/CircularProgressbarView.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.Views.Forms;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class CircularProgressbarView : SKCanvasView
|
||||
{
|
||||
private Circle _circle;
|
||||
|
||||
public static readonly BindableProperty ProgressProperty = BindableProperty.Create(
|
||||
nameof(Progress), typeof(double), typeof(CircularProgressbarView), propertyChanged: OnProgressChanged);
|
||||
|
||||
public static readonly BindableProperty RadiusProperty = BindableProperty.Create(
|
||||
nameof(Radius), typeof(float), typeof(CircularProgressbarView), 15f);
|
||||
|
||||
public static readonly BindableProperty StrokeWidthProperty = BindableProperty.Create(
|
||||
nameof(StrokeWidth), typeof(float), typeof(CircularProgressbarView), 3f);
|
||||
|
||||
public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create(
|
||||
nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
||||
|
||||
public static readonly BindableProperty EndingProgressColorProperty = BindableProperty.Create(
|
||||
nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
||||
|
||||
public static readonly BindableProperty BackgroundProgressColorProperty = BindableProperty.Create(
|
||||
nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
||||
|
||||
public double Progress
|
||||
{
|
||||
get { return (double)GetValue(ProgressProperty); }
|
||||
set { SetValue(ProgressProperty, value); }
|
||||
}
|
||||
|
||||
public float Radius
|
||||
{
|
||||
get => (float)GetValue(RadiusProperty);
|
||||
set => SetValue(RadiusProperty, value);
|
||||
}
|
||||
public float StrokeWidth
|
||||
{
|
||||
get => (float)GetValue(StrokeWidthProperty);
|
||||
set => SetValue(StrokeWidthProperty, value);
|
||||
}
|
||||
|
||||
public Color ProgressColor
|
||||
{
|
||||
get => (Color)GetValue(ProgressColorProperty);
|
||||
set => SetValue(ProgressColorProperty, value);
|
||||
}
|
||||
|
||||
public Color EndingProgressColor
|
||||
{
|
||||
get => (Color)GetValue(EndingProgressColorProperty);
|
||||
set => SetValue(EndingProgressColorProperty, value);
|
||||
}
|
||||
|
||||
public Color BackgroundProgressColor
|
||||
{
|
||||
get => (Color)GetValue(BackgroundProgressColorProperty);
|
||||
set => SetValue(BackgroundProgressColorProperty, value);
|
||||
}
|
||||
|
||||
private static void OnProgressChanged(BindableObject bindable, object oldvalue, object newvalue)
|
||||
{
|
||||
var context = bindable as CircularProgressbarView;
|
||||
context.InvalidateSurface();
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
base.OnPropertyChanged(propertyName);
|
||||
if (propertyName == nameof(Progress))
|
||||
{
|
||||
_circle = new Circle(Radius * (float)DeviceDisplay.MainDisplayInfo.Density, (info) => new SKPoint((float)info.Width / 2, (float)info.Height / 2));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
|
||||
{
|
||||
base.OnPaintSurface(e);
|
||||
if (_circle != null)
|
||||
{
|
||||
_circle.CalculateCenter(e.Info);
|
||||
e.Surface.Canvas.Clear();
|
||||
DrawCircle(e.Surface.Canvas, _circle, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, BackgroundProgressColor.ToSKColor());
|
||||
DrawArc(e.Surface.Canvas, _circle, () => (float)Progress, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, ProgressColor.ToSKColor(), EndingProgressColor.ToSKColor());
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCircle(SKCanvas canvas, Circle circle, float strokewidth, SKColor color)
|
||||
{
|
||||
canvas.DrawCircle(circle.Center, circle.Redius,
|
||||
new SKPaint()
|
||||
{
|
||||
StrokeWidth = strokewidth,
|
||||
Color = color,
|
||||
IsStroke = true,
|
||||
IsAntialias = true
|
||||
});
|
||||
}
|
||||
|
||||
private void DrawArc(SKCanvas canvas, Circle circle, Func<float> progress, float strokewidth, SKColor color, SKColor progressEndColor)
|
||||
{
|
||||
var progressValue = progress();
|
||||
var angle = progressValue * 3.6f;
|
||||
canvas.DrawArc(circle.Rect, 270, angle, false,
|
||||
new SKPaint()
|
||||
{
|
||||
StrokeWidth = strokewidth,
|
||||
Color = progressValue < 20f ? progressEndColor : color,
|
||||
IsStroke = true,
|
||||
IsAntialias = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class Circle
|
||||
{
|
||||
private readonly Func<SKImageInfo, SKPoint> _centerFunc;
|
||||
|
||||
public Circle(float redius, Func<SKImageInfo, SKPoint> centerFunc)
|
||||
{
|
||||
_centerFunc = centerFunc;
|
||||
Redius = redius;
|
||||
}
|
||||
public SKPoint Center { get; set; }
|
||||
public float Redius { get; set; }
|
||||
public SKRect Rect => new SKRect(Center.X - Redius, Center.Y - Redius, Center.X + Redius, Center.Y + Redius);
|
||||
|
||||
public void CalculateCenter(SKImageInfo argsInfo)
|
||||
{
|
||||
Center = _centerFunc(argsInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/App/Controls/DateTime/DateTimePicker.xaml
Normal file
20
src/App/Controls/DateTime/DateTimePicker.xaml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Grid
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
x:Class="Bit.App.Controls.DateTimePicker"
|
||||
ColumnDefinitions="*,*">
|
||||
<controls:ExtendedDatePicker
|
||||
x:Name="_datePicker"
|
||||
Grid.Column="0"
|
||||
NullableDate="{Binding Date, Mode=TwoWay}"
|
||||
Format="d"
|
||||
AutomationProperties.IsInAccessibleTree="True" />
|
||||
<controls:ExtendedTimePicker
|
||||
x:Name="_timePicker"
|
||||
Grid.Column="1"
|
||||
NullableTime="{Binding Time, Mode=TwoWay}"
|
||||
Format="t"
|
||||
AutomationProperties.IsInAccessibleTree="True" />
|
||||
</Grid>
|
||||
34
src/App/Controls/DateTime/DateTimePicker.xaml.cs
Normal file
34
src/App/Controls/DateTime/DateTimePicker.xaml.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Xamarin.CommunityToolkit.UI.Views;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public partial class DateTimePicker : Grid
|
||||
{
|
||||
public DateTimePicker()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
base.OnPropertyChanged(propertyName);
|
||||
|
||||
if (propertyName == nameof(BindingContext)
|
||||
&&
|
||||
BindingContext is DateTimeViewModel dateTimeViewModel)
|
||||
{
|
||||
AutomationProperties.SetName(_datePicker, dateTimeViewModel.DateName);
|
||||
AutomationProperties.SetName(_timePicker, dateTimeViewModel.TimeName);
|
||||
|
||||
_datePicker.PlaceHolder = dateTimeViewModel.DatePlaceholder;
|
||||
_timePicker.PlaceHolder = dateTimeViewModel.TimePlaceholder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class LazyDateTimePicker : LazyView<DateTimePicker>
|
||||
{
|
||||
}
|
||||
}
|
||||
70
src/App/Controls/DateTime/DateTimeViewModel.cs
Normal file
70
src/App/Controls/DateTime/DateTimeViewModel.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class DateTimeViewModel : ExtendedViewModel
|
||||
{
|
||||
DateTime? _date;
|
||||
TimeSpan? _time;
|
||||
|
||||
public DateTimeViewModel(string dateName, string timeName)
|
||||
{
|
||||
DateName = dateName;
|
||||
TimeName = timeName;
|
||||
}
|
||||
|
||||
public Action<DateTime?> OnDateChanged { get; set; }
|
||||
public Action<TimeSpan?> OnTimeChanged { get; set; }
|
||||
|
||||
public DateTime? Date
|
||||
{
|
||||
get => _date;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _date, value))
|
||||
{
|
||||
OnDateChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
public TimeSpan? Time
|
||||
{
|
||||
get => _time;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _time, value))
|
||||
{
|
||||
OnTimeChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string DateName { get; }
|
||||
public string TimeName { get; }
|
||||
|
||||
public string DatePlaceholder { get; set; }
|
||||
public string TimePlaceholder { get; set; }
|
||||
|
||||
public DateTime? DateTime
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Date.HasValue)
|
||||
{
|
||||
if (Time.HasValue)
|
||||
{
|
||||
return Date.Value.Add(Time.Value);
|
||||
}
|
||||
return Date;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
Date = value?.Date;
|
||||
Time = value?.Date.TimeOfDay;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,6 @@ namespace Bit.App.Controls
|
||||
{
|
||||
public class ExtendedCollectionView : CollectionView
|
||||
{
|
||||
public string ExtraDataForLogging { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
<controls:MiButton
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Text=""
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ViewCellMenu}}"
|
||||
IsVisible="{Binding ShowOptions, Mode=OneWay}"
|
||||
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
|
||||
Clicked="MoreButton_Clicked"
|
||||
|
||||
@@ -46,7 +46,11 @@ namespace Bit.App.Pages
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new[] { nameof(ShowPasswordIcon) });
|
||||
additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText)
|
||||
});
|
||||
}
|
||||
|
||||
public bool IsPolicyInEffect
|
||||
@@ -68,6 +72,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string MasterPassword { get; set; }
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Submit_Clicked" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Command="{Binding SubmitCommand}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
|
||||
@@ -36,14 +36,6 @@ namespace Bit.App.Pages
|
||||
};
|
||||
}
|
||||
|
||||
private async void Submit_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubmitSuccessAsync()
|
||||
{
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class EnvironmentPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
public EnvironmentPageViewModel()
|
||||
{
|
||||
@@ -22,10 +24,10 @@ namespace Bit.App.Pages
|
||||
IdentityUrl = _environmentService.IdentityUrl;
|
||||
IconsUrl = _environmentService.IconsUrl;
|
||||
NotificationsUrls = _environmentService.NotificationsUrl;
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public ICommand SubmitCommand { get; }
|
||||
public string BaseUrl { get; set; }
|
||||
public string ApiUrl { get; set; }
|
||||
public string IdentityUrl { get; set; }
|
||||
@@ -37,6 +39,12 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
if (!ValidateUrls())
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
|
||||
{
|
||||
Base = BaseUrl,
|
||||
@@ -57,5 +65,25 @@ namespace Bit.App.Pages
|
||||
|
||||
SubmitSuccessAction?.Invoke();
|
||||
}
|
||||
|
||||
public bool ValidateUrls()
|
||||
{
|
||||
bool IsUrlValid(string url)
|
||||
{
|
||||
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute);
|
||||
}
|
||||
|
||||
return IsUrlValid(BaseUrl)
|
||||
&& IsUrlValid(ApiUrl)
|
||||
&& IsUrlValid(IdentityUrl)
|
||||
&& IsUrlValid(WebVaultUrl)
|
||||
&& IsUrlValid(IconsUrl);
|
||||
}
|
||||
|
||||
private void OnSubmitException(Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
||||
}
|
||||
_broadcasterService.Subscribe(nameof(HomePage), async (message) =>
|
||||
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
|
||||
{
|
||||
if (message.Command == "updatedTheme")
|
||||
{
|
||||
|
||||
@@ -80,7 +80,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||
</Grid>
|
||||
<Grid
|
||||
x:Name="_passwordGrid"
|
||||
@@ -119,7 +120,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</Grid>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
|
||||
@@ -25,8 +25,6 @@ namespace Bit.App.Pages
|
||||
_vm = BindingContext as LockPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
PinEntry = _pin;
|
||||
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
@@ -38,8 +36,17 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public Entry MasterPasswordEntry { get; set; }
|
||||
public Entry PinEntry { get; set; }
|
||||
public Entry SecretEntry
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_vm?.PinLock ?? false)
|
||||
{
|
||||
return _pin;
|
||||
}
|
||||
return _masterPassword;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PromptBiometricAfterResumeAsync()
|
||||
{
|
||||
@@ -70,16 +77,12 @@ namespace Bit.App.Pages
|
||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
||||
|
||||
await _vm.InitAsync();
|
||||
|
||||
_vm.FocusSecretEntry += PerformFocusSecretEntry;
|
||||
|
||||
if (!_vm.BiometricLock)
|
||||
{
|
||||
if (_vm.PinLock)
|
||||
{
|
||||
RequestFocus(PinEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
RequestFocus(MasterPasswordEntry);
|
||||
}
|
||||
RequestFocus(SecretEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -99,6 +102,18 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformFocusSecretEntry(int? cursorPosition)
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
SecretEntry.Focus();
|
||||
if (cursorPosition.HasValue)
|
||||
{
|
||||
SecretEntry.CursorPosition = cursorPosition.Value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override bool OnBackButtonPressed()
|
||||
{
|
||||
if (_accountListOverlay.IsVisible)
|
||||
|
||||
@@ -10,6 +10,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.Helpers;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -27,6 +28,7 @@ namespace Bit.App.Pages
|
||||
private readonly IBiometricService _biometricService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
|
||||
|
||||
private string _email;
|
||||
private bool _showPassword;
|
||||
@@ -72,7 +74,8 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowPasswordIcon)
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -128,9 +131,15 @@ namespace Bit.App.Pages
|
||||
public Command SubmitCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string MasterPassword { get; set; }
|
||||
public string Pin { get; set; }
|
||||
public Action UnlockedAction { get; set; }
|
||||
public event Action<int?> FocusSecretEntry
|
||||
{
|
||||
add => _secretEntryFocusWeakEventManager.AddEventHandler(value);
|
||||
remove => _secretEntryFocusWeakEventManager.RemoveEventHandler(value);
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
@@ -344,11 +353,8 @@ namespace Bit.App.Pages
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
var page = (Page as LockPage);
|
||||
var entry = PinLock ? page.PinEntry : page.MasterPasswordEntry;
|
||||
var str = PinLock ? Pin : MasterPassword;
|
||||
entry.Focus();
|
||||
entry.CursorPosition = String.IsNullOrEmpty(str) ? 0 : str.Length;
|
||||
var secret = PinLock ? Pin : MasterPassword;
|
||||
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
|
||||
}
|
||||
|
||||
public async Task PromptBiometricAsync()
|
||||
@@ -359,18 +365,8 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
PinLock ? AppResources.PIN : AppResources.MasterPassword, () =>
|
||||
{
|
||||
var page = Page as LockPage;
|
||||
if (PinLock)
|
||||
{
|
||||
page.PinEntry.Focus();
|
||||
}
|
||||
else
|
||||
{
|
||||
page.MasterPasswordEntry.Focus();
|
||||
}
|
||||
});
|
||||
PinLock ? AppResources.PIN : AppResources.MasterPassword,
|
||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
|
||||
await _stateService.SetBiometricLockedAsync(!success);
|
||||
if (success)
|
||||
{
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 0">
|
||||
|
||||
@@ -58,7 +58,8 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowPasswordIcon)
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,6 +86,7 @@ namespace Bit.App.Pages
|
||||
public Command LogInCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action LogInSuccessAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
|
||||
@@ -81,10 +81,12 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
||||
string ssoToken;
|
||||
|
||||
try
|
||||
{
|
||||
await _apiService.PreValidateSso(OrgIdentifier);
|
||||
var response = await _apiService.PreValidateSso(OrgIdentifier);
|
||||
ssoToken = response.Token;
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
@@ -112,7 +114,8 @@ namespace Bit.App.Pages
|
||||
"response_type=code&scope=api%20offline_access&" +
|
||||
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
|
||||
"code_challenge_method=S256&response_mode=query&" +
|
||||
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier);
|
||||
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier) + "&" +
|
||||
"ssoToken=" + Uri.EscapeDataString(ssoToken);
|
||||
|
||||
WebAuthenticatorResult authResult = null;
|
||||
try
|
||||
|
||||
@@ -68,7 +68,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordDescription}"
|
||||
@@ -106,7 +107,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
|
||||
@@ -51,7 +51,8 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowPasswordIcon)
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,6 +74,7 @@ namespace Bit.App.Pages
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public Command ToggleConfirmPasswordCommand { get; }
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string MasterPassword { get; set; }
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordDescription}"
|
||||
@@ -145,7 +146,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
|
||||
@@ -55,7 +55,11 @@ namespace Bit.App.Pages
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new[] { nameof(ShowPasswordIcon) });
|
||||
additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText)
|
||||
});
|
||||
}
|
||||
|
||||
public bool IsPolicyInEffect
|
||||
@@ -86,6 +90,7 @@ namespace Bit.App.Pages
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public Command ToggleConfirmPasswordCommand { get; }
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string MasterPassword { get; set; }
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
@@ -214,7 +219,8 @@ namespace Bit.App.Pages
|
||||
// Request
|
||||
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
|
||||
{
|
||||
ResetPasswordKey = encryptedKey.EncryptedString
|
||||
ResetPasswordKey = encryptedKey.EncryptedString,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
};
|
||||
var userId = await _stateService.GetActiveUserIdAsync();
|
||||
// Enroll user
|
||||
|
||||
@@ -105,7 +105,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
@@ -140,7 +141,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding History}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
StyleClass="list, list-platform">
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Generator History Page">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="domain:GeneratedPasswordHistory">
|
||||
<Grid
|
||||
|
||||
@@ -58,8 +58,7 @@ namespace Bit.App.Pages
|
||||
private async void CopyAsync(GeneratedPasswordHistory ph)
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(ph.Password);
|
||||
_platformUtilsService.ShowToast("info", null,
|
||||
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
|
||||
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
|
||||
}
|
||||
|
||||
public async Task UpdateOnThemeChanged()
|
||||
|
||||
@@ -183,52 +183,68 @@
|
||||
<Label
|
||||
Text="A-Z"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n UppercaseAtoZ}"/>
|
||||
<Switch
|
||||
IsToggled="{Binding Uppercase}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseUppercase,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n UppercaseAtoZ}"/>
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="a-z"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n LowercaseAtoZ}"/>
|
||||
<Switch
|
||||
IsToggled="{Binding Lowercase}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseLowercase,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n LowercaseAtoZ}" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="0-9"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n NumbersZeroToNine}"/>
|
||||
<Switch
|
||||
IsToggled="{Binding Number}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseNumbers,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n NumbersZeroToNine}"/>
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="!@#$%^&*"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n SpecialCharacters}"/>
|
||||
<Switch
|
||||
IsToggled="{Binding Special}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseSpecial,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n SpecialCharacters}"/>
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-stepper">
|
||||
@@ -277,7 +293,7 @@
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding AvoidAmbiguous}"
|
||||
IsToggled="{Binding AvoidAmbiguousChars}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
|
||||
@@ -5,7 +5,6 @@ using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -23,7 +22,7 @@ namespace Bit.App.Pages
|
||||
private bool _lowercase;
|
||||
private bool _number;
|
||||
private bool _special;
|
||||
private bool _avoidAmbiguous;
|
||||
private bool _allowAmbiguousChars;
|
||||
private int _minNumber;
|
||||
private int _minSpecial;
|
||||
private int _length = 5;
|
||||
@@ -130,19 +129,29 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public bool AvoidAmbiguous
|
||||
public bool AllowAmbiguousChars
|
||||
{
|
||||
get => _avoidAmbiguous;
|
||||
get => _allowAmbiguousChars;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _avoidAmbiguous, value))
|
||||
if (SetProperty(ref _allowAmbiguousChars, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(AvoidAmbiguousChars)
|
||||
}))
|
||||
{
|
||||
_options.Ambiguous = !value;
|
||||
_options.AllowAmbiguousChar = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AvoidAmbiguousChars
|
||||
{
|
||||
get => !AllowAmbiguousChars;
|
||||
set => AllowAmbiguousChars = !value;
|
||||
}
|
||||
|
||||
public int MinNumber
|
||||
{
|
||||
get => _minNumber;
|
||||
@@ -309,13 +318,12 @@ namespace Bit.App.Pages
|
||||
public async Task CopyAsync()
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(Password);
|
||||
_platformUtilsService.ShowToast("success", null,
|
||||
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
|
||||
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
|
||||
}
|
||||
|
||||
private void LoadFromOptions()
|
||||
{
|
||||
AvoidAmbiguous = !_options.Ambiguous.GetValueOrDefault();
|
||||
AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault();
|
||||
TypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
|
||||
IsPassword = TypeSelectedIndex == 0;
|
||||
MinNumber = _options.MinNumber.GetValueOrDefault();
|
||||
@@ -333,7 +341,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private void SetOptions()
|
||||
{
|
||||
_options.Ambiguous = !AvoidAmbiguous;
|
||||
_options.AllowAmbiguousChar = AllowAmbiguousChars;
|
||||
_options.Type = TypeSelectedIndex == 1 ? "passphrase" : "password";
|
||||
_options.MinNumber = MinNumber;
|
||||
_options.MinSpecial = MinSpecial;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
|
||||
x:Class="Bit.App.Pages.SendAddEditPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
@@ -121,6 +122,7 @@
|
||||
Clicked="FileType_Clicked"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n File}"
|
||||
AutomationProperties.HelpText="{Binding FileTypeAccessibilityLabel}"
|
||||
Grid.Column="0">
|
||||
</Button>
|
||||
<Button
|
||||
@@ -132,6 +134,7 @@
|
||||
Clicked="TextType_Clicked"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Text}"
|
||||
AutomationProperties.HelpText="{Binding TextTypeAccessibilityLabel}"
|
||||
Grid.Column="1">
|
||||
</Button>
|
||||
</Grid>
|
||||
@@ -250,28 +253,31 @@
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Spacing="0">
|
||||
Spacing="0"
|
||||
xct:TouchEffect.Command="{Binding ToggleOptionsCommand}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{Binding OptionsAccessilibityText}">
|
||||
<Button
|
||||
Text="{u:I18n Options}"
|
||||
x:Name="_btnOptions"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
Margin="0"
|
||||
Clicked="ToggleOptions_Clicked"/>
|
||||
AutomationProperties.IsInAccessibleTree="False"/>
|
||||
<controls:IconButton
|
||||
x:Name="_btnOptionsUp"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ChevronUp}}"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
Clicked="ToggleOptions_Clicked"
|
||||
IsVisible="{Binding ShowOptions}" />
|
||||
IsVisible="{Binding ShowOptions}"
|
||||
AutomationProperties.IsInAccessibleTree="False"/>
|
||||
<controls:IconButton
|
||||
x:Name="_btnOptionsDown"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.AngleDown}}"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
Clicked="ToggleOptions_Clicked"
|
||||
IsVisible="{Binding ShowOptions, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding ShowOptions, Converter={StaticResource inverseBool}}"
|
||||
AutomationProperties.IsInAccessibleTree="False"/>
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding ShowOptions}">
|
||||
<StackLayout
|
||||
@@ -297,14 +303,14 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<controls:ExtendedDatePicker
|
||||
NullableDate="{Binding DeletionDate, Mode=TwoWay}"
|
||||
NullableDate="{Binding DeletionDateTimeViewModel.Date, Mode=TwoWay}"
|
||||
Format="d"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n DeletionDate}"
|
||||
Grid.Column="0" />
|
||||
<controls:ExtendedTimePicker
|
||||
NullableTime="{Binding DeletionTime, Mode=TwoWay}"
|
||||
NullableTime="{Binding DeletionDateTimeViewModel.Time, Mode=TwoWay}"
|
||||
Format="t"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
@@ -337,7 +343,7 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<controls:ExtendedDatePicker
|
||||
NullableDate="{Binding ExpirationDate, Mode=TwoWay}"
|
||||
NullableDate="{Binding ExpirationDateTimeViewModel.Date, Mode=TwoWay}"
|
||||
PlaceHolder="mm/dd/yyyy"
|
||||
Format="d"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
@@ -345,7 +351,7 @@
|
||||
AutomationProperties.Name="{u:I18n ExpirationDate}"
|
||||
Grid.Column="0" />
|
||||
<controls:ExtendedTimePicker
|
||||
NullableTime="{Binding ExpirationTime, Mode=TwoWay}"
|
||||
NullableTime="{Binding ExpirationDateTimeViewModel.Time, Mode=TwoWay}"
|
||||
PlaceHolder="--:-- --"
|
||||
Format="t"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
@@ -438,7 +444,8 @@
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Margin="10,0,0,0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n PasswordInfo}"
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace Bit.App.Pages
|
||||
private AppOptions _appOptions;
|
||||
private SendAddEditPageViewModel _vm;
|
||||
|
||||
public Action OnClose { get; set; }
|
||||
public Action AfterSubmit { get; set; }
|
||||
|
||||
public SendAddEditPage(
|
||||
@@ -136,14 +135,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task CloseAsync()
|
||||
{
|
||||
if (OnClose is null)
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnClose();
|
||||
}
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
|
||||
protected override bool OnBackButtonPressed()
|
||||
@@ -209,11 +201,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleOptions_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
_vm.ToggleOptions();
|
||||
}
|
||||
|
||||
private void ClearExpirationDate_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
@@ -23,7 +24,7 @@ namespace Bit.App.Pages
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ISendService _sendService;
|
||||
private readonly ILogger _logger;
|
||||
private bool _sendEnabled;
|
||||
private bool _sendEnabled = true;
|
||||
private bool _canAccessPremium;
|
||||
private bool _emailVerified;
|
||||
private SendView _send;
|
||||
@@ -33,17 +34,15 @@ namespace Bit.App.Pages
|
||||
private int _deletionDateTypeSelectedIndex;
|
||||
private int _expirationDateTypeSelectedIndex;
|
||||
private DateTime _simpleDeletionDateTime;
|
||||
private DateTime _deletionDate;
|
||||
private TimeSpan _deletionTime;
|
||||
private DateTime? _simpleExpirationDateTime;
|
||||
private DateTime? _expirationDate;
|
||||
private TimeSpan? _expirationTime;
|
||||
private bool _isOverridingPickers;
|
||||
private int? _maxAccessCount;
|
||||
private string[] _additionalSendProperties = new[]
|
||||
{
|
||||
nameof(IsText),
|
||||
nameof(IsFile),
|
||||
nameof(FileTypeAccessibilityLabel),
|
||||
nameof(TextTypeAccessibilityLabel)
|
||||
};
|
||||
private bool _disableHideEmail;
|
||||
private bool _sendOptionsPolicyInEffect;
|
||||
@@ -59,6 +58,7 @@ namespace Bit.App.Pages
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleOptionsCommand = new Command(ToggleOptions);
|
||||
|
||||
TypeOptions = new List<KeyValuePair<string, SendType>>
|
||||
{
|
||||
@@ -86,9 +86,36 @@ namespace Bit.App.Pages
|
||||
new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays),
|
||||
new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom),
|
||||
};
|
||||
|
||||
DeletionDateTimeViewModel = new DateTimeViewModel(AppResources.DeletionDate, AppResources.DeletionTime);
|
||||
ExpirationDateTimeViewModel = new DateTimeViewModel(AppResources.ExpirationDate, AppResources.ExpirationTime)
|
||||
{
|
||||
OnDateChanged = date =>
|
||||
{
|
||||
if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Time.HasValue)
|
||||
{
|
||||
// auto-set time to current time upon setting date
|
||||
ExpirationDateTimeViewModel.Time = DateTimeNow().TimeOfDay;
|
||||
}
|
||||
},
|
||||
OnTimeChanged = time =>
|
||||
{
|
||||
if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Date.HasValue)
|
||||
{
|
||||
// auto-set date to current date upon setting time
|
||||
ExpirationDateTimeViewModel.Date = DateTime.Today;
|
||||
}
|
||||
},
|
||||
DatePlaceholder = "mm/dd/yyyy",
|
||||
TimePlaceholder = "--:-- --"
|
||||
};
|
||||
|
||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger);
|
||||
}
|
||||
|
||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||
public Command TogglePasswordCommand { get; set; }
|
||||
public Command ToggleOptionsCommand { get; set; }
|
||||
public string SendId { get; set; }
|
||||
public int SegmentedButtonHeight { get; set; }
|
||||
public int SegmentedButtonFontSize { get; set; }
|
||||
@@ -102,6 +129,7 @@ namespace Bit.App.Pages
|
||||
public bool DisableHideEmailControl { get; set; }
|
||||
public bool IsAddFromShare { get; set; }
|
||||
public string ShareOnSaveText => CopyInsteadOfShareAfterSaving ? AppResources.CopySendLinkOnSave : AppResources.ShareOnSave;
|
||||
public string OptionsAccessilibityText => ShowOptions ? AppResources.OptionsExpanded : AppResources.OptionsCollapsed;
|
||||
public List<KeyValuePair<string, SendType>> TypeOptions { get; }
|
||||
public List<KeyValuePair<string, string>> DeletionTypeOptions { get; }
|
||||
public List<KeyValuePair<string, string>> ExpirationTypeOptions { get; }
|
||||
@@ -121,20 +149,15 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
public DateTime DeletionDate
|
||||
{
|
||||
get => _deletionDate;
|
||||
set => SetProperty(ref _deletionDate, value);
|
||||
}
|
||||
public TimeSpan DeletionTime
|
||||
{
|
||||
get => _deletionTime;
|
||||
set => SetProperty(ref _deletionTime, value);
|
||||
}
|
||||
public bool ShowOptions
|
||||
{
|
||||
get => _showOptions;
|
||||
set => SetProperty(ref _showOptions, value);
|
||||
set => SetProperty(ref _showOptions, value,
|
||||
additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(OptionsAccessilibityText),
|
||||
nameof(OptionsShowHideIcon)
|
||||
});
|
||||
}
|
||||
public int ExpirationDateTypeSelectedIndex
|
||||
{
|
||||
@@ -147,28 +170,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
public DateTime? ExpirationDate
|
||||
{
|
||||
get => _expirationDate;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _expirationDate, value))
|
||||
{
|
||||
ExpirationDateChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
public TimeSpan? ExpirationTime
|
||||
{
|
||||
get => _expirationTime;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _expirationTime, value))
|
||||
{
|
||||
ExpirationTimeChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int? MaxAccessCount
|
||||
{
|
||||
get => _maxAccessCount;
|
||||
@@ -196,7 +198,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
public string FileName
|
||||
{
|
||||
get => _fileName;
|
||||
get => _fileName ?? AppResources.NoFileChosen;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _fileName, value))
|
||||
@@ -211,7 +213,8 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(ShowPasswordIcon)
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText)
|
||||
});
|
||||
}
|
||||
public bool DisableHideEmail
|
||||
@@ -230,7 +233,13 @@ namespace Bit.App.Pages
|
||||
public bool IsFile => Send?.Type == SendType.File;
|
||||
public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6;
|
||||
public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7;
|
||||
public DateTimeViewModel DeletionDateTimeViewModel { get; }
|
||||
public DateTimeViewModel ExpirationDateTimeViewModel { get; }
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string FileTypeAccessibilityLabel => IsFile ? AppResources.FileTypeIsSelected : AppResources.FileTypeIsNotSelected;
|
||||
public string TextTypeAccessibilityLabel => IsText ? AppResources.TextTypeIsSelected : AppResources.TextTypeIsNotSelected;
|
||||
public string OptionsShowHideIcon => ShowOptions ? BitwardenIcons.ChevronUp : BitwardenIcons.AngleDown;
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
@@ -255,10 +264,8 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
Send = await send.DecryptAsync();
|
||||
DeletionDate = Send.DeletionDate.ToLocalTime();
|
||||
DeletionTime = DeletionDate.TimeOfDay;
|
||||
ExpirationDate = Send.ExpirationDate?.ToLocalTime();
|
||||
ExpirationTime = ExpirationDate?.TimeOfDay;
|
||||
DeletionDateTimeViewModel.DateTime = Send.DeletionDate.ToLocalTime();
|
||||
ExpirationDateTimeViewModel.DateTime = Send.ExpirationDate?.ToLocalTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -267,8 +274,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
Type = Type.GetValueOrDefault(defaultType),
|
||||
};
|
||||
_deletionDate = DateTimeNow().AddDays(7);
|
||||
_deletionTime = DeletionDate.TimeOfDay;
|
||||
DeletionDateTimeViewModel.DateTime = DateTimeNow().AddDays(7);
|
||||
DeletionDateTypeSelectedIndex = 4;
|
||||
ExpirationDateTypeSelectedIndex = 0;
|
||||
}
|
||||
@@ -292,23 +298,22 @@ namespace Bit.App.Pages
|
||||
public void ClearExpirationDate()
|
||||
{
|
||||
_isOverridingPickers = true;
|
||||
ExpirationDate = null;
|
||||
ExpirationTime = null;
|
||||
ExpirationDateTimeViewModel.DateTime = null;
|
||||
_isOverridingPickers = false;
|
||||
}
|
||||
|
||||
private void UpdateSendData()
|
||||
{
|
||||
// filename
|
||||
if (Send.File != null && FileName != null)
|
||||
if (Send.File != null && _fileName != null)
|
||||
{
|
||||
Send.File.FileName = FileName;
|
||||
Send.File.FileName = _fileName;
|
||||
}
|
||||
|
||||
// deletion date
|
||||
if (ShowDeletionCustomPickers)
|
||||
{
|
||||
Send.DeletionDate = DeletionDate.Date.Add(DeletionTime).ToUniversalTime();
|
||||
Send.DeletionDate = DeletionDateTimeViewModel.DateTime.Value.ToUniversalTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -316,9 +321,9 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
// expiration date
|
||||
if (ShowExpirationCustomPickers && ExpirationDate.HasValue && ExpirationTime.HasValue)
|
||||
if (ShowExpirationCustomPickers && ExpirationDateTimeViewModel.DateTime.HasValue)
|
||||
{
|
||||
Send.ExpirationDate = ExpirationDate.Value.Date.Add(ExpirationTime.Value).ToUniversalTime();
|
||||
Send.ExpirationDate = ExpirationDateTimeViewModel.DateTime.Value.ToUniversalTime();
|
||||
}
|
||||
else if (_simpleExpirationDateTime.HasValue)
|
||||
{
|
||||
@@ -471,7 +476,7 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
if (Page is SendAddEditPage sendPage && sendPage.OnClose != null)
|
||||
if (Page is SendAddOnlyPage sendPage && sendPage.OnClose != null)
|
||||
{
|
||||
sendPage.OnClose();
|
||||
return;
|
||||
@@ -612,24 +617,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void ExpirationDateChanged()
|
||||
{
|
||||
if (!_isOverridingPickers && !ExpirationTime.HasValue)
|
||||
{
|
||||
// auto-set time to current time upon setting date
|
||||
ExpirationTime = DateTimeNow().TimeOfDay;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExpirationTimeChanged()
|
||||
{
|
||||
if (!_isOverridingPickers && !ExpirationDate.HasValue)
|
||||
{
|
||||
// auto-set date to current date upon setting time
|
||||
ExpirationDate = DateTime.Today;
|
||||
}
|
||||
}
|
||||
|
||||
private void MaxAccessCountChanged()
|
||||
{
|
||||
Send.MaxAccessCount = _maxAccessCount;
|
||||
@@ -653,5 +640,10 @@ namespace Bit.App.Pages
|
||||
DateTimeKind.Local
|
||||
);
|
||||
}
|
||||
|
||||
internal void TriggerSendTextPropertyChanged()
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(Send)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
183
src/App/Pages/Send/SendAddOnlyOptionsView.xaml
Normal file
183
src/App/Pages/Send/SendAddOnlyOptionsView.xaml
Normal file
@@ -0,0 +1,183 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<ContentView
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
|
||||
x:DataType="pages:SendAddEditPageViewModel"
|
||||
x:Class="Bit.App.Pages.SendAddOnlyOptionsView">
|
||||
<ContentView.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentView.Resources>
|
||||
<ContentView.Content>
|
||||
<StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,10,0,0">
|
||||
<Label
|
||||
Text="{u:I18n DeletionDate}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_deletionDateTypePicker"
|
||||
ItemsSource="{Binding DeletionTypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding DeletionDateTypeSelectedIndex}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
ItemDisplayBinding="{Binding Key}"
|
||||
ios:Picker.UpdateMode="WhenFinished"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n DeletionTime}" />
|
||||
<controls:LazyDateTimePicker
|
||||
x:Name="_lazyDeletionDateTimePicker"
|
||||
BindingContext="{Binding DeletionDateTimeViewModel}"
|
||||
IsVisible="{Binding ShowDeletionCustomPickers}"
|
||||
Margin="0,5,0,0" />
|
||||
<Label
|
||||
Text="{u:I18n DeletionDateInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row" Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n ExpirationDate}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_expirationDateTypePicker"
|
||||
ItemsSource="{Binding ExpirationTypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding ExpirationDateTypeSelectedIndex}"
|
||||
ItemDisplayBinding="{Binding Key}"
|
||||
ios:Picker.UpdateMode="WhenFinished"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ExpirationTime}" />
|
||||
<controls:LazyDateTimePicker
|
||||
x:Name="_lazyExpirationDateTimePicker"
|
||||
BindingContext="{Binding ExpirationDateTimeViewModel}"
|
||||
IsVisible="{Binding ShowExpirationCustomPickers}"
|
||||
Margin="0,5,0,0" />
|
||||
<Label
|
||||
Text="{u:I18n ExpirationDateInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n MaximumAccessCount}"
|
||||
StyleClass="box-label" />
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Orientation="Horizontal">
|
||||
<Entry
|
||||
Text="{Binding MaxAccessCount}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Keyboard="Numeric"
|
||||
MaxLength="9"
|
||||
TextChanged="OnMaxAccessCountTextChanged"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<controls:ExtendedStepper
|
||||
x:Name="_maxAccessCountStepper"
|
||||
Value="{Binding MaxAccessCount}"
|
||||
Maximum="999999999"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MaximumAccessCountInfo}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n NewPassword}"
|
||||
StyleClass="box-label" />
|
||||
<StackLayout Orientation="Horizontal">
|
||||
<Entry
|
||||
Text="{Binding NewPassword}"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<controls:IconButton
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Margin="10,0,0,0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n PasswordInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n Notes}"
|
||||
StyleClass="box-label" />
|
||||
<Editor
|
||||
x:Name="_notesEditor"
|
||||
AutoSize="TextChanges"
|
||||
Text="{Binding Send.Notes}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Margin="0,10,0,5"
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
|
||||
<Editor.Effects>
|
||||
<effects:ScrollEnabledEffect />
|
||||
</Editor.Effects>
|
||||
</Editor>
|
||||
<BoxView
|
||||
StyleClass="box-row-separator" />
|
||||
<Label
|
||||
Text="{u:I18n NotesInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n HideEmail}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Send.HideEmail}"
|
||||
IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n DisableSend}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Send.Disabled}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ContentView.Content>
|
||||
</ContentView>
|
||||
91
src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs
Normal file
91
src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Behaviors;
|
||||
using Xamarin.CommunityToolkit.UI.Views;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SendAddOnlyOptionsView : ContentView
|
||||
{
|
||||
public SendAddOnlyOptionsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private SendAddEditPageViewModel ViewModel => BindingContext as SendAddEditPageViewModel;
|
||||
|
||||
public void SetMainScrollView(ScrollView scrollView)
|
||||
{
|
||||
_notesEditor.Behaviors.Add(new EditorPreventAutoBottomScrollingOnFocusedBehavior { ParentScrollView = scrollView });
|
||||
}
|
||||
|
||||
private void OnMaxAccessCountTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (ViewModel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(e.NewTextValue))
|
||||
{
|
||||
ViewModel.MaxAccessCount = null;
|
||||
_maxAccessCountStepper.Value = 0;
|
||||
return;
|
||||
}
|
||||
// accept only digits
|
||||
if (!int.TryParse(e.NewTextValue, out int _))
|
||||
{
|
||||
((Entry)sender).Text = e.OldTextValue;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
base.OnPropertyChanged(propertyName);
|
||||
|
||||
if (propertyName == nameof(BindingContext)
|
||||
&&
|
||||
ViewModel != null)
|
||||
{
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (!_lazyDeletionDateTimePicker.IsLoaded
|
||||
&&
|
||||
e.PropertyName == nameof(SendAddEditPageViewModel.ShowDeletionCustomPickers)
|
||||
&&
|
||||
ViewModel.ShowDeletionCustomPickers)
|
||||
{
|
||||
_lazyDeletionDateTimePicker.LoadViewAsync();
|
||||
}
|
||||
|
||||
if (!_lazyExpirationDateTimePicker.IsLoaded
|
||||
&&
|
||||
e.PropertyName == nameof(SendAddEditPageViewModel.ShowExpirationCustomPickers)
|
||||
&&
|
||||
ViewModel.ShowExpirationCustomPickers)
|
||||
{
|
||||
_lazyExpirationDateTimePicker.LoadViewAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SendAddOnlyOptionsLazyView : LazyView<SendAddOnlyOptionsView>
|
||||
{
|
||||
public ScrollView MainScrollView { get; set; }
|
||||
|
||||
public override async ValueTask LoadViewAsync()
|
||||
{
|
||||
await base.LoadViewAsync();
|
||||
|
||||
if (Content is SendAddOnlyOptionsView optionsView)
|
||||
{
|
||||
optionsView.SetMainScrollView(MainScrollView);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
190
src/App/Pages/Send/SendAddOnlyPage.xaml
Normal file
190
src/App/Pages/Send/SendAddOnlyPage.xaml
Normal file
@@ -0,0 +1,190 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.SendAddOnlyPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
x:DataType="pages:SendAddEditPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SendAddEditPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<!--Order matters here or the avatar's image won't be updated correctly, check iOS CustomNavigationRenderer for more info-->
|
||||
<controls:ExtendedToolbarItem
|
||||
x:Name="_accountAvatar"
|
||||
IconImageSource="{Binding AvatarImageSource}"
|
||||
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
|
||||
Order="Primary"
|
||||
Priority="-2"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Account}" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/>
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<AbsoluteLayout>
|
||||
<ScrollView
|
||||
x:Name="_scrollView"
|
||||
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
|
||||
AbsoluteLayout.LayoutFlags="All">
|
||||
<StackLayout x:Name="_mainContainer" StyleClass="box">
|
||||
<Frame
|
||||
IsVisible="{Binding SendEnabled, Converter={StaticResource inverseBool}}"
|
||||
Padding="10"
|
||||
Margin="0, 12, 0, 0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="Accent">
|
||||
<Label
|
||||
Text="{u:I18n SendDisabledWarning}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<Frame
|
||||
IsVisible="{Binding SendOptionsPolicyInEffect}"
|
||||
Padding="10"
|
||||
Margin="0, 12, 0, 0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="Accent">
|
||||
<Label
|
||||
Text="{u:I18n SendOptionsPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n Name}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_nameEntry"
|
||||
Text="{Binding Send.Name}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value" />
|
||||
<Label
|
||||
Text="{u:I18n NameInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding IsFile}">
|
||||
<Label
|
||||
Text="{u:I18n TypeFile}"
|
||||
StyleClass="box-label" />
|
||||
<StackLayout
|
||||
StyleClass="box-row">
|
||||
<Label
|
||||
Text="{Binding FileName}"
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
<Label
|
||||
Margin="0, 5, 0, 0"
|
||||
Text="{u:I18n MaxFileSize}"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding IsText}">
|
||||
<Label
|
||||
Text="{u:I18n TypeText}"
|
||||
StyleClass="box-label" />
|
||||
<Editor
|
||||
x:Name="_textEditor"
|
||||
AutoSize="TextChanges"
|
||||
Text="{Binding Send.Text.Text}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Margin="{Binding EditorMargins}"
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
|
||||
<Editor.Behaviors>
|
||||
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
|
||||
</Editor.Behaviors>
|
||||
<Editor.Effects>
|
||||
<effects:ScrollEnabledEffect />
|
||||
</Editor.Effects>
|
||||
</Editor>
|
||||
<BoxView
|
||||
StyleClass="box-row-separator" />
|
||||
<Label
|
||||
Text="{u:I18n TypeTextInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,10" />
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
Margin="0,10,0,0">
|
||||
<Label
|
||||
Text="{u:I18n HideTextByDefault}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Send.Text.Hidden}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{Binding ShareOnSaveText}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding ShareOnSave}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Spacing="0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{Binding OptionsAccessilibityText}">
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="OptionsHeader_Tapped" />
|
||||
</StackLayout.GestureRecognizers>
|
||||
<Label
|
||||
Text="{u:I18n Options}"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
Margin="0,0,5,0"
|
||||
AutomationProperties.IsInAccessibleTree="False"/>
|
||||
<controls:IconLabel
|
||||
Text="{Binding OptionsShowHideIcon}"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
AutomationProperties.IsInAccessibleTree="False"/>
|
||||
</StackLayout>
|
||||
<pages:SendAddOnlyOptionsLazyView x:Name="_lazyOptionsView" IsVisible="{Binding ShowOptions}" />
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
<controls:AccountSwitchingOverlayView
|
||||
x:Name="_accountListOverlay"
|
||||
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
|
||||
AbsoluteLayout.LayoutFlags="All"
|
||||
LongPressAccountEnabled="False"
|
||||
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
|
||||
</AbsoluteLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
178
src/App/Pages/Send/SendAddOnlyPage.xaml.cs
Normal file
178
src/App/Pages/Send/SendAddOnlyPage.xaml.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a version of <see cref="SendAddEditPage"/> that is reduced for adding only and adapted
|
||||
/// for performance for iOS Share extension.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should NOT be used in Android.
|
||||
/// </remarks>
|
||||
public partial class SendAddOnlyPage : BaseContentPage
|
||||
{
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
private AppOptions _appOptions;
|
||||
private SendAddEditPageViewModel _vm;
|
||||
|
||||
public Action OnClose { get; set; }
|
||||
public Action AfterSubmit { get; set; }
|
||||
|
||||
public SendAddOnlyPage(
|
||||
AppOptions appOptions = null,
|
||||
string sendId = null,
|
||||
SendType? type = null)
|
||||
{
|
||||
if (appOptions?.IosExtension != true)
|
||||
{
|
||||
throw new InvalidOperationException(nameof(SendAddOnlyPage) + " is only prepared to be used in iOS share extension");
|
||||
}
|
||||
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SendAddEditPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.SendId = sendId;
|
||||
_vm.Type = appOptions?.CreateSend?.Item1 ?? type;
|
||||
|
||||
if (_vm.IsText)
|
||||
{
|
||||
_nameEntry.ReturnType = ReturnType.Next;
|
||||
_nameEntry.ReturnCommand = new Command(() => _textEditor.Focus());
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
if (!await AppHelpers.IsVaultTimeoutImmediateAsync())
|
||||
{
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
}
|
||||
if (await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _vm.InitAsync();
|
||||
|
||||
if (!await _vm.LoadAsync())
|
||||
{
|
||||
await CloseAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_accountAvatar?.OnAppearing();
|
||||
await Device.InvokeOnMainThreadAsync(async () => _vm.AvatarImageSource = await GetAvatarImageSourceAsync());
|
||||
|
||||
await HandleCreateRequest();
|
||||
if (string.IsNullOrWhiteSpace(_vm.Send?.Name))
|
||||
{
|
||||
RequestFocus(_nameEntry);
|
||||
}
|
||||
AdjustToolbar();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
await CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_accountAvatar?.OnDisappearing();
|
||||
}
|
||||
|
||||
private async Task CloseAsync()
|
||||
{
|
||||
if (OnClose is null)
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnClose();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Save_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var submitted = await _vm.SubmitAsync();
|
||||
if (submitted)
|
||||
{
|
||||
AfterSubmit?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void AdjustToolbar()
|
||||
{
|
||||
_saveItem.IsEnabled = _vm.SendEnabled;
|
||||
}
|
||||
|
||||
private Task HandleCreateRequest()
|
||||
{
|
||||
if (_appOptions?.CreateSend == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_vm.IsAddFromShare = true;
|
||||
_vm.CopyInsteadOfShareAfterSaving = _appOptions.CopyInsteadOfShareAfterSaving;
|
||||
|
||||
var name = _appOptions.CreateSend.Item2;
|
||||
_vm.Send.Name = name;
|
||||
|
||||
var type = _appOptions.CreateSend.Item1;
|
||||
if (type == SendType.File)
|
||||
{
|
||||
_vm.FileData = _appOptions.CreateSend.Item3;
|
||||
_vm.FileName = name;
|
||||
}
|
||||
else
|
||||
{
|
||||
var text = _appOptions.CreateSend.Item4;
|
||||
_vm.Send.Text.Text = text;
|
||||
_vm.TriggerSendTextPropertyChanged();
|
||||
}
|
||||
_appOptions.CreateSend = null;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void OptionsHeader_Tapped(object sender, EventArgs e)
|
||||
{
|
||||
_vm.ToggleOptionsCommand.Execute(null);
|
||||
|
||||
if (!_lazyOptionsView.IsLoaded)
|
||||
{
|
||||
_lazyOptionsView.MainScrollView = _scrollView;
|
||||
_lazyOptionsView.LoadViewAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<pages:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.SendGroupingsPage"
|
||||
@@ -138,7 +138,8 @@
|
||||
ItemTemplate="{StaticResource sendListItemDataTemplateSelector}"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform" />
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Send Groupings Page" />
|
||||
</RefreshView>
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -5,6 +5,7 @@ using Bit.App.Controls;
|
||||
using Bit.App.Models;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -68,21 +69,28 @@ namespace Bit.App.Pages
|
||||
|
||||
_broadcasterService.Subscribe(_pageName, async (message) =>
|
||||
{
|
||||
if (message.Command == "syncStarted")
|
||||
try
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
else if (message.Command == "syncCompleted" || message.Command == "sendUpdated")
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
else if (message.Command == "syncCompleted" || message.Command == "sendUpdated")
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
@@ -66,7 +66,8 @@
|
||||
VerticalOptions="FillAndExpand"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Sends Page">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:SendView">
|
||||
<controls:SendViewCell
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
Grid.Column="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
IsVisible="{Binding UseOTPVerification, Converter={StaticResource inverseBool}}"/>
|
||||
<Label
|
||||
Text="{u:I18n ConfirmYourIdentity}"
|
||||
|
||||
@@ -109,7 +109,11 @@ namespace Bit.App.Pages
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new string[] { nameof(ShowPasswordIcon) });
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText),
|
||||
});
|
||||
}
|
||||
|
||||
public bool UseOTPVerification
|
||||
@@ -139,6 +143,7 @@ namespace Bit.App.Pages
|
||||
public Command TogglePasswordCommand { get; }
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
VerticalOptions="FillAndExpand"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Folders Page">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:FolderView">
|
||||
<controls:ExtendedStackLayout
|
||||
|
||||
@@ -33,6 +33,23 @@
|
||||
StyleClass="box-footer-label"
|
||||
Text="{u:I18n ThemeDescription}" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box"
|
||||
IsVisible="{Binding ShowAutoDarkThemeOptions}">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
Text="{u:I18n DefaultDarkTheme}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_autoDarkThemePicker"
|
||||
ItemsSource="{Binding AutoDarkThemeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding AutoDarkThemeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
StyleClass="box-footer-label"
|
||||
Text="{u:I18n DefaultDarkThemeDescription}" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
@@ -66,31 +83,31 @@
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n DisableAutoTotpCopy}"
|
||||
Text="{u:I18n CopyTotpAutomatically}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding DisableAutoTotpCopy}"
|
||||
IsToggled="{Binding AutoTotpCopy}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n DisableAutoTotpCopyDescription}"
|
||||
Text="{u:I18n CopyTotpAutomaticallyDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n DisableWebsiteIcons}"
|
||||
Text="{u:I18n ShowWebsiteIcons}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding DisableFavicon}"
|
||||
IsToggled="{Binding Favicon}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n DisableWebsiteIconsDescription}"
|
||||
Text="{u:I18n ShowWebsiteIconsDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||
@@ -100,16 +117,16 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n DisableSavePrompt}"
|
||||
Text="{u:I18n AskToAddLogin}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding AutofillDisableSavePrompt}"
|
||||
IsToggled="{Binding AutofillSavePrompt}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n DisableSavePromptDescription}"
|
||||
Text="{u:I18n AskToAddLoginDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Bit.App.Pages
|
||||
_vm = BindingContext as OptionsPageViewModel;
|
||||
_vm.Page = this;
|
||||
_themePicker.ItemDisplayBinding = new Binding("Value");
|
||||
_autoDarkThemePicker.ItemDisplayBinding = new Binding("Value");
|
||||
_uriMatchPicker.ItemDisplayBinding = new Binding("Value");
|
||||
_clearClipboardPicker.ItemDisplayBinding = new Binding("Value");
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
@@ -29,6 +30,7 @@ namespace Bit.App.Pages
|
||||
else
|
||||
{
|
||||
_themePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_autoDarkThemePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_uriMatchPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_clearClipboardPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
|
||||
@@ -12,17 +12,17 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public class OptionsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly ITotpService _totpService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
|
||||
|
||||
private bool _autofillDisableSavePrompt;
|
||||
private bool _autofillSavePrompt;
|
||||
private string _autofillBlacklistedUris;
|
||||
private bool _disableFavicon;
|
||||
private bool _disableAutoTotpCopy;
|
||||
private bool _favicon;
|
||||
private bool _autoTotpCopy;
|
||||
private int _clearClipboardSelectedIndex;
|
||||
private int _themeSelectedIndex;
|
||||
private int _autoDarkThemeSelectedIndex;
|
||||
private int _uriMatchSelectedIndex;
|
||||
private bool _inited;
|
||||
private bool _updatingAutofill;
|
||||
@@ -30,7 +30,6 @@ namespace Bit.App.Pages
|
||||
|
||||
public OptionsPageViewModel()
|
||||
{
|
||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
|
||||
@@ -53,10 +52,16 @@ namespace Bit.App.Pages
|
||||
ThemeOptions = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(null, AppResources.ThemeDefault),
|
||||
new KeyValuePair<string, string>("light", AppResources.Light),
|
||||
new KeyValuePair<string, string>("dark", AppResources.Dark),
|
||||
new KeyValuePair<string, string>("black", AppResources.Black),
|
||||
new KeyValuePair<string, string>("nord", "Nord"),
|
||||
new KeyValuePair<string, string>(ThemeManager.Light, AppResources.Light),
|
||||
new KeyValuePair<string, string>(ThemeManager.Dark, AppResources.Dark),
|
||||
new KeyValuePair<string, string>(ThemeManager.Black, AppResources.Black),
|
||||
new KeyValuePair<string, string>(ThemeManager.Nord, AppResources.Nord),
|
||||
};
|
||||
AutoDarkThemeOptions = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(ThemeManager.Dark, AppResources.Dark),
|
||||
new KeyValuePair<string, string>(ThemeManager.Black, AppResources.Black),
|
||||
new KeyValuePair<string, string>(ThemeManager.Nord, AppResources.Nord),
|
||||
};
|
||||
UriMatchOptions = new List<KeyValuePair<UriMatchType?, string>>
|
||||
{
|
||||
@@ -71,6 +76,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public List<KeyValuePair<int?, string>> ClearClipboardOptions { get; set; }
|
||||
public List<KeyValuePair<string, string>> ThemeOptions { get; set; }
|
||||
public List<KeyValuePair<string, string>> AutoDarkThemeOptions { get; set; }
|
||||
public List<KeyValuePair<UriMatchType?, string>> UriMatchOptions { get; set; }
|
||||
|
||||
public int ClearClipboardSelectedIndex
|
||||
@@ -80,7 +86,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (SetProperty(ref _clearClipboardSelectedIndex, value))
|
||||
{
|
||||
var task = SaveClipboardChangedAsync();
|
||||
SaveClipboardChangedAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,9 +96,25 @@ namespace Bit.App.Pages
|
||||
get => _themeSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _themeSelectedIndex, value))
|
||||
if (SetProperty(ref _themeSelectedIndex, value,
|
||||
additionalPropertyNames: new[] { nameof(ShowAutoDarkThemeOptions) })
|
||||
)
|
||||
{
|
||||
var task = SaveThemeAsync();
|
||||
SaveThemeAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowAutoDarkThemeOptions => ThemeOptions[ThemeSelectedIndex].Key == null;
|
||||
|
||||
public int AutoDarkThemeSelectedIndex
|
||||
{
|
||||
get => _autoDarkThemeSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _autoDarkThemeSelectedIndex, value))
|
||||
{
|
||||
SaveThemeAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,43 +126,43 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (SetProperty(ref _uriMatchSelectedIndex, value))
|
||||
{
|
||||
var task = SaveDefaultUriAsync();
|
||||
SaveDefaultUriAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool DisableFavicon
|
||||
public bool Favicon
|
||||
{
|
||||
get => _disableFavicon;
|
||||
get => _favicon;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _disableFavicon, value))
|
||||
if (SetProperty(ref _favicon, value))
|
||||
{
|
||||
var task = UpdateDisableFaviconAsync();
|
||||
UpdateFaviconAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool DisableAutoTotpCopy
|
||||
public bool AutoTotpCopy
|
||||
{
|
||||
get => _disableAutoTotpCopy;
|
||||
get => _autoTotpCopy;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _disableAutoTotpCopy, value))
|
||||
if (SetProperty(ref _autoTotpCopy, value))
|
||||
{
|
||||
var task = UpdateAutoTotpCopyAsync();
|
||||
UpdateAutoTotpCopyAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutofillDisableSavePrompt
|
||||
public bool AutofillSavePrompt
|
||||
{
|
||||
get => _autofillDisableSavePrompt;
|
||||
get => _autofillSavePrompt;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _autofillDisableSavePrompt, value))
|
||||
if (SetProperty(ref _autofillSavePrompt, value))
|
||||
{
|
||||
var task = UpdateAutofillDisableSavePromptAsync();
|
||||
UpdateAutofillSavePromptAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,13 +181,15 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
AutofillDisableSavePrompt = (await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
|
||||
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
|
||||
var blacklistedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
AutofillBlacklistedUris = blacklistedUrisList != null ? string.Join(", ", blacklistedUrisList) : null;
|
||||
DisableAutoTotpCopy = !(await _totpService.IsAutoCopyEnabledAsync());
|
||||
DisableFavicon = (await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
||||
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
|
||||
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
||||
var theme = await _stateService.GetThemeAsync();
|
||||
ThemeSelectedIndex = ThemeOptions.FindIndex(k => k.Key == theme);
|
||||
var autoDarkTheme = await _stateService.GetAutoDarkThemeAsync() ?? "dark";
|
||||
AutoDarkThemeSelectedIndex = AutoDarkThemeOptions.FindIndex(k => k.Key == autoDarkTheme);
|
||||
var defaultUriMatch = await _stateService.GetDefaultUriMatchAsync();
|
||||
UriMatchSelectedIndex = defaultUriMatch == null ? 0 :
|
||||
UriMatchOptions.FindIndex(k => (int?)k.Key == defaultUriMatch);
|
||||
@@ -178,15 +202,17 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
await _stateService.SetDisableAutoTotpCopyAsync(DisableAutoTotpCopy);
|
||||
// TODO: [PS-961] Fix negative function names
|
||||
await _stateService.SetDisableAutoTotpCopyAsync(!AutoTotpCopy);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateDisableFaviconAsync()
|
||||
private async Task UpdateFaviconAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
await _stateService.SetDisableFaviconAsync(DisableFavicon);
|
||||
// TODO: [PS-961] Fix negative function names
|
||||
await _stateService.SetDisableFaviconAsync(!Favicon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,8 +228,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (_inited && ThemeSelectedIndex > -1)
|
||||
{
|
||||
var theme = ThemeOptions[ThemeSelectedIndex].Key;
|
||||
await _stateService.SetThemeAsync(theme);
|
||||
await _stateService.SetThemeAsync(ThemeOptions[ThemeSelectedIndex].Key);
|
||||
await _stateService.SetAutoDarkThemeAsync(AutoDarkThemeOptions[AutoDarkThemeSelectedIndex].Key);
|
||||
ThemeManager.SetTheme(Application.Current.Resources);
|
||||
_messagingService.Send("updatedTheme");
|
||||
}
|
||||
@@ -217,11 +243,12 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateAutofillDisableSavePromptAsync()
|
||||
private async Task UpdateAutofillSavePromptAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
await _stateService.SetAutofillDisableSavePromptAsync(AutofillDisableSavePrompt);
|
||||
// TODO: [PS-961] Fix negative function names
|
||||
await _stateService.SetAutofillDisableSavePromptAsync(!AutofillSavePrompt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform" />
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Settings Page" />
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -2,18 +2,13 @@
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Pages.Accounts;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SettingsPage : BaseContentPage
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly TabsPage _tabsPage;
|
||||
private SettingsPageViewModel _vm;
|
||||
|
||||
@@ -21,7 +16,6 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_tabsPage = tabsPage;
|
||||
InitializeComponent();
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_vm = BindingContext as SettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
@@ -67,118 +61,12 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
private void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
if (e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Name == AppResources.Sync)
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new SyncPage()));
|
||||
}
|
||||
else if (item.Name == AppResources.AutofillServices)
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(this)));
|
||||
}
|
||||
else if (item.Name == AppResources.PasswordAutofill)
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new AutofillPage()));
|
||||
}
|
||||
else if (item.Name == AppResources.AppExtension)
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new ExtensionPage()));
|
||||
}
|
||||
else if (item.Name == AppResources.Options)
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new OptionsPage()));
|
||||
}
|
||||
else if (item.Name == AppResources.Folders)
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new FoldersPage()));
|
||||
}
|
||||
else if (item.Name == AppResources.About)
|
||||
{
|
||||
await _vm.AboutAsync();
|
||||
}
|
||||
else if (item.Name == AppResources.HelpAndFeedback)
|
||||
{
|
||||
_vm.Help();
|
||||
}
|
||||
else if (item.Name == AppResources.FingerprintPhrase)
|
||||
{
|
||||
await _vm.FingerprintAsync();
|
||||
}
|
||||
else if (item.Name == AppResources.RateTheApp)
|
||||
{
|
||||
_vm.Rate();
|
||||
}
|
||||
else if (item.Name == AppResources.ImportItems)
|
||||
{
|
||||
_vm.Import();
|
||||
}
|
||||
else if (item.Name == AppResources.ExportVault)
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()));
|
||||
}
|
||||
else if (item.Name == AppResources.LearnOrg)
|
||||
{
|
||||
await _vm.ShareAsync();
|
||||
}
|
||||
else if (item.Name == AppResources.WebVault)
|
||||
{
|
||||
_vm.WebVault();
|
||||
}
|
||||
else if (item.Name == AppResources.ChangeMasterPassword)
|
||||
{
|
||||
await _vm.ChangePasswordAsync();
|
||||
}
|
||||
else if (item.Name == AppResources.TwoStepLogin)
|
||||
{
|
||||
await _vm.TwoStepAsync();
|
||||
}
|
||||
else if (item.Name == AppResources.LogOut)
|
||||
{
|
||||
await _vm.LogOutAsync();
|
||||
}
|
||||
else if (item.Name == AppResources.DeleteAccount)
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage()));
|
||||
}
|
||||
else if (item.Name == AppResources.LockNow)
|
||||
{
|
||||
await _vm.LockAsync();
|
||||
}
|
||||
else if (item.Name == AppResources.VaultTimeout)
|
||||
{
|
||||
await _vm.VaultTimeoutAsync();
|
||||
}
|
||||
else if (item.Name == AppResources.VaultTimeoutAction)
|
||||
{
|
||||
await _vm.VaultTimeoutActionAsync();
|
||||
}
|
||||
else if (item.Name == AppResources.UnlockWithPIN)
|
||||
{
|
||||
await _vm.UpdatePinAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
var biometricName = AppResources.Biometrics;
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
biometricName = supportsFace ? AppResources.FaceID : AppResources.TouchID;
|
||||
}
|
||||
if (item.Name == string.Format(AppResources.UnlockWith, biometricName))
|
||||
{
|
||||
await _vm.UpdateBiometricAsync();
|
||||
}
|
||||
_vm?.ExecuteSettingItemCommand.Execute(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
@@ -12,6 +13,8 @@ namespace Bit.App.Pages
|
||||
public string SubLabel { get; set; }
|
||||
public TimeSpan? Time { get; set; }
|
||||
public bool UseFrame { get; set; }
|
||||
public Func<Task> ExecuteAsync { get; set; }
|
||||
|
||||
public bool SubLabelTextEnabled => SubLabel == AppResources.Enabled;
|
||||
public string LineBreakMode => SubLabel == null ? "TailTruncation" : "";
|
||||
public bool ShowSubLabel => SubLabel.Length != 0;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Pages.Accounts;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
@@ -10,7 +11,6 @@ using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Forms;
|
||||
using ZXing.Client.Result;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -29,16 +29,19 @@ namespace Bit.App.Pages
|
||||
private readonly ILocalizeService _localizeService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ILogger _loggerService;
|
||||
|
||||
private const int CustomVaultTimeoutValue = -100;
|
||||
|
||||
private bool _supportsBiometric;
|
||||
private bool _pin;
|
||||
private bool _biometric;
|
||||
private bool _screenCaptureAllowed;
|
||||
private string _lastSyncDate;
|
||||
private string _vaultTimeoutDisplayValue;
|
||||
private string _vaultTimeoutActionDisplayValue;
|
||||
private bool _showChangeMasterPassword;
|
||||
private bool _reportLoggingEnabled;
|
||||
|
||||
private List<KeyValuePair<string, int?>> _vaultTimeouts =
|
||||
new List<KeyValuePair<string, int?>>
|
||||
@@ -79,13 +82,18 @@ namespace Bit.App.Pages
|
||||
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
|
||||
PageTitle = AppResources.Settings;
|
||||
|
||||
ExecuteSettingItemCommand = new AsyncCommand<SettingsPageListItem>(item => item.ExecuteAsync(), onException: _loggerService.Exception, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public ObservableRangeCollection<ISettingsPageListItem> GroupedItems { get; set; }
|
||||
|
||||
public IAsyncCommand<SettingsPageListItem> ExecuteSettingItemCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
|
||||
@@ -115,6 +123,7 @@ namespace Bit.App.Pages
|
||||
var pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
|
||||
_pin = pinSet.Item1 || pinSet.Item2;
|
||||
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
|
||||
_screenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
|
||||
|
||||
if (_vaultTimeoutDisplayValue == null)
|
||||
{
|
||||
@@ -123,7 +132,7 @@ namespace Bit.App.Pages
|
||||
|
||||
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
|
||||
!await _keyConnectorService.GetUsesKeyConnector();
|
||||
|
||||
_reportLoggingEnabled = await _loggerService.IsEnabled();
|
||||
BuildList();
|
||||
}
|
||||
|
||||
@@ -255,6 +264,17 @@ namespace Bit.App.Pages
|
||||
}
|
||||
var cleanSelection = selection.Replace("✓ ", string.Empty);
|
||||
var selectionOption = _vaultTimeouts.FirstOrDefault(o => o.Key == cleanSelection);
|
||||
|
||||
// Check if the selected Timeout action is "Never" and if it's different from the previous selected value
|
||||
if (selectionOption.Value == null && selectionOption.Value != oldTimeout)
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.NeverLockWarning,
|
||||
AppResources.Warning, AppResources.Yes, AppResources.Cancel);
|
||||
if (!confirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
_vaultTimeoutDisplayValue = selectionOption.Key;
|
||||
newTimeout = selectionOption.Value;
|
||||
}
|
||||
@@ -286,6 +306,26 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoggerReportingAsync()
|
||||
{
|
||||
var options = new[]
|
||||
{
|
||||
CreateSelectableOption(AppResources.Yes, _reportLoggingEnabled),
|
||||
CreateSelectableOption(AppResources.No, !_reportLoggingEnabled),
|
||||
};
|
||||
|
||||
var selection = await Page.DisplayActionSheet(AppResources.SubmitCrashLogsDescription, AppResources.Cancel, null, options);
|
||||
|
||||
if (selection == null || selection == AppResources.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _loggerService.SetEnabled(CompareSelection(selection, AppResources.Yes));
|
||||
_reportLoggingEnabled = await _loggerService.IsEnabled();
|
||||
BuildList();
|
||||
}
|
||||
|
||||
public async Task VaultTimeoutActionAsync()
|
||||
{
|
||||
var options = _vaultTimeoutActions.Select(o =>
|
||||
@@ -401,6 +441,8 @@ namespace Bit.App.Pages
|
||||
|
||||
public void BuildList()
|
||||
{
|
||||
//TODO: Refactor this once navigation is abstracted so that it doesn't depend on Page, e.g. Page.Navigation.PushModalAsync...
|
||||
|
||||
var doUpper = Device.RuntimePlatform != Device.Android;
|
||||
var autofillItems = new List<SettingsPageListItem>();
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
@@ -408,38 +450,69 @@ namespace Bit.App.Pages
|
||||
autofillItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.AutofillServices,
|
||||
SubLabel = _deviceActionService.AutofillServicesEnabled() ?
|
||||
AppResources.Enabled : AppResources.Disabled
|
||||
SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.Enabled : AppResources.Disabled,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage)))
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
autofillItems.Add(new SettingsPageListItem { Name = AppResources.PasswordAutofill });
|
||||
autofillItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.PasswordAutofill,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillPage()))
|
||||
});
|
||||
}
|
||||
autofillItems.Add(new SettingsPageListItem { Name = AppResources.AppExtension });
|
||||
autofillItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.AppExtension,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExtensionPage()))
|
||||
});
|
||||
}
|
||||
var manageItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem { Name = AppResources.Folders },
|
||||
new SettingsPageListItem { Name = AppResources.Sync, SubLabel = _lastSyncDate }
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.Folders,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new FoldersPage()))
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.Sync,
|
||||
SubLabel = _lastSyncDate,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new SyncPage()))
|
||||
}
|
||||
};
|
||||
var securityItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem { Name = AppResources.VaultTimeout, SubLabel = _vaultTimeoutDisplayValue },
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.VaultTimeout,
|
||||
SubLabel = _vaultTimeoutDisplayValue,
|
||||
ExecuteAsync = () => VaultTimeoutAsync() },
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.VaultTimeoutAction,
|
||||
SubLabel = _vaultTimeoutActionDisplayValue
|
||||
SubLabel = _vaultTimeoutActionDisplayValue,
|
||||
ExecuteAsync = () => VaultTimeoutActionAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.UnlockWithPIN,
|
||||
SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled
|
||||
SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled,
|
||||
ExecuteAsync = () => UpdatePinAsync()
|
||||
},
|
||||
new SettingsPageListItem { Name = AppResources.LockNow },
|
||||
new SettingsPageListItem { Name = AppResources.TwoStepLogin }
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.LockNow,
|
||||
ExecuteAsync = () => LockAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.TwoStepLogin,
|
||||
ExecuteAsync = () => TwoStepAsync()
|
||||
}
|
||||
};
|
||||
if (_supportsBiometric || _biometric)
|
||||
{
|
||||
@@ -452,7 +525,8 @@ namespace Bit.App.Pages
|
||||
var item = new SettingsPageListItem
|
||||
{
|
||||
Name = string.Format(AppResources.UnlockWith, biometricName),
|
||||
SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled
|
||||
SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled,
|
||||
ExecuteAsync = () => UpdateBiometricAsync()
|
||||
};
|
||||
securityItems.Insert(2, item);
|
||||
}
|
||||
@@ -475,32 +549,98 @@ namespace Bit.App.Pages
|
||||
UseFrame = true,
|
||||
});
|
||||
}
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
securityItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.AllowScreenCapture,
|
||||
SubLabel = _screenCaptureAllowed ? AppResources.Enabled : AppResources.Disabled,
|
||||
ExecuteAsync = () => SetScreenCaptureAllowedAsync()
|
||||
});
|
||||
}
|
||||
var accountItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem { Name = AppResources.FingerprintPhrase },
|
||||
new SettingsPageListItem { Name = AppResources.LogOut }
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.FingerprintPhrase,
|
||||
ExecuteAsync = () => FingerprintAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.LogOut,
|
||||
ExecuteAsync = () => LogOutAsync()
|
||||
}
|
||||
};
|
||||
if (_showChangeMasterPassword)
|
||||
{
|
||||
accountItems.Insert(0, new SettingsPageListItem { Name = AppResources.ChangeMasterPassword });
|
||||
accountItems.Insert(0, new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ChangeMasterPassword,
|
||||
ExecuteAsync = () => ChangePasswordAsync()
|
||||
});
|
||||
}
|
||||
var toolsItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem { Name = AppResources.ImportItems },
|
||||
new SettingsPageListItem { Name = AppResources.ExportVault }
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ImportItems,
|
||||
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Import())
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ExportVault,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()))
|
||||
}
|
||||
};
|
||||
if (IncludeLinksWithSubscriptionInfo())
|
||||
{
|
||||
toolsItems.Add(new SettingsPageListItem { Name = AppResources.LearnOrg });
|
||||
toolsItems.Add(new SettingsPageListItem { Name = AppResources.WebVault });
|
||||
toolsItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.LearnOrg,
|
||||
ExecuteAsync = () => ShareAsync()
|
||||
});
|
||||
toolsItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.WebVault,
|
||||
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => WebVault())
|
||||
});
|
||||
}
|
||||
|
||||
var otherItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem { Name = AppResources.Options },
|
||||
new SettingsPageListItem { Name = AppResources.About },
|
||||
new SettingsPageListItem { Name = AppResources.HelpAndFeedback },
|
||||
new SettingsPageListItem { Name = AppResources.RateTheApp },
|
||||
new SettingsPageListItem { Name = AppResources.DeleteAccount }
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.Options,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new OptionsPage()))
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.About,
|
||||
ExecuteAsync = () => AboutAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.HelpAndFeedback,
|
||||
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Help())
|
||||
},
|
||||
#if !FDROID
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.SubmitCrashLogs,
|
||||
SubLabel = _reportLoggingEnabled ? AppResources.Enabled : AppResources.Disabled,
|
||||
ExecuteAsync = () => LoggerReportingAsync()
|
||||
},
|
||||
#endif
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.RateTheApp,
|
||||
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Rate())
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.DeleteAccount,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage()))
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: improve this. Leaving this as is to reduce error possibility on the hotfix.
|
||||
@@ -576,5 +716,37 @@ namespace Bit.App.Pages
|
||||
{
|
||||
return _vaultTimeouts.FirstOrDefault(o => o.Key == key).Value;
|
||||
}
|
||||
|
||||
private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option;
|
||||
|
||||
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == $"✓ {compareTo}";
|
||||
|
||||
public async Task SetScreenCaptureAllowedAsync()
|
||||
{
|
||||
if (CoreHelpers.ForceScreenCaptureEnabled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!_screenCaptureAllowed
|
||||
&&
|
||||
!await Page.DisplayAlert(AppResources.AllowScreenCapture, AppResources.AreYouSureYouWantToEnableScreenCapture, AppResources.Yes, AppResources.No))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _stateService.SetScreenCaptureAllowedAsync(!_screenCaptureAllowed);
|
||||
_screenCaptureAllowed = !_screenCaptureAllowed;
|
||||
await _deviceActionService.SetScreenCaptureAllowedAsync();
|
||||
BuildList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_loggerService.Exception(ex);
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Bit.App.Effects;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Effects;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
@@ -10,8 +12,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public class TabsPage : TabbedPage
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
private NavigationPage _groupingsPage;
|
||||
private NavigationPage _sendGroupingsPage;
|
||||
@@ -19,6 +23,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public TabsPage(AppOptions appOptions = null, PreviousPageInfo previousPage = null)
|
||||
{
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
|
||||
@@ -78,12 +83,26 @@ namespace Bit.App.Pages
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
_broadcasterService.Subscribe(nameof(TabsPage), async (message) =>
|
||||
{
|
||||
if (message.Command == "syncCompleted")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () => await UpdateVaultButtonTitleAsync());
|
||||
}
|
||||
});
|
||||
await UpdateVaultButtonTitleAsync();
|
||||
if (await _keyConnectorService.UserNeedsMigration())
|
||||
{
|
||||
_messagingService.Send("convertAccountToKeyConnector");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_broadcasterService.Unsubscribe(nameof(TabsPage));
|
||||
}
|
||||
|
||||
public void ResetToVaultPage()
|
||||
{
|
||||
CurrentPage = _groupingsPage;
|
||||
@@ -131,5 +150,19 @@ namespace Bit.App.Pages
|
||||
groupingsPage.HideAccountSwitchingOverlayAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateVaultButtonTitleAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
var isShowingVaultFilter = await policyService.ShouldShowVaultFilterAsync();
|
||||
_groupingsPage.Title = isShowingVaultFilter ? AppResources.Vaults : AppResources.MyVault;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
@@ -183,31 +184,61 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n AuthenticatorKey}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<Frame
|
||||
IsVisible="{Binding HasTotpValue, Converter={StaticResource inverseBool}}"
|
||||
Margin="0,5,0,0"
|
||||
StyleClass="btn-icon-row"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand"
|
||||
Padding="0"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3">
|
||||
<Frame.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="ScanTotp_Clicked" />
|
||||
</Frame.GestureRecognizers>
|
||||
<controls:IconLabel
|
||||
Text="{Binding SetupTotpText}"
|
||||
Padding="0,15"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<controls:MonoEntry
|
||||
x:Name="_loginTotpEntry"
|
||||
Text="{Binding Cipher.Login.Totp}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsVisible="{Binding HasTotpValue}"
|
||||
IsPassword="{Binding Cipher.ViewPassword, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding Cipher.ViewPassword}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="{Binding TotpColumnSpan}" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
IsVisible="{Binding HasTotpValue}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Camera}}"
|
||||
Clicked="ScanTotp_Clicked"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding Cipher.ViewPassword}"
|
||||
IsVisible="{Binding HasTotpValue}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ScanQrTitle}" />
|
||||
</Grid>
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace Bit.App.Pages
|
||||
CipherType? type = null,
|
||||
string folderId = null,
|
||||
string collectionId = null,
|
||||
string organizationId = null,
|
||||
string name = null,
|
||||
string uri = null,
|
||||
bool fromAutofill = false,
|
||||
@@ -51,6 +52,7 @@ namespace Bit.App.Pages
|
||||
_vm.CipherId = cipherId;
|
||||
_vm.FolderId = folderId == "none" ? null : folderId;
|
||||
_vm.CollectionIds = collectionId != null ? new HashSet<string>(new List<string> { collectionId }) : null;
|
||||
_vm.OrganizationId = organizationId;
|
||||
_vm.Type = type;
|
||||
_vm.DefaultName = name ?? appOptions?.SaveName;
|
||||
_vm.DefaultUri = uri ?? appOptions?.Uri;
|
||||
|
||||
@@ -11,6 +11,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -29,6 +30,7 @@ namespace Bit.App.Pages
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
|
||||
private CipherView _cipher;
|
||||
private bool _showNotesSeparator;
|
||||
@@ -53,6 +55,7 @@ namespace Bit.App.Pages
|
||||
nameof(ShowUris),
|
||||
nameof(ShowAttachments),
|
||||
nameof(ShowCollections),
|
||||
nameof(HasTotpValue)
|
||||
};
|
||||
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
|
||||
new List<KeyValuePair<UriMatchType?, string>>
|
||||
@@ -80,6 +83,7 @@ namespace Bit.App.Pages
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
|
||||
GeneratePasswordCommand = new Command(GeneratePassword);
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@@ -89,6 +93,7 @@ namespace Bit.App.Pages
|
||||
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
|
||||
FieldOptionsCommand = new Command<AddEditPageFieldViewModel>(FieldOptions);
|
||||
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
|
||||
CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
Uris = new ExtendedObservableCollection<LoginUriView>();
|
||||
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
|
||||
Collections = new ExtendedObservableCollection<CollectionViewModel>();
|
||||
@@ -150,6 +155,7 @@ namespace Bit.App.Pages
|
||||
public Command UriOptionsCommand { get; set; }
|
||||
public Command FieldOptionsCommand { get; set; }
|
||||
public Command PasswordPromptHelpCommand { get; set; }
|
||||
public AsyncCommand CopyCommand { get; set; }
|
||||
public string CipherId { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
@@ -249,7 +255,8 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowPasswordIcon)
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText)
|
||||
});
|
||||
}
|
||||
public bool ShowCardNumber
|
||||
@@ -298,7 +305,9 @@ namespace Bit.App.Pages
|
||||
public int TotpColumnSpan => Cipher.ViewPassword ? 1 : 2;
|
||||
public bool AllowPersonal { get; set; }
|
||||
public bool PasswordPrompt => Cipher.Reprompt != CipherRepromptType.None;
|
||||
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public bool HasTotpValue => IsLogin && !string.IsNullOrEmpty(Cipher?.Login?.Totp);
|
||||
public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTotp}";
|
||||
public void Init()
|
||||
{
|
||||
PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem;
|
||||
@@ -856,6 +865,19 @@ namespace Bit.App.Pages
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CopyTotpClipboardAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(_cipher.Login.Totp);
|
||||
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.AuthenticatorKeyScanner));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AddEditPageFieldViewModel : ExtendedViewModel
|
||||
|
||||
@@ -85,7 +85,8 @@
|
||||
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform" />
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Autofill Ciphers Page" />
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -55,21 +56,28 @@ namespace Bit.App.Pages
|
||||
|
||||
_broadcasterService.Subscribe(nameof(AutofillCiphersPage), async (message) =>
|
||||
{
|
||||
if (message.Command == "syncStarted")
|
||||
try
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
Text=""
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Clicked="BackButton_Clicked"
|
||||
x:Name="_backButton" />
|
||||
x:Name="_backButton"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n TapToGoBack}"/>
|
||||
<controls:ExtendedSearchBar
|
||||
x:Name="_searchBar"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
@@ -47,6 +49,31 @@
|
||||
</ContentPage.Resources>
|
||||
|
||||
<StackLayout x:Name="_mainLayout" Spacing="0" Padding="0">
|
||||
<StackLayout
|
||||
IsVisible="{Binding ShowVaultFilter}"
|
||||
Orientation="Horizontal"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{Binding VaultFilterDescription}"
|
||||
LineBreakMode="TailTruncation"
|
||||
Margin="10,0"
|
||||
StyleClass="text-md, text-muted"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Filter}" />
|
||||
<controls:MiButton
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ViewCellMenu}}"
|
||||
StyleClass="list-row-button-text, list-row-button-platform"
|
||||
Command="{Binding VaultFilterCommand}"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Filter}" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:IconLabel IsVisible="{Binding ShowSearchDirection}"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Search}}"
|
||||
StyleClass="text-muted"
|
||||
@@ -66,7 +93,8 @@
|
||||
VerticalOptions="FillAndExpand"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Ciphers Page">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:CipherView">
|
||||
<controls:CipherViewCell
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace Bit.App.Pages
|
||||
private CiphersPageViewModel _vm;
|
||||
private bool _hasFocused;
|
||||
|
||||
public CiphersPage(Func<CipherView, bool> filter, string pageTitle = null, string autofillUrl = null, bool deleted = false)
|
||||
public CiphersPage(Func<CipherView, bool> filter, string pageTitle = null, string vaultFilterSelection = null,
|
||||
string autofillUrl = null, bool deleted = false)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as CiphersPageViewModel;
|
||||
@@ -33,6 +34,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_vm.PageTitle = AppResources.SearchVault;
|
||||
}
|
||||
_vm.VaultFilterDescription = vaultFilterSelection;
|
||||
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
|
||||
@@ -5,17 +5,17 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class CiphersPageViewModel : BaseViewModel
|
||||
public class CiphersPageViewModel : VaultFilterViewModel
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly ICipherService _cipherService;
|
||||
@@ -23,7 +23,10 @@ namespace Bit.App.Pages
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private CancellationTokenSource _searchCancellationTokenSource;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private bool _showNoData;
|
||||
private bool _showList;
|
||||
@@ -37,6 +40,9 @@ namespace Bit.App.Pages
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
Ciphers = new ExtendedObservableCollection<CipherView>();
|
||||
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
|
||||
@@ -48,6 +54,11 @@ namespace Bit.App.Pages
|
||||
public string AutofillUrl { get; set; }
|
||||
public bool Deleted { get; set; }
|
||||
|
||||
protected override ICipherService cipherService => _cipherService;
|
||||
protected override IPolicyService policyService => _policyService;
|
||||
protected override IOrganizationService organizationService => _organizationService;
|
||||
protected override ILogger logger => _logger;
|
||||
|
||||
public bool ShowNoData
|
||||
{
|
||||
get => _showNoData;
|
||||
@@ -76,11 +87,9 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await InitVaultFilterAsync(true);
|
||||
WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace((Page as CiphersPage).SearchBar.Text))
|
||||
{
|
||||
Search((Page as CiphersPage).SearchBar.Text, 200);
|
||||
}
|
||||
PerformSearchIfPopulated();
|
||||
}
|
||||
|
||||
public void Search(string searchText, int? timeout = null)
|
||||
@@ -107,8 +116,9 @@ namespace Bit.App.Pages
|
||||
}
|
||||
try
|
||||
{
|
||||
var vaultFilteredCiphers = await GetAllCiphersAsync();
|
||||
ciphers = await _searchService.SearchCiphersAsync(searchText,
|
||||
Filter ?? (c => c.IsDeleted == Deleted), null, cts.Token);
|
||||
Filter ?? (c => c.IsDeleted == Deleted), vaultFilteredCiphers, cts.Token);
|
||||
cts.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -192,6 +202,19 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformSearchIfPopulated()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace((Page as CiphersPage).SearchBar.Text))
|
||||
{
|
||||
Search((Page as CiphersPage).SearchBar.Text, 200);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnVaultFilterSelectedAsync()
|
||||
{
|
||||
PerformSearchIfPopulated();
|
||||
}
|
||||
|
||||
private async void CipherOptionsAsync(CipherView cipher)
|
||||
{
|
||||
if ((Page as BaseContentPage).DoOnce())
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
|
||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||
x:DataType="pages:GroupingsPageViewModel"
|
||||
Title="{Binding PageTitle}"
|
||||
x:Name="_page">
|
||||
@@ -52,6 +54,14 @@
|
||||
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="authenticatorTemplate"
|
||||
x:DataType="pages:GroupingsPageTOTPListItem">
|
||||
<controls:AuthenticatorViewCell
|
||||
Cipher="{Binding Cipher}"
|
||||
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}"
|
||||
TotpSec="{Binding TotpSec}"/>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="groupTemplate"
|
||||
x:DataType="pages:GroupingsPageListItem">
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
@@ -103,9 +113,57 @@
|
||||
<pages:GroupingsPageListItemSelector x:Key="listItemDataTemplateSelector"
|
||||
HeaderTemplate="{StaticResource headerTemplate}"
|
||||
CipherTemplate="{StaticResource cipherTemplate}"
|
||||
AuthenticatorTemplate="{StaticResource authenticatorTemplate}"
|
||||
GroupTemplate="{StaticResource groupTemplate}" />
|
||||
|
||||
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
|
||||
<StackLayout
|
||||
IsVisible="{Binding ShowVaultFilter}"
|
||||
Orientation="Horizontal"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{Binding VaultFilterDescription}"
|
||||
LineBreakMode="TailTruncation"
|
||||
Margin="10,0"
|
||||
StyleClass="text-md, text-muted"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Filter}" />
|
||||
<controls:MiButton
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ViewCellMenu}}"
|
||||
StyleClass="list-row-button-text, list-row-button-platform"
|
||||
Command="{Binding VaultFilterCommand}"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Filter}" />
|
||||
</StackLayout>
|
||||
|
||||
<StackLayout
|
||||
IsVisible="{Binding ShowTotpFilter}"
|
||||
Orientation="Horizontal"
|
||||
Margin="0,5,10,0">
|
||||
<Label
|
||||
Text="{u:I18n DisplayItemsContainingTOTP}"
|
||||
LineBreakMode="TailTruncation"
|
||||
Margin="10,0"
|
||||
StyleClass="text-md, text-muted"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding TotpFilterEnable}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End">
|
||||
<Switch.Behaviors>
|
||||
<xct:EventToCommandBehavior
|
||||
EventName="Toggled"
|
||||
Command="{Binding TotpFilterCommand}" />
|
||||
</Switch.Behaviors>
|
||||
</Switch>
|
||||
</StackLayout>
|
||||
|
||||
<StackLayout
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Padding="20, 0"
|
||||
@@ -130,7 +188,8 @@
|
||||
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform" />
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Groupings Page" />
|
||||
</RefreshView>
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -7,6 +7,7 @@ using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -27,8 +28,8 @@ namespace Bit.App.Pages
|
||||
private PreviousPageInfo _previousPage;
|
||||
|
||||
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
|
||||
string collectionId = null, string pageTitle = null, PreviousPageInfo previousPage = null,
|
||||
bool deleted = false)
|
||||
string collectionId = null, string pageTitle = null, string vaultFilterSelection = null,
|
||||
PreviousPageInfo previousPage = null, bool deleted = false)
|
||||
{
|
||||
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||
InitializeComponent();
|
||||
@@ -52,6 +53,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_vm.PageTitle = pageTitle;
|
||||
}
|
||||
if (vaultFilterSelection != null)
|
||||
{
|
||||
_vm.VaultFilterDescription = vaultFilterSelection;
|
||||
}
|
||||
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
@@ -91,21 +96,28 @@ namespace Bit.App.Pages
|
||||
|
||||
_broadcasterService.Subscribe(_pageName, async (message) =>
|
||||
{
|
||||
if (message.Command == "syncStarted")
|
||||
try
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -177,10 +189,11 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
protected override async void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
IsBusy = false;
|
||||
_vm.StopCiphersTotpTick();
|
||||
_broadcasterService.Unsubscribe(_pageName);
|
||||
_vm.DisableRefreshing();
|
||||
_accountAvatar?.OnDisappearing();
|
||||
@@ -193,6 +206,13 @@ namespace Bit.App.Pages
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageTOTPListItem totpItem)
|
||||
{
|
||||
await _vm.SelectCipherAsync(totpItem.Cipher);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
|
||||
{
|
||||
return;
|
||||
@@ -259,7 +279,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (!_vm.Deleted && DoOnce())
|
||||
{
|
||||
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId);
|
||||
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class GroupingsPageListGroup : List<GroupingsPageListItem>
|
||||
public class GroupingsPageListGroup : List<IGroupingsPageListItem>
|
||||
{
|
||||
public GroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false)
|
||||
: this(new List<GroupingsPageListItem>(), name, count, doUpper, first)
|
||||
: this(new List<IGroupingsPageListItem>(), name, count, doUpper, first)
|
||||
{ }
|
||||
|
||||
public GroupingsPageListGroup(List<GroupingsPageListItem> groupItems, string name, int count,
|
||||
public GroupingsPageListGroup(IEnumerable<IGroupingsPageListItem> groupItems, string name, int count,
|
||||
bool doUpper = true, bool first = false)
|
||||
{
|
||||
AddRange(groupItems);
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Bit.App.Pages
|
||||
public DataTemplate HeaderTemplate { get; set; }
|
||||
public DataTemplate CipherTemplate { get; set; }
|
||||
public DataTemplate GroupTemplate { get; set; }
|
||||
public DataTemplate AuthenticatorTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
|
||||
{
|
||||
@@ -15,10 +16,16 @@ namespace Bit.App.Pages
|
||||
return HeaderTemplate;
|
||||
}
|
||||
|
||||
if (item is GroupingsPageTOTPListItem)
|
||||
{
|
||||
return AuthenticatorTemplate;
|
||||
}
|
||||
|
||||
if (item is GroupingsPageListItem listItem)
|
||||
{
|
||||
return listItem.Cipher != null ? CipherTemplate : GroupTemplate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
123
src/App/Pages/Vault/GroupingsPage/GroupingsPageTOTPListItem.cs
Normal file
123
src/App/Pages/Vault/GroupingsPage/GroupingsPageTOTPListItem.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class GroupingsPageTOTPListItem : ExtendedViewModel, IGroupingsPageListItem
|
||||
{
|
||||
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
private readonly ITotpService _totpService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private CipherView _cipher;
|
||||
|
||||
private bool _websiteIconsEnabled;
|
||||
private string _iconImageSource = string.Empty;
|
||||
|
||||
public int interval { get; set; }
|
||||
private double _progress;
|
||||
private string _totpSec;
|
||||
private string _totpCodeFormatted;
|
||||
private TotpHelper _totpTickHelper;
|
||||
|
||||
|
||||
public GroupingsPageTOTPListItem(CipherView cipherView, bool websiteIconsEnabled)
|
||||
{
|
||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
|
||||
Cipher = cipherView;
|
||||
WebsiteIconsEnabled = websiteIconsEnabled;
|
||||
interval = _totpService.GetTimeInterval(Cipher.Login.Totp);
|
||||
CopyCommand = new AsyncCommand(CopyToClipboardAsync,
|
||||
onException: ex => _logger.Value.Exception(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
_totpTickHelper = new TotpHelper(cipherView);
|
||||
}
|
||||
|
||||
public AsyncCommand CopyCommand { get; set; }
|
||||
|
||||
public CipherView Cipher
|
||||
{
|
||||
get => _cipher;
|
||||
set => SetProperty(ref _cipher, value);
|
||||
}
|
||||
|
||||
public string TotpCodeFormatted
|
||||
{
|
||||
get => _totpCodeFormatted;
|
||||
set => SetProperty(ref _totpCodeFormatted, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(TotpCodeFormattedStart),
|
||||
nameof(TotpCodeFormattedEnd),
|
||||
});
|
||||
}
|
||||
|
||||
public string TotpSec
|
||||
{
|
||||
get => _totpSec;
|
||||
set => SetProperty(ref _totpSec, value);
|
||||
}
|
||||
public double Progress
|
||||
{
|
||||
get => _progress;
|
||||
set => SetProperty(ref _progress, value);
|
||||
}
|
||||
public bool WebsiteIconsEnabled
|
||||
{
|
||||
get => _websiteIconsEnabled;
|
||||
set => SetProperty(ref _websiteIconsEnabled, value);
|
||||
}
|
||||
|
||||
public bool ShowIconImage
|
||||
{
|
||||
get => WebsiteIconsEnabled
|
||||
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
|
||||
&& IconImageSource != null;
|
||||
}
|
||||
|
||||
public string IconImageSource
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_iconImageSource == string.Empty) // default value since icon source can return null
|
||||
{
|
||||
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
|
||||
}
|
||||
return _iconImageSource;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public string TotpCodeFormattedStart => TotpCodeFormatted?.Split(' ')[0];
|
||||
|
||||
public string TotpCodeFormattedEnd => TotpCodeFormatted?.Split(' ')[1];
|
||||
|
||||
public async Task CopyToClipboardAsync()
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(TotpCodeFormatted);
|
||||
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
|
||||
}
|
||||
|
||||
public async Task TotpTickAsync()
|
||||
{
|
||||
await _totpTickHelper.GenerateNewTotpValues();
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
TotpSec = _totpTickHelper.TotpSec;
|
||||
Progress = _totpTickHelper.Progress;
|
||||
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Resources;
|
||||
@@ -16,7 +18,7 @@ using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class GroupingsPageViewModel : BaseViewModel
|
||||
public class GroupingsPageViewModel : VaultFilterViewModel
|
||||
{
|
||||
private const int NoFolderListSize = 100;
|
||||
|
||||
@@ -29,13 +31,16 @@ namespace Bit.App.Pages
|
||||
private bool _showList;
|
||||
private bool _websiteIconsEnabled;
|
||||
private bool _syncRefreshing;
|
||||
private bool _showTotpFilter;
|
||||
private bool _totpFilterEnable;
|
||||
private string _noDataText;
|
||||
private List<CipherView> _allCiphers;
|
||||
private Dictionary<string, int> _folderCounts = new Dictionary<string, int>();
|
||||
private Dictionary<string, int> _collectionCounts = new Dictionary<string, int>();
|
||||
private Dictionary<CipherType, int> _typeCounts = new Dictionary<CipherType, int>();
|
||||
private int _deletedCount = 0;
|
||||
|
||||
private CancellationTokenSource _totpTickCts;
|
||||
private Task _totpTickTask;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly ICollectionService _collectionService;
|
||||
@@ -46,6 +51,8 @@ namespace Bit.App.Pages
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public GroupingsPageViewModel()
|
||||
@@ -60,10 +67,11 @@ namespace Bit.App.Pages
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
Loading = true;
|
||||
PageTitle = AppResources.MyVault;
|
||||
GroupedItems = new ObservableRangeCollection<IGroupingsPageListItem>();
|
||||
RefreshCommand = new Command(async () =>
|
||||
{
|
||||
@@ -71,6 +79,12 @@ namespace Bit.App.Pages
|
||||
await LoadAsync();
|
||||
});
|
||||
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
|
||||
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
|
||||
onException: ex => _logger.Exception(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
TotpFilterCommand = new AsyncCommand(LoadAsync,
|
||||
onException: ex => _logger.Exception(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||
{
|
||||
@@ -87,8 +101,12 @@ namespace Bit.App.Pages
|
||||
public bool HasCiphers { get; set; }
|
||||
public bool HasFolders { get; set; }
|
||||
public bool HasCollections { get; set; }
|
||||
public bool ShowNoFolderCiphers => (NoFolderCiphers?.Count ?? int.MaxValue) < NoFolderListSize &&
|
||||
(!Collections?.Any() ?? true);
|
||||
public string ShowTotpCodesAccessibilityText => TotpFilterEnable ?
|
||||
AppResources.AuthenticationCodesListIsVisibleActivateToShowCipherList
|
||||
: AppResources.CipherListIsVisibleActivateToShowAuthenticationCodesList;
|
||||
public bool ShowNoFolderCipherGroup => NoFolderCiphers != null
|
||||
&& NoFolderCiphers.Count < NoFolderListSize
|
||||
&& (Collections is null || !Collections.Any());
|
||||
public List<CipherView> Ciphers { get; set; }
|
||||
public List<CipherView> FavoriteCiphers { get; set; }
|
||||
public List<CipherView> NoFolderCiphers { get; set; }
|
||||
@@ -97,6 +115,11 @@ namespace Bit.App.Pages
|
||||
public List<Core.Models.View.CollectionView> Collections { get; set; }
|
||||
public List<TreeNode<Core.Models.View.CollectionView>> NestedCollections { get; set; }
|
||||
|
||||
protected override ICipherService cipherService => _cipherService;
|
||||
protected override IPolicyService policyService => _policyService;
|
||||
protected override IOrganizationService organizationService => _organizationService;
|
||||
protected override ILogger logger => _logger;
|
||||
|
||||
public bool Refreshing
|
||||
{
|
||||
get => _refreshing;
|
||||
@@ -142,12 +165,21 @@ namespace Bit.App.Pages
|
||||
get => _websiteIconsEnabled;
|
||||
set => SetProperty(ref _websiteIconsEnabled, value);
|
||||
}
|
||||
|
||||
public bool ShowTotpFilter
|
||||
{
|
||||
get => _showTotpFilter;
|
||||
set => SetProperty(ref _showTotpFilter, value);
|
||||
}
|
||||
public bool TotpFilterEnable
|
||||
{
|
||||
get => _totpFilterEnable;
|
||||
set => SetProperty(ref _totpFilterEnable, value);
|
||||
}
|
||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||
|
||||
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
|
||||
public Command RefreshCommand { get; set; }
|
||||
public Command<CipherView> CipherOptionsCommand { get; set; }
|
||||
public ICommand TotpFilterCommand { get; }
|
||||
public bool LoadedOnce { get; set; }
|
||||
|
||||
public async Task LoadAsync()
|
||||
@@ -172,12 +204,22 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget();
|
||||
|
||||
await InitVaultFilterAsync(MainPage);
|
||||
if (MainPage)
|
||||
{
|
||||
PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault;
|
||||
}
|
||||
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
|
||||
_doingLoad = true;
|
||||
LoadedOnce = true;
|
||||
ShowNoData = false;
|
||||
Loading = true;
|
||||
ShowList = false;
|
||||
ShowAddCipherButton = !Deleted;
|
||||
ShowTotpFilter = Type == CipherType.Login && canAccessPremium;
|
||||
|
||||
var groupedItems = new List<GroupingsPageListGroup>();
|
||||
var page = Page as GroupingsPage;
|
||||
|
||||
@@ -185,9 +227,9 @@ namespace Bit.App.Pages
|
||||
try
|
||||
{
|
||||
await LoadDataAsync();
|
||||
if (ShowNoFolderCiphers && (NestedFolders?.Any() ?? false))
|
||||
if (ShowNoFolderCipherGroup && (NestedFolders?.Any() ?? false))
|
||||
{
|
||||
// Remove "No Folder" from folder listing
|
||||
// Remove "No Folder" folder from folders group
|
||||
NestedFolders = NestedFolders.GetRange(0, NestedFolders.Count - 1);
|
||||
}
|
||||
|
||||
@@ -257,12 +299,9 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (Ciphers?.Any() ?? false)
|
||||
{
|
||||
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted)
|
||||
.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
|
||||
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
|
||||
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
|
||||
CreateCipherGroupedItems(groupedItems);
|
||||
}
|
||||
if (ShowNoFolderCiphers)
|
||||
if (ShowNoFolderCipherGroup)
|
||||
{
|
||||
var noFolderCiphersListItems = NoFolderCiphers.Select(
|
||||
c => new GroupingsPageListItem { Cipher = c }).ToList();
|
||||
@@ -348,12 +387,55 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateCipherGroupedItems(List<GroupingsPageListGroup> groupedItems)
|
||||
{
|
||||
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
|
||||
_totpTickCts?.Cancel();
|
||||
if (TotpFilterEnable)
|
||||
{
|
||||
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted && !string.IsNullOrEmpty(c.Login.Totp))
|
||||
.Select(c => new GroupingsPageTOTPListItem(c, true)).ToList();
|
||||
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
|
||||
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
|
||||
|
||||
StartCiphersTotpTick(ciphersListItems);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted)
|
||||
.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
|
||||
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
|
||||
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCiphersTotpTick(List<GroupingsPageTOTPListItem> ciphersListItems)
|
||||
{
|
||||
_totpTickCts?.Cancel();
|
||||
_totpTickCts = new CancellationTokenSource();
|
||||
_totpTickTask = new TimerTask(logger, () => ciphersListItems.ForEach(i => i.TotpTickAsync()), _totpTickCts).RunPeriodic();
|
||||
}
|
||||
|
||||
public async Task StopCiphersTotpTick()
|
||||
{
|
||||
_totpTickCts?.Cancel();
|
||||
if (_totpTickTask != null)
|
||||
{
|
||||
await _totpTickTask;
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableRefreshing()
|
||||
{
|
||||
Refreshing = false;
|
||||
SyncRefreshing = false;
|
||||
}
|
||||
|
||||
protected override async Task OnVaultFilterSelectedAsync()
|
||||
{
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
public async Task SelectCipherAsync(CipherView cipher)
|
||||
{
|
||||
var page = new ViewPage(cipher.Id);
|
||||
@@ -380,25 +462,26 @@ namespace Bit.App.Pages
|
||||
default:
|
||||
break;
|
||||
}
|
||||
var page = new GroupingsPage(false, type, null, null, title);
|
||||
var page = new GroupingsPage(false, type, null, null, title, _vaultFilterSelection);
|
||||
await Page.Navigation.PushAsync(page);
|
||||
}
|
||||
|
||||
public async Task SelectFolderAsync(FolderView folder)
|
||||
{
|
||||
var page = new GroupingsPage(false, null, folder.Id ?? "none", null, folder.Name);
|
||||
var page = new GroupingsPage(false, null, folder.Id ?? "none", null, folder.Name, _vaultFilterSelection);
|
||||
await Page.Navigation.PushAsync(page);
|
||||
}
|
||||
|
||||
public async Task SelectCollectionAsync(Core.Models.View.CollectionView collection)
|
||||
{
|
||||
var page = new GroupingsPage(false, null, null, collection.Id, collection.Name);
|
||||
var page = new GroupingsPage(false, null, null, collection.Id, collection.Name, _vaultFilterSelection);
|
||||
await Page.Navigation.PushAsync(page);
|
||||
}
|
||||
|
||||
public async Task SelectTrashAsync()
|
||||
{
|
||||
var page = new GroupingsPage(false, null, null, null, AppResources.Trash, null, true);
|
||||
var page = new GroupingsPage(false, null, null, null, AppResources.Trash, _vaultFilterSelection, null,
|
||||
true);
|
||||
await Page.Navigation.PushAsync(page);
|
||||
}
|
||||
|
||||
@@ -437,7 +520,7 @@ namespace Bit.App.Pages
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
NoDataText = AppResources.NoItems;
|
||||
_allCiphers = await _cipherService.GetAllDecryptedAsync();
|
||||
_allCiphers = await GetAllCiphersAsync();
|
||||
HasCiphers = _allCiphers.Any();
|
||||
FavoriteCiphers?.Clear();
|
||||
NoFolderCiphers?.Clear();
|
||||
@@ -451,12 +534,11 @@ namespace Bit.App.Pages
|
||||
|
||||
if (MainPage)
|
||||
{
|
||||
Folders = await _folderService.GetAllDecryptedAsync();
|
||||
NestedFolders = await _folderService.GetAllNestedAsync();
|
||||
await FillFoldersAndCollectionsAsync();
|
||||
NestedFolders = await _folderService.GetAllNestedAsync(Folders);
|
||||
HasFolders = NestedFolders.Any(f => f.Node?.Id != null);
|
||||
Collections = await _collectionService.GetAllDecryptedAsync();
|
||||
NestedCollections = await _collectionService.GetAllNestedAsync(Collections);
|
||||
HasCollections = NestedCollections.Any();
|
||||
NestedCollections = Collections != null ? await _collectionService.GetAllNestedAsync(Collections) : null;
|
||||
HasCollections = NestedCollections?.Any() ?? false;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -576,6 +658,34 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FillFoldersAndCollectionsAsync()
|
||||
{
|
||||
var orgId = GetVaultFilterOrgId();
|
||||
var decFolders = await _folderService.GetAllDecryptedAsync();
|
||||
var decCollections = await _collectionService.GetAllDecryptedAsync();
|
||||
if (IsVaultFilterMyVault)
|
||||
{
|
||||
Folders = BuildFolders(decFolders);
|
||||
Collections = null;
|
||||
}
|
||||
else if (IsVaultFilterOrgVault && !string.IsNullOrWhiteSpace(orgId))
|
||||
{
|
||||
Folders = BuildFolders(decFolders);
|
||||
Collections = decCollections?.Where(c => c.OrganizationId == orgId).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
Folders = decFolders;
|
||||
Collections = decCollections;
|
||||
}
|
||||
}
|
||||
|
||||
private List<FolderView> BuildFolders(List<FolderView> decFolders)
|
||||
{
|
||||
var folders = decFolders.Where(f => _allCiphers.Any(c => c.FolderId == f.Id)).ToList();
|
||||
return folders.Any() ? folders : null;
|
||||
}
|
||||
|
||||
private async void CipherOptionsAsync(CipherView cipher)
|
||||
{
|
||||
if ((Page as BaseContentPage).DoOnce())
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding History}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
StyleClass="list, list-platform">
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Password History Page">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:PasswordHistoryView">
|
||||
<Grid
|
||||
|
||||
@@ -41,15 +41,17 @@ namespace Bit.App.Pages
|
||||
{
|
||||
var cipher = await _cipherService.GetAsync(CipherId);
|
||||
var decCipher = await cipher.DecryptAsync();
|
||||
History.ResetWithRange(decCipher.PasswordHistory ?? new List<PasswordHistoryView>());
|
||||
ShowNoData = History.Count == 0;
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
History.ResetWithRange(decCipher.PasswordHistory ?? new List<PasswordHistoryView>());
|
||||
ShowNoData = History.Count == 0;
|
||||
});
|
||||
}
|
||||
|
||||
private async void CopyAsync(PasswordHistoryView ph)
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(ph.Password);
|
||||
_platformUtilsService.ShowToast("info", null,
|
||||
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
|
||||
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.ScanPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:forms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
|
||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||
xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms"
|
||||
x:Name="_page"
|
||||
Title="{u:I18n ScanQrTitle}">
|
||||
Title="{Binding ScanQrPageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:ScanPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
@@ -16,67 +29,114 @@
|
||||
<Grid
|
||||
VerticalOptions="FillAndExpand"
|
||||
HorizontalOptions="FillAndExpand">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<zxing:ZXingScannerView
|
||||
x:Name="_zxing"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand"
|
||||
AutomationId="zxingScannerView"
|
||||
OnScanResult="OnScanResult">
|
||||
</zxing:ZXingScannerView>
|
||||
|
||||
<Grid
|
||||
VerticalOptions="FillAndExpand"
|
||||
IsVisible="{Binding ShowScanner}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
OnScanResult="OnScanResult"/>
|
||||
<StackLayout
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="zxingDefaultOverlay">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
IsVisible="{Binding ShowScanner}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Margin="30,0">
|
||||
|
||||
<BoxView
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
VerticalOptions="Fill"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
BackgroundColor="Black"
|
||||
Opacity="0.7" />
|
||||
|
||||
<Label
|
||||
Text="{u:I18n CameraInstructionTop}"
|
||||
AutomationId="zxingDefaultOverlay_TopTextLabel"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
VerticalOptions="Center"
|
||||
<forms:SKCanvasView
|
||||
x:Name="SkCanvasView"
|
||||
Margin="0,50,0,0"
|
||||
WidthRequest="250"
|
||||
HeightRequest="250"
|
||||
IsVisible="{Binding ShowScanner}"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
TextColor="White" />
|
||||
PaintSurface="OnCanvasViewPaintSurface"/>
|
||||
|
||||
<BoxView
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
VerticalOptions="Fill"
|
||||
<controls:IconButton
|
||||
x:Name="_checkIcon"
|
||||
IsVisible="{Binding ShowScanner}"
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Start"
|
||||
FontSize="Title"
|
||||
TextColor="Transparent"/>
|
||||
</StackLayout>
|
||||
<BoxView
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding ShowScanner, Converter={StaticResource inverseBool}}"
|
||||
BackgroundColor="{DynamicResource BackgroundColor}"/>
|
||||
<StackLayout
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
BackgroundColor="Transparent" />
|
||||
|
||||
<BoxView
|
||||
IsVisible="{Binding ShowScanner, Converter={StaticResource inverseBool}}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Margin="30,0">
|
||||
<Label
|
||||
Text="{u:I18n EnterKeyManually}"
|
||||
FontSize="Title" />
|
||||
<Label
|
||||
Text="{u:I18n AuthenticatorKeyScanner}"
|
||||
StyleClass="box-label" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_authenticationKeyEntry"
|
||||
Text="{Binding TotpAuthenticationKey}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
StyleClass="box-value" />
|
||||
<Button
|
||||
Text="{u:I18n AddTotp}"
|
||||
StyleClass="box-button-row"
|
||||
Clicked="AddAuthenticationKey_OnClicked"/>
|
||||
</StackLayout>
|
||||
<BoxView
|
||||
Grid.Column="0"
|
||||
Grid.Row="2"
|
||||
VerticalOptions="Fill"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
BackgroundColor="Black"
|
||||
Opacity="0.7" />
|
||||
|
||||
<StackLayout
|
||||
VerticalOptions="Start"
|
||||
HorizontalOptions="Center"
|
||||
Grid.Column="0"
|
||||
Grid.Row="2">
|
||||
<Label
|
||||
Text="{u:I18n CameraInstructionBottom}"
|
||||
AutomationId="zxingDefaultOverlay_BottomTextLabel"
|
||||
Text="{Binding CameraInstructionTop}"
|
||||
AutomationId="zxingDefaultOverlay_TopTextLabel"
|
||||
Margin="30,15,30,0"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
StyleClass="text-sm"
|
||||
TextColor="White" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
FormattedText="{Binding ToggleScanModeLabel}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="2"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
TextColor="White" />
|
||||
</Grid>
|
||||
|
||||
Margin="0,15"
|
||||
StyleClass="text-sm"
|
||||
FontAttributes="Bold"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Center" >
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="ToggleScanMode_OnTapped" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
</Grid>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
</pages:BaseContentPage>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user