mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
12 Commits
feature/to
...
v2022.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d224f5e22 | ||
|
|
4831097c0b | ||
|
|
72f2983885 | ||
|
|
ee68dcf3b0 | ||
|
|
daab473cbb | ||
|
|
e6b99270cf | ||
|
|
a8d9aaa7fe | ||
|
|
542ef5f31a | ||
|
|
0b2fc2a647 | ||
|
|
a95fdf67a1 | ||
|
|
73890162bf | ||
|
|
ef14a8f850 |
64
.github/workflows/automatic-issue-responses.yml
vendored
64
.github/workflows/automatic-issue-responses.yml
vendored
@@ -1,64 +0,0 @@
|
||||
---
|
||||
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.
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -13,11 +13,6 @@ on:
|
||||
- Initial Release
|
||||
- Redeploy
|
||||
- Dry Run
|
||||
fdroid_publish:
|
||||
description: 'Publish to f-droid store'
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -83,7 +78,6 @@ 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
30
.github/workflows/stale-bot.yml
vendored
@@ -1,30 +0,0 @@
|
||||
---
|
||||
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,6 +19,12 @@ 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
|
||||
@@ -50,32 +56,16 @@ jobs:
|
||||
version: ${{ github.event.inputs.version_number }}
|
||||
file_path: "./src/iOS/Info.plist"
|
||||
|
||||
- name: Setup git
|
||||
- name: Commit files
|
||||
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,3 +1,40 @@
|
||||
# How to Contribute
|
||||
|
||||
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.
|
||||
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/
|
||||
|
||||
22
README.md
22
README.md
@@ -12,7 +12,16 @@ The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin
|
||||
|
||||
# Build/Run
|
||||
|
||||
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.
|
||||
**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.
|
||||
|
||||
# We're Hiring!
|
||||
|
||||
@@ -20,7 +29,8 @@ Interested in contributing in a big way? Consider joining our team! We're hiring
|
||||
|
||||
# Contribute
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -35,3 +45,11 @@ 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,7 +54,6 @@ 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"),
|
||||
@@ -68,7 +67,6 @@ 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"),
|
||||
|
||||
@@ -73,7 +73,6 @@ 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",
|
||||
@@ -87,7 +86,6 @@ 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,11 +64,10 @@ namespace Bit.Droid
|
||||
Intent?.Validate();
|
||||
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget(_ =>
|
||||
if (!CoreHelpers.InDebugMode())
|
||||
{
|
||||
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
|
||||
});
|
||||
}
|
||||
|
||||
ServiceContainer.Resolve<ILogger>("logger").InitAsync();
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ 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
|
||||
@@ -70,8 +69,7 @@ namespace Bit.Droid
|
||||
ServiceContainer.Resolve<IStorageService>("secureStorageService"),
|
||||
ServiceContainer.Resolve<IStateService>("stateService"),
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
||||
ServiceContainer.Resolve<IAuthService>("authService"),
|
||||
ServiceContainer.Resolve<ILogger>("logger"));
|
||||
ServiceContainer.Resolve<IAuthService>("authService"));
|
||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||
}
|
||||
#if !FDROID
|
||||
@@ -101,13 +99,12 @@ namespace Bit.Droid
|
||||
{
|
||||
ServiceContainer.Register<INativeLogService>("nativeLogService", new AndroidLogService());
|
||||
#if FDROID
|
||||
var logger = new StubLogger();
|
||||
ServiceContainer.Register<ILogger>("logger", new StubLogger());
|
||||
#elif DEBUG
|
||||
var logger = DebugLogger.Instance;
|
||||
ServiceContainer.Register<ILogger>("logger", DebugLogger.Instance);
|
||||
#else
|
||||
var logger = Logger.Instance;
|
||||
ServiceContainer.Register<ILogger>("logger", Logger.Instance);
|
||||
#endif
|
||||
ServiceContainer.Register("logger", logger);
|
||||
|
||||
// Note: This might cause a race condition. Investigate more.
|
||||
Task.Run(() =>
|
||||
@@ -127,7 +124,7 @@ 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(logger);
|
||||
var broadcasterService = new BroadcasterService();
|
||||
var messagingService = new MobileBroadcasterMessagingService(broadcasterService);
|
||||
var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo());
|
||||
var secureStorageService = new SecureStorageService();
|
||||
@@ -162,7 +159,6 @@ namespace Bit.Droid
|
||||
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.6.2" 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.1" 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"/>
|
||||
|
||||
|
||||
@@ -77,9 +77,6 @@
|
||||
<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"/>
|
||||
@@ -119,9 +116,6 @@
|
||||
<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"/>
|
||||
|
||||
@@ -28,25 +28,17 @@ namespace Bit.Droid.Services
|
||||
|
||||
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
|
||||
{
|
||||
try
|
||||
// Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+
|
||||
if ((int)Build.VERSION.SdkInt < 33)
|
||||
{
|
||||
// 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 Clipboard.SetTextAsync(text);
|
||||
}
|
||||
else
|
||||
{
|
||||
CopyToClipboard(text, isSensitive);
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
await ClearClipboardAlarmAsync(expiresInMs);
|
||||
}
|
||||
|
||||
public bool IsCopyNotificationHandledByPlatform()
|
||||
|
||||
@@ -948,21 +948,5 @@ 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +59,10 @@ namespace Bit.Droid.Utilities
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(theme) && osDarkModeEnabled)
|
||||
{
|
||||
theme = ThemeManager.Dark;
|
||||
theme = "dark";
|
||||
}
|
||||
|
||||
if (theme == ThemeManager.Dark || theme == ThemeManager.Black || theme == ThemeManager.Nord)
|
||||
if (theme == "dark" || theme == "black" || theme == "nord")
|
||||
{
|
||||
LightTheme = false;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,5 @@ namespace Bit.App.Abstractions
|
||||
{
|
||||
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
|
||||
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
|
||||
Task LogOutAsync(string userId, bool userInitiated, bool expired);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,5 @@ namespace Bit.App.Abstractions
|
||||
bool SupportsFido2();
|
||||
float GetSystemFontSizeScale();
|
||||
Task OnAccountSwitchCompleteAsync();
|
||||
Task SetScreenCaptureAllowedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,10 +129,12 @@
|
||||
<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>
|
||||
|
||||
@@ -160,6 +162,12 @@
|
||||
</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>
|
||||
@@ -414,6 +422,5 @@
|
||||
<None Remove="Xamarin.CommunityToolkit" />
|
||||
<None Remove="Controls\AccountSwitchingOverlay\" />
|
||||
<None Remove="Utilities\AccountManagement\" />
|
||||
<None Remove="Controls\DateTime\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -10,7 +10,6 @@ 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;
|
||||
@@ -57,93 +56,86 @@ namespace Bit.App
|
||||
Bootstrap();
|
||||
_broadcasterService.Subscribe(nameof(App), async (message) =>
|
||||
{
|
||||
try
|
||||
if (message.Command == "showDialog")
|
||||
{
|
||||
if (message.Command == "showDialog")
|
||||
var details = message.Data as DialogDetails;
|
||||
var confirmed = true;
|
||||
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
||||
AppResources.Ok : details.ConfirmText;
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
var details = message.Data as DialogDetails;
|
||||
var confirmed = true;
|
||||
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
||||
AppResources.Ok : details.ConfirmText;
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
||||
{
|
||||
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")
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ResumedAsync().FireAndForget();
|
||||
confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
||||
details.CancelText);
|
||||
}
|
||||
}
|
||||
else if (message.Command == "slept")
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
else
|
||||
{
|
||||
await SleptAsync();
|
||||
await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText);
|
||||
}
|
||||
}
|
||||
else if (message.Command == "migrated")
|
||||
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
||||
});
|
||||
}
|
||||
else if (message.Command == "resumed")
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
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()));
|
||||
});
|
||||
ResumedAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (message.Command == "slept")
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
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()));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -301,7 +293,7 @@ namespace Bit.App
|
||||
UpdateThemeAsync();
|
||||
};
|
||||
Current.MainPage = new NavigationPage(new HomePage(Options));
|
||||
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||
var mainPageTask = _accountsManager.NavigateOnAccountChangeAsync();
|
||||
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ namespace Bit.App.Controls
|
||||
public AccountViewCellViewModel(AccountView accountView)
|
||||
{
|
||||
AccountView = accountView;
|
||||
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
|
||||
?.GetOrCreateAvatar(AccountView.Name, AccountView.Email);
|
||||
AvatarImageSource = new AvatarImageSource(AccountView.Name, AccountView.Email);
|
||||
}
|
||||
|
||||
public AccountView AccountView
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,67 +0,0 @@
|
||||
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,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SkiaSharp;
|
||||
@@ -51,7 +50,7 @@ namespace Bit.App.Controls
|
||||
|
||||
private Stream Draw()
|
||||
{
|
||||
string chars;
|
||||
string chars = null;
|
||||
string upperData = null;
|
||||
|
||||
if (string.IsNullOrEmpty(_data))
|
||||
@@ -72,83 +71,62 @@ namespace Bit.App.Controls
|
||||
var textColor = Color.White;
|
||||
var size = 50;
|
||||
|
||||
using (var bitmap = new SKBitmap(size * 2,
|
||||
var bitmap = new SKBitmap(
|
||||
size * 2,
|
||||
size * 2,
|
||||
SKImageInfo.PlatformColorType,
|
||||
SKAlphaType.Premul))
|
||||
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
|
||||
{
|
||||
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;
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Fill,
|
||||
StrokeJoin = SKStrokeJoin.Miter,
|
||||
Color = SKColor.Parse(bgColor.ToHex())
|
||||
};
|
||||
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
||||
|
||||
using (var circlePaint = new SKPaint
|
||||
{
|
||||
IsAntialias = true,
|
||||
Style = SKPaintStyle.Fill,
|
||||
StrokeJoin = SKStrokeJoin.Miter,
|
||||
Color = SKColor.Parse(bgColor.ToHex())
|
||||
})
|
||||
{
|
||||
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100).AsStream();
|
||||
}
|
||||
|
||||
private string GetFirstLetters(string data, int charCount)
|
||||
{
|
||||
var sanitizedData = data.Trim();
|
||||
var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var parts = data.Split();
|
||||
if (parts.Length > 1 && charCount <= 2)
|
||||
{
|
||||
var text = string.Empty;
|
||||
for (var i = 0; i < charCount; i++)
|
||||
var text = "";
|
||||
for (int i = 0; i < charCount; i++)
|
||||
{
|
||||
text += parts[i][0];
|
||||
text += parts[i].Substring(0, 1);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
if (sanitizedData.Length > 2)
|
||||
if (data.Length > 2)
|
||||
{
|
||||
return sanitizedData.Substring(0, 2);
|
||||
return data.Substring(0, 2);
|
||||
}
|
||||
return sanitizedData;
|
||||
return data;
|
||||
}
|
||||
|
||||
private Color StringToColor(string str)
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,34 +0,0 @@
|
||||
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>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Command="{Binding SubmitCommand}" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Submit_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
|
||||
@@ -36,6 +36,14 @@ 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,17 +1,15 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class EnvironmentPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
public EnvironmentPageViewModel()
|
||||
{
|
||||
@@ -24,10 +22,10 @@ namespace Bit.App.Pages
|
||||
IdentityUrl = _environmentService.IdentityUrl;
|
||||
IconsUrl = _environmentService.IconsUrl;
|
||||
NotificationsUrls = _environmentService.NotificationsUrl;
|
||||
SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
}
|
||||
|
||||
public ICommand SubmitCommand { get; }
|
||||
public Command SubmitCommand { get; }
|
||||
public string BaseUrl { get; set; }
|
||||
public string ApiUrl { get; set; }
|
||||
public string IdentityUrl { get; set; }
|
||||
@@ -39,12 +37,6 @@ 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,
|
||||
@@ -65,25 +57,5 @@ 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), (message) =>
|
||||
_broadcasterService.Subscribe(nameof(HomePage), async (message) =>
|
||||
{
|
||||
if (message.Command == "updatedTheme")
|
||||
{
|
||||
|
||||
@@ -25,6 +25,8 @@ 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)
|
||||
{
|
||||
@@ -36,17 +38,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public Entry SecretEntry
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_vm?.PinLock ?? false)
|
||||
{
|
||||
return _pin;
|
||||
}
|
||||
return _masterPassword;
|
||||
}
|
||||
}
|
||||
public Entry MasterPasswordEntry { get; set; }
|
||||
public Entry PinEntry { get; set; }
|
||||
|
||||
public async Task PromptBiometricAfterResumeAsync()
|
||||
{
|
||||
@@ -77,12 +70,16 @@ namespace Bit.App.Pages
|
||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
||||
|
||||
await _vm.InitAsync();
|
||||
|
||||
_vm.FocusSecretEntry += PerformFocusSecretEntry;
|
||||
|
||||
if (!_vm.BiometricLock)
|
||||
{
|
||||
RequestFocus(SecretEntry);
|
||||
if (_vm.PinLock)
|
||||
{
|
||||
RequestFocus(PinEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
RequestFocus(MasterPasswordEntry);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -102,18 +99,6 @@ 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,7 +10,6 @@ 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
|
||||
@@ -28,7 +27,6 @@ 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;
|
||||
@@ -135,11 +133,6 @@ namespace Bit.App.Pages
|
||||
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()
|
||||
{
|
||||
@@ -353,8 +346,11 @@ namespace Bit.App.Pages
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
var secret = PinLock ? Pin : MasterPassword;
|
||||
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task PromptBiometricAsync()
|
||||
@@ -365,8 +361,18 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
PinLock ? AppResources.PIN : AppResources.MasterPassword,
|
||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
|
||||
PinLock ? AppResources.PIN : AppResources.MasterPassword, () =>
|
||||
{
|
||||
var page = Page as LockPage;
|
||||
if (PinLock)
|
||||
{
|
||||
page.PinEntry.Focus();
|
||||
}
|
||||
else
|
||||
{
|
||||
page.MasterPasswordEntry.Focus();
|
||||
}
|
||||
});
|
||||
await _stateService.SetBiometricLockedAsync(!success);
|
||||
if (success)
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
@@ -303,14 +303,14 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<controls:ExtendedDatePicker
|
||||
NullableDate="{Binding DeletionDateTimeViewModel.Date, Mode=TwoWay}"
|
||||
NullableDate="{Binding DeletionDate, Mode=TwoWay}"
|
||||
Format="d"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n DeletionDate}"
|
||||
Grid.Column="0" />
|
||||
<controls:ExtendedTimePicker
|
||||
NullableTime="{Binding DeletionDateTimeViewModel.Time, Mode=TwoWay}"
|
||||
NullableTime="{Binding DeletionTime, Mode=TwoWay}"
|
||||
Format="t"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
@@ -343,7 +343,7 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<controls:ExtendedDatePicker
|
||||
NullableDate="{Binding ExpirationDateTimeViewModel.Date, Mode=TwoWay}"
|
||||
NullableDate="{Binding ExpirationDate, Mode=TwoWay}"
|
||||
PlaceHolder="mm/dd/yyyy"
|
||||
Format="d"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
@@ -351,7 +351,7 @@
|
||||
AutomationProperties.Name="{u:I18n ExpirationDate}"
|
||||
Grid.Column="0" />
|
||||
<controls:ExtendedTimePicker
|
||||
NullableTime="{Binding ExpirationDateTimeViewModel.Time, Mode=TwoWay}"
|
||||
NullableTime="{Binding ExpirationTime, Mode=TwoWay}"
|
||||
PlaceHolder="--:-- --"
|
||||
Format="t"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace Bit.App.Pages
|
||||
private AppOptions _appOptions;
|
||||
private SendAddEditPageViewModel _vm;
|
||||
|
||||
public Action OnClose { get; set; }
|
||||
public Action AfterSubmit { get; set; }
|
||||
|
||||
public SendAddEditPage(
|
||||
@@ -135,7 +136,14 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task CloseAsync()
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
if (OnClose is null)
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnClose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnBackButtonPressed()
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
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;
|
||||
@@ -24,7 +23,7 @@ namespace Bit.App.Pages
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ISendService _sendService;
|
||||
private readonly ILogger _logger;
|
||||
private bool _sendEnabled = true;
|
||||
private bool _sendEnabled;
|
||||
private bool _canAccessPremium;
|
||||
private bool _emailVerified;
|
||||
private SendView _send;
|
||||
@@ -34,7 +33,11 @@ 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[]
|
||||
@@ -86,34 +89,8 @@ 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; }
|
||||
@@ -149,14 +126,23 @@ 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,
|
||||
additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(OptionsAccessilibityText),
|
||||
nameof(OptionsShowHideIcon)
|
||||
nameof(OptionsAccessilibityText)
|
||||
});
|
||||
}
|
||||
public int ExpirationDateTypeSelectedIndex
|
||||
@@ -170,7 +156,28 @@ 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;
|
||||
@@ -198,7 +205,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
public string FileName
|
||||
{
|
||||
get => _fileName ?? AppResources.NoFileChosen;
|
||||
get => _fileName;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _fileName, value))
|
||||
@@ -233,13 +240,10 @@ 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()
|
||||
{
|
||||
@@ -264,8 +268,10 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
Send = await send.DecryptAsync();
|
||||
DeletionDateTimeViewModel.DateTime = Send.DeletionDate.ToLocalTime();
|
||||
ExpirationDateTimeViewModel.DateTime = Send.ExpirationDate?.ToLocalTime();
|
||||
DeletionDate = Send.DeletionDate.ToLocalTime();
|
||||
DeletionTime = DeletionDate.TimeOfDay;
|
||||
ExpirationDate = Send.ExpirationDate?.ToLocalTime();
|
||||
ExpirationTime = ExpirationDate?.TimeOfDay;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -274,7 +280,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
Type = Type.GetValueOrDefault(defaultType),
|
||||
};
|
||||
DeletionDateTimeViewModel.DateTime = DateTimeNow().AddDays(7);
|
||||
_deletionDate = DateTimeNow().AddDays(7);
|
||||
_deletionTime = DeletionDate.TimeOfDay;
|
||||
DeletionDateTypeSelectedIndex = 4;
|
||||
ExpirationDateTypeSelectedIndex = 0;
|
||||
}
|
||||
@@ -298,22 +305,23 @@ namespace Bit.App.Pages
|
||||
public void ClearExpirationDate()
|
||||
{
|
||||
_isOverridingPickers = true;
|
||||
ExpirationDateTimeViewModel.DateTime = null;
|
||||
ExpirationDate = null;
|
||||
ExpirationTime = 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 = DeletionDateTimeViewModel.DateTime.Value.ToUniversalTime();
|
||||
Send.DeletionDate = DeletionDate.Date.Add(DeletionTime).ToUniversalTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -321,9 +329,9 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
// expiration date
|
||||
if (ShowExpirationCustomPickers && ExpirationDateTimeViewModel.DateTime.HasValue)
|
||||
if (ShowExpirationCustomPickers && ExpirationDate.HasValue && ExpirationTime.HasValue)
|
||||
{
|
||||
Send.ExpirationDate = ExpirationDateTimeViewModel.DateTime.Value.ToUniversalTime();
|
||||
Send.ExpirationDate = ExpirationDate.Value.Date.Add(ExpirationTime.Value).ToUniversalTime();
|
||||
}
|
||||
else if (_simpleExpirationDateTime.HasValue)
|
||||
{
|
||||
@@ -476,7 +484,7 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
if (Page is SendAddOnlyPage sendPage && sendPage.OnClose != null)
|
||||
if (Page is SendAddEditPage sendPage && sendPage.OnClose != null)
|
||||
{
|
||||
sendPage.OnClose();
|
||||
return;
|
||||
@@ -617,6 +625,24 @@ 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;
|
||||
@@ -640,10 +666,5 @@ namespace Bit.App.Pages
|
||||
DateTimeKind.Local
|
||||
);
|
||||
}
|
||||
|
||||
internal void TriggerSendTextPropertyChanged()
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(Send)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<ContentView
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns: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>
|
||||
@@ -1,91 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.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>
|
||||
@@ -1,178 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
|
||||
@@ -69,28 +68,21 @@ namespace Bit.App.Pages
|
||||
|
||||
_broadcasterService.Subscribe(_pageName, async (message) =>
|
||||
{
|
||||
try
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
else if (message.Command == "syncCompleted" || message.Command == "sendUpdated")
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (message.Command == "syncCompleted" || message.Command == "sendUpdated")
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -33,23 +33,6 @@
|
||||
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
|
||||
@@ -83,31 +66,31 @@
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n CopyTotpAutomatically}"
|
||||
Text="{u:I18n DisableAutoTotpCopy}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding AutoTotpCopy}"
|
||||
IsToggled="{Binding DisableAutoTotpCopy}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n CopyTotpAutomaticallyDescription}"
|
||||
Text="{u:I18n DisableAutoTotpCopyDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n ShowWebsiteIcons}"
|
||||
Text="{u:I18n DisableWebsiteIcons}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Favicon}"
|
||||
IsToggled="{Binding DisableFavicon}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n ShowWebsiteIconsDescription}"
|
||||
Text="{u:I18n DisableWebsiteIconsDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||
@@ -117,16 +100,16 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n AskToAddLogin}"
|
||||
Text="{u:I18n DisableSavePrompt}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding AutofillSavePrompt}"
|
||||
IsToggled="{Binding AutofillDisableSavePrompt}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n AskToAddLoginDescription}"
|
||||
Text="{u:I18n DisableSavePromptDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||
|
||||
@@ -19,7 +19,6 @@ 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)
|
||||
@@ -30,7 +29,6 @@ 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 _autofillSavePrompt;
|
||||
private bool _autofillDisableSavePrompt;
|
||||
private string _autofillBlacklistedUris;
|
||||
private bool _favicon;
|
||||
private bool _autoTotpCopy;
|
||||
private bool _disableFavicon;
|
||||
private bool _disableAutoTotpCopy;
|
||||
private int _clearClipboardSelectedIndex;
|
||||
private int _themeSelectedIndex;
|
||||
private int _autoDarkThemeSelectedIndex;
|
||||
private int _uriMatchSelectedIndex;
|
||||
private bool _inited;
|
||||
private bool _updatingAutofill;
|
||||
@@ -30,6 +30,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public OptionsPageViewModel()
|
||||
{
|
||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
|
||||
@@ -52,16 +53,10 @@ namespace Bit.App.Pages
|
||||
ThemeOptions = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(null, AppResources.ThemeDefault),
|
||||
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),
|
||||
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"),
|
||||
};
|
||||
UriMatchOptions = new List<KeyValuePair<UriMatchType?, string>>
|
||||
{
|
||||
@@ -76,7 +71,6 @@ 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
|
||||
@@ -86,7 +80,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (SetProperty(ref _clearClipboardSelectedIndex, value))
|
||||
{
|
||||
SaveClipboardChangedAsync().FireAndForget();
|
||||
var task = SaveClipboardChangedAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,25 +90,9 @@ namespace Bit.App.Pages
|
||||
get => _themeSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _themeSelectedIndex, value,
|
||||
additionalPropertyNames: new[] { nameof(ShowAutoDarkThemeOptions) })
|
||||
)
|
||||
if (SetProperty(ref _themeSelectedIndex, value))
|
||||
{
|
||||
SaveThemeAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowAutoDarkThemeOptions => ThemeOptions[ThemeSelectedIndex].Key == null;
|
||||
|
||||
public int AutoDarkThemeSelectedIndex
|
||||
{
|
||||
get => _autoDarkThemeSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _autoDarkThemeSelectedIndex, value))
|
||||
{
|
||||
SaveThemeAsync().FireAndForget();
|
||||
var task = SaveThemeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,43 +104,43 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (SetProperty(ref _uriMatchSelectedIndex, value))
|
||||
{
|
||||
SaveDefaultUriAsync().FireAndForget();
|
||||
var task = SaveDefaultUriAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Favicon
|
||||
public bool DisableFavicon
|
||||
{
|
||||
get => _favicon;
|
||||
get => _disableFavicon;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _favicon, value))
|
||||
if (SetProperty(ref _disableFavicon, value))
|
||||
{
|
||||
UpdateFaviconAsync().FireAndForget();
|
||||
var task = UpdateDisableFaviconAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutoTotpCopy
|
||||
public bool DisableAutoTotpCopy
|
||||
{
|
||||
get => _autoTotpCopy;
|
||||
get => _disableAutoTotpCopy;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _autoTotpCopy, value))
|
||||
if (SetProperty(ref _disableAutoTotpCopy, value))
|
||||
{
|
||||
UpdateAutoTotpCopyAsync().FireAndForget();
|
||||
var task = UpdateAutoTotpCopyAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutofillSavePrompt
|
||||
public bool AutofillDisableSavePrompt
|
||||
{
|
||||
get => _autofillSavePrompt;
|
||||
get => _autofillDisableSavePrompt;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _autofillSavePrompt, value))
|
||||
if (SetProperty(ref _autofillDisableSavePrompt, value))
|
||||
{
|
||||
UpdateAutofillSavePromptAsync().FireAndForget();
|
||||
var task = UpdateAutofillDisableSavePromptAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,15 +159,13 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
|
||||
AutofillDisableSavePrompt = (await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
|
||||
var blacklistedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
AutofillBlacklistedUris = blacklistedUrisList != null ? string.Join(", ", blacklistedUrisList) : null;
|
||||
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
|
||||
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
||||
DisableAutoTotpCopy = !(await _totpService.IsAutoCopyEnabledAsync());
|
||||
DisableFavicon = (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);
|
||||
@@ -202,17 +178,15 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
// TODO: [PS-961] Fix negative function names
|
||||
await _stateService.SetDisableAutoTotpCopyAsync(!AutoTotpCopy);
|
||||
await _stateService.SetDisableAutoTotpCopyAsync(DisableAutoTotpCopy);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateFaviconAsync()
|
||||
private async Task UpdateDisableFaviconAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
// TODO: [PS-961] Fix negative function names
|
||||
await _stateService.SetDisableFaviconAsync(!Favicon);
|
||||
await _stateService.SetDisableFaviconAsync(DisableFavicon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,8 +202,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (_inited && ThemeSelectedIndex > -1)
|
||||
{
|
||||
await _stateService.SetThemeAsync(ThemeOptions[ThemeSelectedIndex].Key);
|
||||
await _stateService.SetAutoDarkThemeAsync(AutoDarkThemeOptions[AutoDarkThemeSelectedIndex].Key);
|
||||
var theme = ThemeOptions[ThemeSelectedIndex].Key;
|
||||
await _stateService.SetThemeAsync(theme);
|
||||
ThemeManager.SetTheme(Application.Current.Resources);
|
||||
_messagingService.Send("updatedTheme");
|
||||
}
|
||||
@@ -243,12 +217,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateAutofillSavePromptAsync()
|
||||
private async Task UpdateAutofillDisableSavePromptAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
// TODO: [PS-961] Fix negative function names
|
||||
await _stateService.SetAutofillDisableSavePromptAsync(!AutofillSavePrompt);
|
||||
await _stateService.SetAutofillDisableSavePromptAsync(AutofillDisableSavePrompt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
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;
|
||||
|
||||
@@ -16,6 +21,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_tabsPage = tabsPage;
|
||||
InitializeComponent();
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_vm = BindingContext as SettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
@@ -61,12 +67,122 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item)
|
||||
if (!DoOnce())
|
||||
{
|
||||
_vm?.ExecuteSettingItemCommand.Execute(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 if (item.Name == AppResources.SubmitCrashLogs)
|
||||
{
|
||||
await _vm.LoggerReportingAsync();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
@@ -13,8 +12,6 @@ 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,11 +3,11 @@ 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;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Forms;
|
||||
@@ -30,13 +30,11 @@ namespace Bit.App.Pages
|
||||
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;
|
||||
@@ -86,14 +84,10 @@ namespace Bit.App.Pages
|
||||
|
||||
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();
|
||||
@@ -123,7 +117,6 @@ 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)
|
||||
{
|
||||
@@ -264,17 +257,6 @@ 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;
|
||||
}
|
||||
@@ -441,8 +423,6 @@ 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)
|
||||
@@ -450,69 +430,38 @@ namespace Bit.App.Pages
|
||||
autofillItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.AutofillServices,
|
||||
SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.Enabled : AppResources.Disabled,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage)))
|
||||
SubLabel = _deviceActionService.AutofillServicesEnabled() ?
|
||||
AppResources.Enabled : AppResources.Disabled
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
autofillItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.PasswordAutofill,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillPage()))
|
||||
});
|
||||
autofillItems.Add(new SettingsPageListItem { Name = AppResources.PasswordAutofill });
|
||||
}
|
||||
autofillItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.AppExtension,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExtensionPage()))
|
||||
});
|
||||
autofillItems.Add(new SettingsPageListItem { Name = AppResources.AppExtension });
|
||||
}
|
||||
var manageItems = new List<SettingsPageListItem>
|
||||
{
|
||||
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()))
|
||||
}
|
||||
new SettingsPageListItem { Name = AppResources.Folders },
|
||||
new SettingsPageListItem { Name = AppResources.Sync, SubLabel = _lastSyncDate }
|
||||
};
|
||||
var securityItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.VaultTimeout,
|
||||
SubLabel = _vaultTimeoutDisplayValue,
|
||||
ExecuteAsync = () => VaultTimeoutAsync() },
|
||||
new SettingsPageListItem { Name = AppResources.VaultTimeout, SubLabel = _vaultTimeoutDisplayValue },
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.VaultTimeoutAction,
|
||||
SubLabel = _vaultTimeoutActionDisplayValue,
|
||||
ExecuteAsync = () => VaultTimeoutActionAsync()
|
||||
SubLabel = _vaultTimeoutActionDisplayValue
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.UnlockWithPIN,
|
||||
SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled,
|
||||
ExecuteAsync = () => UpdatePinAsync()
|
||||
SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.LockNow,
|
||||
ExecuteAsync = () => LockAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.TwoStepLogin,
|
||||
ExecuteAsync = () => TwoStepAsync()
|
||||
}
|
||||
new SettingsPageListItem { Name = AppResources.LockNow },
|
||||
new SettingsPageListItem { Name = AppResources.TwoStepLogin }
|
||||
};
|
||||
if (_supportsBiometric || _biometric)
|
||||
{
|
||||
@@ -525,8 +474,7 @@ namespace Bit.App.Pages
|
||||
var item = new SettingsPageListItem
|
||||
{
|
||||
Name = string.Format(AppResources.UnlockWith, biometricName),
|
||||
SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled,
|
||||
ExecuteAsync = () => UpdateBiometricAsync()
|
||||
SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled
|
||||
};
|
||||
securityItems.Insert(2, item);
|
||||
}
|
||||
@@ -549,98 +497,40 @@ 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,
|
||||
ExecuteAsync = () => FingerprintAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.LogOut,
|
||||
ExecuteAsync = () => LogOutAsync()
|
||||
}
|
||||
new SettingsPageListItem { Name = AppResources.FingerprintPhrase },
|
||||
new SettingsPageListItem { Name = AppResources.LogOut }
|
||||
};
|
||||
if (_showChangeMasterPassword)
|
||||
{
|
||||
accountItems.Insert(0, new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ChangeMasterPassword,
|
||||
ExecuteAsync = () => ChangePasswordAsync()
|
||||
});
|
||||
accountItems.Insert(0, new SettingsPageListItem { Name = AppResources.ChangeMasterPassword });
|
||||
}
|
||||
var toolsItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ImportItems,
|
||||
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Import())
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ExportVault,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()))
|
||||
}
|
||||
new SettingsPageListItem { Name = AppResources.ImportItems },
|
||||
new SettingsPageListItem { Name = AppResources.ExportVault }
|
||||
};
|
||||
if (IncludeLinksWithSubscriptionInfo())
|
||||
{
|
||||
toolsItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.LearnOrg,
|
||||
ExecuteAsync = () => ShareAsync()
|
||||
});
|
||||
toolsItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.WebVault,
|
||||
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => WebVault())
|
||||
});
|
||||
toolsItems.Add(new SettingsPageListItem { Name = AppResources.LearnOrg });
|
||||
toolsItems.Add(new SettingsPageListItem { Name = AppResources.WebVault });
|
||||
}
|
||||
|
||||
var otherItems = new List<SettingsPageListItem>
|
||||
{
|
||||
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())
|
||||
},
|
||||
new SettingsPageListItem { Name = AppResources.Options },
|
||||
new SettingsPageListItem { Name = AppResources.About },
|
||||
new SettingsPageListItem { Name = AppResources.HelpAndFeedback },
|
||||
#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()))
|
||||
}
|
||||
new SettingsPageListItem { Name = AppResources.RateTheApp },
|
||||
new SettingsPageListItem { Name = AppResources.DeleteAccount }
|
||||
};
|
||||
|
||||
// TODO: improve this. Leaving this as is to reduce error possibility on the hotfix.
|
||||
@@ -720,33 +610,5 @@ namespace Bit.App.Pages
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,61 +184,31 @@
|
||||
<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="2"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding HasTotpValue}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ScanQrTitle}" />
|
||||
</Grid>
|
||||
|
||||
@@ -11,7 +11,6 @@ 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
|
||||
@@ -30,7 +29,6 @@ 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;
|
||||
@@ -55,7 +53,6 @@ namespace Bit.App.Pages
|
||||
nameof(ShowUris),
|
||||
nameof(ShowAttachments),
|
||||
nameof(ShowCollections),
|
||||
nameof(HasTotpValue)
|
||||
};
|
||||
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
|
||||
new List<KeyValuePair<UriMatchType?, string>>
|
||||
@@ -83,7 +80,6 @@ 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);
|
||||
@@ -93,7 +89,6 @@ 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>();
|
||||
@@ -155,7 +150,6 @@ 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; }
|
||||
@@ -306,8 +300,7 @@ namespace Bit.App.Pages
|
||||
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;
|
||||
@@ -865,19 +858,6 @@ 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
|
||||
|
||||
@@ -6,7 +6,6 @@ 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;
|
||||
|
||||
@@ -56,28 +55,21 @@ namespace Bit.App.Pages
|
||||
|
||||
_broadcasterService.Subscribe(nameof(AutofillCiphersPage), async (message) =>
|
||||
{
|
||||
try
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
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}"
|
||||
@@ -54,14 +53,6 @@
|
||||
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"
|
||||
@@ -113,7 +104,6 @@
|
||||
<pages:GroupingsPageListItemSelector x:Key="listItemDataTemplateSelector"
|
||||
HeaderTemplate="{StaticResource headerTemplate}"
|
||||
CipherTemplate="{StaticResource cipherTemplate}"
|
||||
AuthenticatorTemplate="{StaticResource authenticatorTemplate}"
|
||||
GroupTemplate="{StaticResource groupTemplate}" />
|
||||
|
||||
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
|
||||
@@ -141,29 +131,6 @@
|
||||
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"
|
||||
|
||||
@@ -7,7 +7,6 @@ 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;
|
||||
|
||||
@@ -96,28 +95,21 @@ namespace Bit.App.Pages
|
||||
|
||||
_broadcasterService.Subscribe(_pageName, async (message) =>
|
||||
{
|
||||
try
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -189,11 +181,10 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override async void OnDisappearing()
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
IsBusy = false;
|
||||
_vm.StopCiphersTotpTick();
|
||||
_broadcasterService.Unsubscribe(_pageName);
|
||||
_vm.DisableRefreshing();
|
||||
_accountAvatar?.OnDisappearing();
|
||||
@@ -206,13 +197,6 @@ 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;
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class GroupingsPageListGroup : List<IGroupingsPageListItem>
|
||||
public class GroupingsPageListGroup : List<GroupingsPageListItem>
|
||||
{
|
||||
public GroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false)
|
||||
: this(new List<IGroupingsPageListItem>(), name, count, doUpper, first)
|
||||
: this(new List<GroupingsPageListItem>(), name, count, doUpper, first)
|
||||
{ }
|
||||
|
||||
public GroupingsPageListGroup(IEnumerable<IGroupingsPageListItem> groupItems, string name, int count,
|
||||
public GroupingsPageListGroup(List<GroupingsPageListItem> groupItems, string name, int count,
|
||||
bool doUpper = true, bool first = false)
|
||||
{
|
||||
AddRange(groupItems);
|
||||
|
||||
@@ -7,7 +7,6 @@ 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)
|
||||
{
|
||||
@@ -16,16 +15,10 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
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,9 +1,7 @@
|
||||
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;
|
||||
@@ -31,16 +29,13 @@ 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;
|
||||
@@ -79,12 +74,6 @@ 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)
|
||||
{
|
||||
@@ -101,9 +90,6 @@ namespace Bit.App.Pages
|
||||
public bool HasCiphers { get; set; }
|
||||
public bool HasFolders { get; set; }
|
||||
public bool HasCollections { get; set; }
|
||||
public string ShowTotpCodesAccessibilityText => TotpFilterEnable ?
|
||||
AppResources.AuthenticationCodesListIsVisibleActivateToShowCipherList
|
||||
: AppResources.CipherListIsVisibleActivateToShowAuthenticationCodesList;
|
||||
public bool ShowNoFolderCipherGroup => NoFolderCiphers != null
|
||||
&& NoFolderCiphers.Count < NoFolderListSize
|
||||
&& (Collections is null || !Collections.Any());
|
||||
@@ -165,21 +151,12 @@ 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()
|
||||
@@ -204,22 +181,18 @@ 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;
|
||||
|
||||
@@ -299,7 +272,10 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (Ciphers?.Any() ?? false)
|
||||
{
|
||||
CreateCipherGroupedItems(groupedItems);
|
||||
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()));
|
||||
}
|
||||
if (ShowNoFolderCipherGroup)
|
||||
{
|
||||
@@ -387,44 +363,6 @@ 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;
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
<?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="{Binding ScanQrPageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:ScanPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
Title="{u:I18n ScanQrTitle}">
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
@@ -29,114 +16,67 @@
|
||||
<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"
|
||||
IsVisible="{Binding ShowScanner}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
OnScanResult="OnScanResult"/>
|
||||
<StackLayout
|
||||
VerticalOptions="Center"
|
||||
OnScanResult="OnScanResult">
|
||||
</zxing:ZXingScannerView>
|
||||
|
||||
<Grid
|
||||
VerticalOptions="FillAndExpand"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
IsVisible="{Binding ShowScanner}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Margin="30,0">
|
||||
AutomationId="zxingDefaultOverlay">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<forms:SKCanvasView
|
||||
x:Name="SkCanvasView"
|
||||
Margin="0,50,0,0"
|
||||
WidthRequest="250"
|
||||
HeightRequest="250"
|
||||
IsVisible="{Binding ShowScanner}"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
PaintSurface="OnCanvasViewPaintSurface"/>
|
||||
|
||||
<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"
|
||||
IsVisible="{Binding ShowScanner, Converter={StaticResource inverseBool}}"
|
||||
<BoxView
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Margin="30,0">
|
||||
VerticalOptions="Fill"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
BackgroundColor="Black"
|
||||
Opacity="0.7" />
|
||||
|
||||
<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
|
||||
Text="{u:I18n CameraInstructionTop}"
|
||||
AutomationId="zxingDefaultOverlay_TopTextLabel"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
TextColor="White" />
|
||||
|
||||
<BoxView
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
VerticalOptions="Fill"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
BackgroundColor="Transparent" />
|
||||
|
||||
<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="{Binding CameraInstructionTop}"
|
||||
AutomationId="zxingDefaultOverlay_TopTextLabel"
|
||||
Margin="30,15,30,0"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
StyleClass="text-sm"
|
||||
TextColor="White" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
FormattedText="{Binding ToggleScanModeLabel}"
|
||||
Text="{u:I18n CameraInstructionBottom}"
|
||||
AutomationId="zxingDefaultOverlay_BottomTextLabel"
|
||||
Grid.Column="0"
|
||||
Grid.Row="2"
|
||||
Margin="0,15"
|
||||
StyleClass="text-sm"
|
||||
FontAttributes="Bold"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Center" >
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="ToggleScanMode_OnTapped" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
TextColor="White" />
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</pages:BaseContentPage>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.Views.Forms;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class ScanPage : BaseContentPage
|
||||
{
|
||||
private ScanPageViewModel ViewModel => BindingContext as ScanPageViewModel;
|
||||
private readonly Action<string> _callback;
|
||||
|
||||
private CancellationTokenSource _autofocusCts;
|
||||
private Task _continuousAutofocusTask;
|
||||
private readonly Color _greenColor;
|
||||
private readonly SKColor _blueSKColor;
|
||||
private readonly SKColor _greenSKColor;
|
||||
private readonly Stopwatch _stopwatch;
|
||||
private bool _pageIsActive;
|
||||
private bool _qrcodeFound;
|
||||
private float _scale;
|
||||
|
||||
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
@@ -44,12 +32,6 @@ namespace Bit.App.Pages
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
}
|
||||
|
||||
_greenColor = ThemeManager.GetResourceColor("SuccessColor");
|
||||
_greenSKColor = _greenColor.ToSKColor();
|
||||
_blueSKColor = ThemeManager.GetResourceColor("PrimaryColor").ToSKColor();
|
||||
_stopwatch = new Stopwatch();
|
||||
_qrcodeFound = false;
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
@@ -76,14 +58,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (!autofocusCts.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_zxing.AutoFocus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
_zxing.AutoFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -94,83 +69,27 @@ namespace Bit.App.Pages
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}, autofocusCts.Token);
|
||||
_pageIsActive = true;
|
||||
AnimationLoopAsync();
|
||||
}
|
||||
|
||||
protected override async void OnDisappearing()
|
||||
{
|
||||
_autofocusCts?.Cancel();
|
||||
|
||||
if (_continuousAutofocusTask != null)
|
||||
{
|
||||
await _continuousAutofocusTask;
|
||||
}
|
||||
_zxing.IsScanning = false;
|
||||
_pageIsActive = false;
|
||||
|
||||
base.OnDisappearing();
|
||||
}
|
||||
|
||||
private async void OnScanResult(ZXing.Result result)
|
||||
private void OnScanResult(ZXing.Result result)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Stop analysis until we navigate away so we don't keep reading barcodes
|
||||
_zxing.IsAnalyzing = false;
|
||||
var text = result?.Text;
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
if (text.StartsWith("otpauth://totp"))
|
||||
{
|
||||
await QrCodeFoundAsync();
|
||||
_callback(text);
|
||||
return;
|
||||
}
|
||||
else if (Uri.TryCreate(text, UriKind.Absolute, out Uri uri) &&
|
||||
!string.IsNullOrWhiteSpace(uri?.Query))
|
||||
{
|
||||
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
|
||||
foreach (var part in queryParts)
|
||||
{
|
||||
if (part.StartsWith("secret="))
|
||||
{
|
||||
await QrCodeFoundAsync();
|
||||
var subResult = part.Substring(7);
|
||||
if (!string.IsNullOrEmpty(subResult))
|
||||
{
|
||||
_callback(subResult.ToUpperInvariant());
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_callback(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Value?.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task QrCodeFoundAsync()
|
||||
{
|
||||
_qrcodeFound = true;
|
||||
Vibration.Vibrate();
|
||||
await Task.Delay(1000);
|
||||
// Stop analysis until we navigate away so we don't keep reading barcodes
|
||||
_zxing.IsAnalyzing = false;
|
||||
_zxing.IsScanning = false;
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAuthenticationKey_OnClicked(object sender, EventArgs e)
|
||||
{
|
||||
var text = ViewModel.TotpAuthenticationKey;
|
||||
var text = result?.Text;
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
if (text.StartsWith("otpauth://totp"))
|
||||
@@ -179,7 +98,7 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
else if (Uri.TryCreate(text, UriKind.Absolute, out Uri uri) &&
|
||||
!string.IsNullOrWhiteSpace(uri?.Query))
|
||||
!string.IsNullOrWhiteSpace(uri?.Query))
|
||||
{
|
||||
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
|
||||
foreach (var part in queryParts)
|
||||
@@ -195,77 +114,11 @@ namespace Bit.App.Pages
|
||||
_callback(null);
|
||||
}
|
||||
|
||||
private void ToggleScanMode_OnTapped(object sender, EventArgs e)
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
ViewModel.ToggleScanModeCommand.Execute(null);
|
||||
if (!ViewModel.ShowScanner)
|
||||
if (DoOnce())
|
||||
{
|
||||
_authenticationKeyEntry.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
|
||||
{
|
||||
var info = args.Info;
|
||||
var surface = args.Surface;
|
||||
var canvas = surface.Canvas;
|
||||
var margins = 20;
|
||||
var maxSquareSize = (Math.Min(info.Height, info.Width) * 0.9f - margins) * _scale;
|
||||
var squareSize = maxSquareSize;
|
||||
var lineSize = squareSize * 0.15f;
|
||||
var startXPoint = (info.Width / 2) - (squareSize / 2);
|
||||
var startYPoint = (info.Height / 2) - (squareSize / 2);
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
using (var strokePaint = new SKPaint
|
||||
{
|
||||
Color = _qrcodeFound ? _greenSKColor : _blueSKColor,
|
||||
StrokeWidth = 9 * _scale,
|
||||
StrokeCap = SKStrokeCap.Round,
|
||||
})
|
||||
{
|
||||
canvas.Scale(1, 1);
|
||||
//top left
|
||||
canvas.DrawLine(startXPoint, startYPoint, startXPoint, startYPoint + lineSize, strokePaint);
|
||||
canvas.DrawLine(startXPoint, startYPoint, startXPoint + lineSize, startYPoint, strokePaint);
|
||||
//bot left
|
||||
canvas.DrawLine(startXPoint, startYPoint + squareSize, startXPoint, startYPoint + squareSize - lineSize, strokePaint);
|
||||
canvas.DrawLine(startXPoint, startYPoint + squareSize, startXPoint + lineSize, startYPoint + squareSize, strokePaint);
|
||||
//top right
|
||||
canvas.DrawLine(startXPoint + squareSize, startYPoint, startXPoint + squareSize - lineSize, startYPoint, strokePaint);
|
||||
canvas.DrawLine(startXPoint + squareSize, startYPoint, startXPoint + squareSize, startYPoint + lineSize, strokePaint);
|
||||
//bot right
|
||||
canvas.DrawLine(startXPoint + squareSize, startYPoint + squareSize, startXPoint + squareSize - lineSize, startYPoint + squareSize, strokePaint);
|
||||
canvas.DrawLine(startXPoint + squareSize, startYPoint + squareSize, startXPoint + squareSize, startYPoint + squareSize - lineSize, strokePaint);
|
||||
}
|
||||
}
|
||||
|
||||
async Task AnimationLoopAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_stopwatch.Start();
|
||||
while (_pageIsActive)
|
||||
{
|
||||
var t = _stopwatch.Elapsed.TotalSeconds % 2 / 2;
|
||||
_scale = (20 - (1 - (float)Math.Sin(4 * Math.PI * t))) / 20;
|
||||
SkCanvasView.InvalidateSurface();
|
||||
await Task.Delay(TimeSpan.FromSeconds(1.0 / 30));
|
||||
if (_qrcodeFound && _scale > 0.98f)
|
||||
{
|
||||
_checkIcon.TextColor = _greenColor;
|
||||
SkCanvasView.InvalidateSurface();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Value?.Exception(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stopwatch?.Stop();
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class ScanPageViewModel : BaseViewModel
|
||||
{
|
||||
private bool _showScanner = true;
|
||||
private string _totpAuthenticationKey;
|
||||
|
||||
public ScanPageViewModel()
|
||||
{
|
||||
ToggleScanModeCommand = new Command(() => ShowScanner = !ShowScanner);
|
||||
}
|
||||
|
||||
public Command ToggleScanModeCommand { get; set; }
|
||||
public string ScanQrPageTitle => ShowScanner ? AppResources.ScanQrTitle : AppResources.AuthenticatorKeyScanner;
|
||||
public string CameraInstructionTop => ShowScanner ? AppResources.PointYourCameraAtTheQRCode : AppResources.OnceTheKeyIsSuccessfullyEntered;
|
||||
public string TotpAuthenticationKey
|
||||
{
|
||||
get => _totpAuthenticationKey;
|
||||
set => SetProperty(ref _totpAuthenticationKey, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ToggleScanModeLabel)
|
||||
});
|
||||
}
|
||||
public bool ShowScanner
|
||||
{
|
||||
get => _showScanner;
|
||||
set => SetProperty(ref _showScanner, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ToggleScanModeLabel),
|
||||
nameof(ScanQrPageTitle),
|
||||
nameof(CameraInstructionTop)
|
||||
});
|
||||
}
|
||||
|
||||
public FormattedString ToggleScanModeLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
var fs = new FormattedString();
|
||||
fs.Spans.Add(new Span
|
||||
{
|
||||
Text = ShowScanner ? AppResources.CannotScanQRCode : AppResources.CannotAddAuthenticatorKey,
|
||||
TextColor = ThemeManager.GetResourceColor("TitleTextColor")
|
||||
});
|
||||
fs.Spans.Add(new Span
|
||||
{
|
||||
Text = ShowScanner ? AppResources.EnterKeyManually : AppResources.ScanQRCode,
|
||||
TextColor = ThemeManager.GetResourceColor("ScanningToggleModeTextColor")
|
||||
});
|
||||
return fs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?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.ViewPage"
|
||||
@@ -22,15 +22,15 @@
|
||||
<u:StringHasValueConverter x:Key="stringHasValue" />
|
||||
<u:IsNotNullConverter x:Key="notNull" />
|
||||
<ToolbarItem Text="{u:I18n Collections}"
|
||||
x:Key="collectionsItem"
|
||||
x:Name="_collectionsItem"
|
||||
Clicked="Collections_Clicked"
|
||||
Order="Secondary" />
|
||||
x:Key="collectionsItem"
|
||||
x:Name="_collectionsItem"
|
||||
Clicked="Collections_Clicked"
|
||||
Order="Secondary" />
|
||||
<ToolbarItem Text="{u:I18n MoveToOrganization}"
|
||||
x:Key="shareItem"
|
||||
x:Name="_shareItem"
|
||||
Clicked="Share_Clicked"
|
||||
Order="Secondary" />
|
||||
x:Key="shareItem"
|
||||
x:Name="_shareItem"
|
||||
Clicked="Share_Clicked"
|
||||
Order="Secondary" />
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<ToolbarItem Clicked="EditToolbarItem_Clicked" Order="Primary"
|
||||
@@ -83,7 +83,7 @@
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
@@ -96,7 +96,7 @@
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}" />
|
||||
<Grid StyleClass="box-row"
|
||||
<Grid StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -126,7 +126,7 @@
|
||||
Grid.Column="0"
|
||||
LineBreakMode="CharacterWrap"
|
||||
IsVisible="{Binding ShowPassword}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
|
||||
Command="{Binding CheckPasswordCommand}"
|
||||
@@ -136,7 +136,7 @@
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CheckPassword}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
@@ -147,7 +147,7 @@
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
@@ -165,11 +165,10 @@
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="40" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
@@ -179,49 +178,29 @@
|
||||
Grid.Column="0" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding TotpCodeFormatted, Mode=OneWay}"
|
||||
IsVisible="{Binding ShowUpgradePremiumTotpText, Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
VerticalTextAlignment="Start"
|
||||
VerticalOptions="Start" />
|
||||
<controls:CircularProgressbarView
|
||||
Progress="{Binding TotpProgress}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand" />
|
||||
Grid.Column="0" />
|
||||
<Label
|
||||
Text="{Binding TotpSec, Mode=OneWay}"
|
||||
Style="{DynamicResource textTotp}"
|
||||
Margin="0, 0, 10, 0"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
StyleClass="text-sm"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand" />
|
||||
<controls:IconButton
|
||||
HorizontalOptions="End"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="CenterAndExpand" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
IsVisible="{Binding CanAccessPremium}"
|
||||
CommandParameter="LoginTotp"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyTotp}" />
|
||||
<Label
|
||||
Text="{u:I18n PremiumSubscriptionRequired}"
|
||||
StyleClass="box-footer-label"
|
||||
IsVisible="{Binding ShowUpgradePremiumTotpText}"
|
||||
Margin="0,5,0,2"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowTotp}" />
|
||||
</StackLayout>
|
||||
@@ -265,7 +244,7 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding ShowCardNumber}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowCardNumberIcon}"
|
||||
Command="{Binding ToggleCardNumberCommand}"
|
||||
@@ -274,7 +253,7 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
@@ -337,7 +316,7 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding ShowCardCode}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowCardCodeIcon}"
|
||||
Command="{Binding ToggleCardCodeCommand}"
|
||||
@@ -346,7 +325,7 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
@@ -550,7 +529,7 @@
|
||||
<StackLayout StyleClass="box-row">
|
||||
<controls:SelectableLabel
|
||||
Text="{Binding Cipher.Notes, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"/>
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@@ -609,7 +588,7 @@
|
||||
StyleClass="box-value"
|
||||
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
|
||||
</StackLayout>
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowHiddenValueIcon}"
|
||||
Command="{Binding ToggleHiddenValueCommand}"
|
||||
@@ -657,7 +636,7 @@
|
||||
StyleClass="box-sub-label"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalTextAlignment="Center" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Download}}"
|
||||
Command="{Binding BindingContext.DownloadAttachmentCommand, Source={x:Reference _page}}"
|
||||
@@ -721,4 +700,4 @@
|
||||
</Button>
|
||||
</AbsoluteLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -57,44 +56,37 @@ namespace Bit.App.Pages
|
||||
|
||||
_broadcasterService.Subscribe(nameof(ViewPage), async (message) =>
|
||||
{
|
||||
try
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
IsBusy = false;
|
||||
if (message.Data is Dictionary<string, object> data && data.ContainsKey("successfully"))
|
||||
{
|
||||
var success = data["successfully"] as bool?;
|
||||
if (success.GetValueOrDefault())
|
||||
{
|
||||
var task = _vm.LoadAsync(() => AdjustToolbar());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (message.Command == "selectSaveFileResult")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
var data = message.Data as Tuple<string, string>;
|
||||
if (data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_vm.SaveFileSelected(data.Item1, data.Item2);
|
||||
});
|
||||
}
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (message.Command == "syncCompleted")
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
IsBusy = false;
|
||||
if (message.Data is Dictionary<string, object> data && data.ContainsKey("successfully"))
|
||||
{
|
||||
var success = data["successfully"] as bool?;
|
||||
if (success.GetValueOrDefault())
|
||||
{
|
||||
var task = _vm.LoadAsync(() => AdjustToolbar());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (message.Command == "selectSaveFileResult")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
var data = message.Data as Tuple<string, string>;
|
||||
if (data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_vm.SaveFileSelected(data.Item1, data.Item2);
|
||||
});
|
||||
}
|
||||
});
|
||||
await LoadOnAppearedAsync(_scrollView, true, async () =>
|
||||
@@ -112,7 +104,7 @@ namespace Bit.App.Pages
|
||||
base.OnDisappearing();
|
||||
IsBusy = false;
|
||||
_broadcasterService.Unsubscribe(nameof(ViewPage));
|
||||
_vm.StopCiphersTotpTick();
|
||||
_vm.CleanUp();
|
||||
}
|
||||
|
||||
private async void PasswordHistory_Tapped(object sender, System.EventArgs e)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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.Resources;
|
||||
using Bit.App.Utilities;
|
||||
@@ -13,7 +11,6 @@ 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
|
||||
@@ -23,6 +20,7 @@ namespace Bit.App.Pages
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ITotpService _totpService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
@@ -30,7 +28,6 @@ namespace Bit.App.Pages
|
||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||
private readonly ILocalizeService _localizeService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private CipherView _cipher;
|
||||
private List<ViewPageFieldViewModel> _fields;
|
||||
@@ -47,15 +44,13 @@ namespace Bit.App.Pages
|
||||
private byte[] _attachmentData;
|
||||
private string _attachmentFilename;
|
||||
private bool _passwordReprompted;
|
||||
private TotpHelper _totpTickHelper;
|
||||
private CancellationTokenSource _totpTickCancellationToken;
|
||||
private Task _totpTickTask;
|
||||
|
||||
public ViewPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
@@ -63,11 +58,10 @@ namespace Bit.App.Pages
|
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
CopyCommand = new AsyncCommand<string>((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
CopyUriCommand = new AsyncCommand<LoginUriView>(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
CopyFieldCommand = new AsyncCommand<FieldView>(field => CopyAsync(field.Type == FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
CopyCommand = new Command<string>((id) => CopyAsync(id, null));
|
||||
CopyUriCommand = new Command<LoginUriView>(CopyUri);
|
||||
CopyFieldCommand = new Command<FieldView>(CopyField);
|
||||
LaunchUriCommand = new Command<LoginUriView>(LaunchUri);
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
||||
@@ -78,9 +72,9 @@ namespace Bit.App.Pages
|
||||
PageTitle = AppResources.ViewItem;
|
||||
}
|
||||
|
||||
public ICommand CopyCommand { get; set; }
|
||||
public ICommand CopyUriCommand { get; set; }
|
||||
public ICommand CopyFieldCommand { get; set; }
|
||||
public Command CopyCommand { get; set; }
|
||||
public Command CopyUriCommand { get; set; }
|
||||
public Command CopyFieldCommand { get; set; }
|
||||
public Command LaunchUriCommand { get; set; }
|
||||
public Command TogglePasswordCommand { get; set; }
|
||||
public Command ToggleCardNumberCommand { get; set; }
|
||||
@@ -108,7 +102,6 @@ namespace Bit.App.Pages
|
||||
nameof(ShowIdentityAddress),
|
||||
nameof(IsDeleted),
|
||||
nameof(CanEdit),
|
||||
nameof(ShowUpgradePremiumTotpText)
|
||||
});
|
||||
}
|
||||
public List<ViewPageFieldViewModel> Fields
|
||||
@@ -210,22 +203,21 @@ namespace Bit.App.Pages
|
||||
return fs;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowUpgradePremiumTotpText => !CanAccessPremium && ShowTotp;
|
||||
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
|
||||
public bool ShowIdentityAddress => IsIdentity && (
|
||||
!string.IsNullOrWhiteSpace(Cipher.Identity.Address1) ||
|
||||
!string.IsNullOrWhiteSpace(Cipher.Identity.City) ||
|
||||
!string.IsNullOrWhiteSpace(Cipher.Identity.Country));
|
||||
public bool ShowAttachments => Cipher.HasAttachments && (CanAccessPremium || Cipher.OrganizationId != null);
|
||||
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp);
|
||||
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
||||
!string.IsNullOrWhiteSpace(TotpCodeFormatted);
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string ShowCardNumberIcon => ShowCardNumber ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string ShowCardCodeIcon => ShowCardCode ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string TotpCodeFormatted
|
||||
{
|
||||
get => _canAccessPremium ? _totpCodeFormatted : string.Empty;
|
||||
get => _totpCodeFormatted;
|
||||
set => SetProperty(ref _totpCodeFormatted, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
@@ -235,11 +227,7 @@ namespace Bit.App.Pages
|
||||
public string TotpSec
|
||||
{
|
||||
get => _totpSec;
|
||||
set => SetProperty(ref _totpSec, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(TotpProgress)
|
||||
});
|
||||
set => SetProperty(ref _totpSec, value);
|
||||
}
|
||||
public bool TotpLow
|
||||
{
|
||||
@@ -250,12 +238,12 @@ namespace Bit.App.Pages
|
||||
Page.Resources["textTotp"] = ThemeManager.Resources()[value ? "text-danger" : "text-default"];
|
||||
}
|
||||
}
|
||||
public double TotpProgress => string.IsNullOrEmpty(TotpSec) ? 0 : double.Parse(TotpSec) * 100 / 30;
|
||||
public bool IsDeleted => Cipher.IsDeleted;
|
||||
public bool CanEdit => !Cipher.IsDeleted;
|
||||
|
||||
public async Task<bool> LoadAsync(Action finishedLoadingAction = null)
|
||||
{
|
||||
CleanUp();
|
||||
var cipher = await _cipherService.GetAsync(CipherId);
|
||||
if (cipher == null)
|
||||
{
|
||||
@@ -269,10 +257,19 @@ namespace Bit.App.Pages
|
||||
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
||||
(Cipher.OrganizationUseTotp || CanAccessPremium))
|
||||
{
|
||||
_totpTickHelper = new TotpHelper(Cipher);
|
||||
_totpTickCancellationToken?.Cancel();
|
||||
_totpTickCancellationToken = new CancellationTokenSource();
|
||||
_totpTickTask = new TimerTask(_logger, StartCiphersTotpTick, _totpTickCancellationToken).RunPeriodic();
|
||||
await TotpUpdateCodeAsync();
|
||||
var interval = _totpService.GetTimeInterval(Cipher.Login.Totp);
|
||||
await TotpTickAsync(interval);
|
||||
_totpInterval = DateTime.UtcNow;
|
||||
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
|
||||
{
|
||||
if (_totpInterval == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var task = TotpTickAsync(interval);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (_previousCipherId != CipherId)
|
||||
{
|
||||
@@ -283,27 +280,9 @@ namespace Bit.App.Pages
|
||||
return true;
|
||||
}
|
||||
|
||||
private async void StartCiphersTotpTick()
|
||||
public void CleanUp()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _totpTickHelper.GenerateNewTotpValues();
|
||||
TotpSec = _totpTickHelper.TotpSec;
|
||||
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopCiphersTotpTick()
|
||||
{
|
||||
_totpTickCancellationToken?.Cancel();
|
||||
if (_totpTickTask != null)
|
||||
{
|
||||
await _totpTickTask;
|
||||
}
|
||||
_totpInterval = null;
|
||||
}
|
||||
|
||||
public async void TogglePassword()
|
||||
@@ -431,6 +410,47 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task TotpUpdateCodeAsync()
|
||||
{
|
||||
if (Cipher == null || Cipher.Type != Core.Enums.CipherType.Login || Cipher.Login.Totp == null)
|
||||
{
|
||||
_totpInterval = null;
|
||||
return;
|
||||
}
|
||||
_totpCode = await _totpService.GetCodeAsync(Cipher.Login.Totp);
|
||||
if (_totpCode != null)
|
||||
{
|
||||
if (_totpCode.Length > 4)
|
||||
{
|
||||
var half = (int)Math.Floor(_totpCode.Length / 2M);
|
||||
TotpCodeFormatted = string.Format("{0} {1}", _totpCode.Substring(0, half),
|
||||
_totpCode.Substring(half));
|
||||
}
|
||||
else
|
||||
{
|
||||
TotpCodeFormatted = _totpCode;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TotpCodeFormatted = null;
|
||||
_totpInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TotpTickAsync(int intervalSeconds)
|
||||
{
|
||||
var epoc = CoreHelpers.EpocUtcNow() / 1000;
|
||||
var mod = epoc % intervalSeconds;
|
||||
var totpSec = intervalSeconds - mod;
|
||||
TotpSec = totpSec.ToString();
|
||||
TotpLow = totpSec < 7;
|
||||
if (mod == 0)
|
||||
{
|
||||
await TotpUpdateCodeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void CheckPasswordAsync()
|
||||
{
|
||||
if (!(Page as BaseContentPage).DoOnce())
|
||||
@@ -596,7 +616,7 @@ namespace Bit.App.Pages
|
||||
_attachmentFilename = null;
|
||||
}
|
||||
|
||||
private async Task CopyAsync(string id, string text = null)
|
||||
private async void CopyAsync(string id, string text = null)
|
||||
{
|
||||
if (_passwordRepromptService.ProtectedFields.Contains(id) && !await PromptPasswordAsync())
|
||||
{
|
||||
@@ -616,7 +636,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if (id == "LoginTotp")
|
||||
{
|
||||
text = TotpCodeFormatted.Replace(" ", string.Empty);
|
||||
text = _totpCode;
|
||||
name = AppResources.VerificationCodeTotp;
|
||||
}
|
||||
else if (id == "LoginUri")
|
||||
@@ -660,6 +680,16 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyUri(LoginUriView uri)
|
||||
{
|
||||
CopyAsync("LoginUri", uri.Uri);
|
||||
}
|
||||
|
||||
private void CopyField(FieldView field)
|
||||
{
|
||||
CopyAsync(field.Type == Core.Enums.FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value);
|
||||
}
|
||||
|
||||
private void LaunchUri(LoginUriView uri)
|
||||
{
|
||||
if (uri.CanLaunch && (Page as BaseContentPage).DoOnce())
|
||||
@@ -726,7 +756,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (IsBooleanType)
|
||||
{
|
||||
return _field.Value == "true" ? BitwardenIcons.CheckSquare : BitwardenIcons.Square;
|
||||
return _field.Value == "true" ? BitwardenIcons.Square : BitwardenIcons.CheckSquare;
|
||||
}
|
||||
else if (IsLinkedType)
|
||||
{
|
||||
|
||||
9661
src/App/Resources/AppResources.Designer.cs
generated
9661
src/App/Resources/AppResources.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
@@ -236,7 +236,7 @@
|
||||
<comment>The button text that allows user to launch the website to their web browser.</comment>
|
||||
</data>
|
||||
<data name="HelpAndFeedback" xml:space="preserve">
|
||||
<value>Help and feedback</value>
|
||||
<value>Help and Feedback</value>
|
||||
</data>
|
||||
<data name="Hide" xml:space="preserve">
|
||||
<value>Hide</value>
|
||||
@@ -269,7 +269,7 @@
|
||||
<comment>Title for login page. (noun)</comment>
|
||||
</data>
|
||||
<data name="LogOut" xml:space="preserve">
|
||||
<value>Log out</value>
|
||||
<value>Log Out</value>
|
||||
<comment>The log out button text (verb).</comment>
|
||||
</data>
|
||||
<data name="LogoutConfirmation" xml:space="preserve">
|
||||
@@ -299,10 +299,6 @@
|
||||
<value>My Vault</value>
|
||||
<comment>The title for the vault page.</comment>
|
||||
</data>
|
||||
<data name="Authenticator" xml:space="preserve">
|
||||
<value>Authenticator</value>
|
||||
<comment>Authenticator TOTP feature</comment>
|
||||
</data>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
<comment>Label for an entity name.</comment>
|
||||
@@ -416,7 +412,7 @@
|
||||
<value>Add an Item</value>
|
||||
</data>
|
||||
<data name="AppExtension" xml:space="preserve">
|
||||
<value>App extension</value>
|
||||
<value>App Extension</value>
|
||||
</data>
|
||||
<data name="AutofillAccessibilityDescription" xml:space="preserve">
|
||||
<value>Use the Bitwarden accessibility service to auto-fill your logins across apps and the web.</value>
|
||||
@@ -520,7 +516,7 @@
|
||||
<value>Get your master password hint</value>
|
||||
</data>
|
||||
<data name="ImportItems" xml:space="preserve">
|
||||
<value>Import items</value>
|
||||
<value>Import Items</value>
|
||||
</data>
|
||||
<data name="ImportItemsConfirmation" xml:space="preserve">
|
||||
<value>You can bulk import items from the bitwarden.com web vault. Do you want to visit the website now?</value>
|
||||
@@ -553,10 +549,10 @@
|
||||
<value>Immediately</value>
|
||||
</data>
|
||||
<data name="VaultTimeout" xml:space="preserve">
|
||||
<value>Vault timeout</value>
|
||||
<value>Vault Timeout</value>
|
||||
</data>
|
||||
<data name="VaultTimeoutAction" xml:space="preserve">
|
||||
<value>Vault timeout action</value>
|
||||
<value>Vault Timeout Action</value>
|
||||
</data>
|
||||
<data name="VaultTimeoutLogOutConfirmation" xml:space="preserve">
|
||||
<value>Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?</value>
|
||||
@@ -651,7 +647,7 @@
|
||||
<comment>Push notifications for apple products</comment>
|
||||
</data>
|
||||
<data name="RateTheApp" xml:space="preserve">
|
||||
<value>Rate the app</value>
|
||||
<value>Rate the App</value>
|
||||
</data>
|
||||
<data name="RateTheAppDescription" xml:space="preserve">
|
||||
<value>Please consider helping us out with a good review!</value>
|
||||
@@ -705,7 +701,7 @@
|
||||
<comment>What Apple calls their fingerprint reader.</comment>
|
||||
</data>
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>Two-step login</value>
|
||||
<value>Two-step Login</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?</value>
|
||||
@@ -714,7 +710,7 @@
|
||||
<value>Unlock with {0}</value>
|
||||
</data>
|
||||
<data name="UnlockWithPIN" xml:space="preserve">
|
||||
<value>Unlock with PIN code</value>
|
||||
<value>Unlock with PIN Code</value>
|
||||
</data>
|
||||
<data name="Validating" xml:space="preserve">
|
||||
<value>Validating</value>
|
||||
@@ -893,9 +889,11 @@
|
||||
<data name="AuthenticatorKeyReadError" xml:space="preserve">
|
||||
<value>Cannot read authenticator key.</value>
|
||||
</data>
|
||||
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
|
||||
<value>Point your camera at the QR Code.
|
||||
Scanning will happen automatically.</value>
|
||||
<data name="CameraInstructionBottom" xml:space="preserve">
|
||||
<value>Scanning will happen automatically.</value>
|
||||
</data>
|
||||
<data name="CameraInstructionTop" xml:space="preserve">
|
||||
<value>Point your camera at the QR code.</value>
|
||||
</data>
|
||||
<data name="ScanQrTitle" xml:space="preserve">
|
||||
<value>Scan QR Code</value>
|
||||
@@ -909,11 +907,11 @@ Scanning will happen automatically.</value>
|
||||
<data name="CopyTotp" xml:space="preserve">
|
||||
<value>Copy TOTP</value>
|
||||
</data>
|
||||
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||
<value>If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login.</value>
|
||||
<data name="DisableAutoTotpCopyDescription" xml:space="preserve">
|
||||
<value>If your login has an authenticator key attached to it, the TOTP verification code is automatically copied to your clipboard whenever you auto-fill the login.</value>
|
||||
</data>
|
||||
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||
<value>Copy TOTP automatically</value>
|
||||
<data name="DisableAutoTotpCopy" xml:space="preserve">
|
||||
<value>Disable Automatic TOTP Copy</value>
|
||||
</data>
|
||||
<data name="PremiumRequired" xml:space="preserve">
|
||||
<value>A premium membership is required to use this feature.</value>
|
||||
@@ -1130,11 +1128,11 @@ Scanning will happen automatically.</value>
|
||||
<data name="Expiration" xml:space="preserve">
|
||||
<value>Expiration</value>
|
||||
</data>
|
||||
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||
<value>Show website icons</value>
|
||||
<data name="DisableWebsiteIcons" xml:space="preserve">
|
||||
<value>Disable Website Icons</value>
|
||||
</data>
|
||||
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
|
||||
<value>Show a recognizable image next to each login.</value>
|
||||
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
|
||||
<value>Website Icons provide a recognizable image next to each login item in your vault.</value>
|
||||
</data>
|
||||
<data name="IconsUrl" xml:space="preserve">
|
||||
<value>Icons Server URL</value>
|
||||
@@ -1313,7 +1311,7 @@ Scanning will happen automatically.</value>
|
||||
<value>5. Select "Bitwarden"</value>
|
||||
</data>
|
||||
<data name="PasswordAutofill" xml:space="preserve">
|
||||
<value>Password auto-fill</value>
|
||||
<value>Password AutoFill</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillAlert2" xml:space="preserve">
|
||||
<value>The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Settings" screen.</value>
|
||||
@@ -1451,7 +1449,7 @@ Scanning will happen automatically.</value>
|
||||
<value>There are no folders to list.</value>
|
||||
</data>
|
||||
<data name="FingerprintPhrase" xml:space="preserve">
|
||||
<value>Fingerprint phrase</value>
|
||||
<value>Fingerprint Phrase</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="YourAccountsFingerprint" xml:space="preserve">
|
||||
@@ -1462,10 +1460,10 @@ Scanning will happen automatically.</value>
|
||||
<value>Bitwarden allows you to share your vault items with others by using an organization account. Would you like to visit the bitwarden.com website to learn more?</value>
|
||||
</data>
|
||||
<data name="ExportVault" xml:space="preserve">
|
||||
<value>Export vault</value>
|
||||
<value>Export Vault</value>
|
||||
</data>
|
||||
<data name="LockNow" xml:space="preserve">
|
||||
<value>Lock now</value>
|
||||
<value>Lock Now</value>
|
||||
</data>
|
||||
<data name="PIN" xml:space="preserve">
|
||||
<value>PIN</value>
|
||||
@@ -1519,7 +1517,7 @@ Scanning will happen automatically.</value>
|
||||
<value>2 minutes</value>
|
||||
</data>
|
||||
<data name="ClearClipboard" xml:space="preserve">
|
||||
<value>Clear clipboard</value>
|
||||
<value>Clear Clipboard</value>
|
||||
<comment>Clipboard is the operating system thing where you copy/paste data to on your device.</comment>
|
||||
</data>
|
||||
<data name="ClearClipboardDescription" xml:space="preserve">
|
||||
@@ -1527,7 +1525,7 @@ Scanning will happen automatically.</value>
|
||||
<comment>Clipboard is the operating system thing where you copy/paste data to on your device.</comment>
|
||||
</data>
|
||||
<data name="DefaultUriMatchDetection" xml:space="preserve">
|
||||
<value>Default URI match detection</value>
|
||||
<value>Default URI Match Detection</value>
|
||||
<comment>Default URI match detection for auto-fill.</comment>
|
||||
</data>
|
||||
<data name="DefaultUriMatchDetectionDescription" xml:space="preserve">
|
||||
@@ -1543,12 +1541,6 @@ Scanning will happen automatically.</value>
|
||||
<data name="ThemeDefault" xml:space="preserve">
|
||||
<value>Default (System)</value>
|
||||
</data>
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Default Dark Theme</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Choose the dark theme to use when using Default (System) theme while your device's dark mode is enabled</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Copy Note</value>
|
||||
</data>
|
||||
@@ -1565,24 +1557,20 @@ Scanning will happen automatically.</value>
|
||||
<value>Black</value>
|
||||
<comment>The color black</comment>
|
||||
</data>
|
||||
<data name="Nord" xml:space="preserve">
|
||||
<value>Nord</value>
|
||||
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
|
||||
</data>
|
||||
<data name="BlacklistedUris" xml:space="preserve">
|
||||
<value>Blacklisted URIs</value>
|
||||
</data>
|
||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
||||
<value>URIs that are blacklisted will not offer auto-fill. The list should be comma separated. Ex: "https://twitter.com, androidapp://com.twitter.android".</value>
|
||||
</data>
|
||||
<data name="AskToAddLogin" xml:space="preserve">
|
||||
<value>Ask to add login</value>
|
||||
<data name="DisableSavePrompt" xml:space="preserve">
|
||||
<value>Disable Save Prompt</value>
|
||||
</data>
|
||||
<data name="AskToAddLoginDescription" xml:space="preserve">
|
||||
<value>Ask to add an item if one isn't found in your vault.</value>
|
||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
||||
<value>The "Save Prompt" automatically prompts you to save new items to your vault whenever you enter them for the first time.</value>
|
||||
</data>
|
||||
<data name="OnRestart" xml:space="preserve">
|
||||
<value>On app restart</value>
|
||||
<value>On App Restart</value>
|
||||
</data>
|
||||
<data name="AutofillServiceNotEnabled" xml:space="preserve">
|
||||
<value>Auto-fill makes it easy to securely access your Bitwarden vault from other websites and apps. It looks like you have not enabled an auto-fill service for Bitwarden. Enable auto-fill for Bitwarden from the "Settings" screen.</value>
|
||||
@@ -2161,13 +2149,13 @@ Scanning will happen automatically.</value>
|
||||
<value>Account removed successfully</value>
|
||||
</data>
|
||||
<data name="DeleteAccount" xml:space="preserve">
|
||||
<value>Delete account</value>
|
||||
<value>Delete Account</value>
|
||||
</data>
|
||||
<data name="DeletingYourAccountIsPermanent" xml:space="preserve">
|
||||
<value>Deleting your account is permanent</value>
|
||||
</data>
|
||||
<data name="DeleteAccountExplanation" xml:space="preserve">
|
||||
<value>Your account and all vault data will be erased and unrecoverable. Are you sure you want to continue?</value>
|
||||
<value>Your account and all associated data will be erased and unrecoverable. Are you sure you want to continue?</value>
|
||||
</data>
|
||||
<data name="DeletingYourAccount" xml:space="preserve">
|
||||
<value>Deleting your account</value>
|
||||
@@ -2256,59 +2244,4 @@ Scanning will happen automatically.</value>
|
||||
<data name="All" xml:space="preserve">
|
||||
<value>All</value>
|
||||
</data>
|
||||
<data name="DisplayItemsContainingTOTP" xml:space="preserve">
|
||||
<value>Display items containing TOTP</value>
|
||||
</data>
|
||||
<data name="PremiumSubscriptionRequired" xml:space="preserve">
|
||||
<value>Premium subscription required</value>
|
||||
</data>
|
||||
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
|
||||
<value>Cannot add authenticator key? </value>
|
||||
</data>
|
||||
<data name="ScanQRCode" xml:space="preserve">
|
||||
<value>Scan QR Code</value>
|
||||
</data>
|
||||
<data name="CannotScanQRCode" xml:space="preserve">
|
||||
<value>Cannot scan QR Code? </value>
|
||||
</data>
|
||||
<data name="AuthenticatorKeyScanner" xml:space="preserve">
|
||||
<value>Authenticator Key</value>
|
||||
</data>
|
||||
<data name="EnterKeyManually" xml:space="preserve">
|
||||
<value>Enter Key Manually</value>
|
||||
</data>
|
||||
<data name="AddTotp" xml:space="preserve">
|
||||
<value>Add TOTP</value>
|
||||
</data>
|
||||
<data name="SetupTotp" xml:space="preserve">
|
||||
<value>Set up TOTP</value>
|
||||
</data>
|
||||
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
|
||||
<value>Once the key is successfully entered,
|
||||
select Add TOTP to store the key safely</value>
|
||||
</data>
|
||||
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
|
||||
<value></value>
|
||||
</data>
|
||||
<data name="NeverLockWarning" xml:space="preserve">
|
||||
<value>Setting your lock options to “Never” keeps your vault available to anyone with access to your device. If you use this option, you should ensure that you keep your device properly protected.</value>
|
||||
</data>
|
||||
<data name="CipherListIsVisibleActivateToShowAuthenticationCodesList" xml:space="preserve">
|
||||
<value>Cipher list is visible, activate to show authentication codes list.</value>
|
||||
</data>
|
||||
<data name="AuthenticationCodesListIsVisibleActivateToShowCipherList" xml:space="preserve">
|
||||
<value>Authentication codes list is visible, activate to show cipher list.</value>
|
||||
</data>
|
||||
<data name="EnvironmentPageUrlsError" xml:space="preserve">
|
||||
<value>One or more of the URLs entered are invalid. Please revise it and try to save again.</value>
|
||||
</data>
|
||||
<data name="GenericErrorMessage" xml:space="preserve">
|
||||
<value>We were unable to process your request. Please try again or contact us.</value>
|
||||
</data>
|
||||
<data name="AllowScreenCapture" xml:space="preserve">
|
||||
<value>Allow Screen Capture</value>
|
||||
</data>
|
||||
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||
<value>Are you sure you want to enable Screen Capture?</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -79,34 +79,9 @@
|
||||
<Setter Property="StepperForegroundColor"
|
||||
Value="{DynamicResource StepperForegroundColor}" />
|
||||
</Style>
|
||||
<Style TargetType="Frame"
|
||||
Class="btn-icon-row">
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource ButtonBackgroundColor}" />
|
||||
<Setter Property="BorderColor"
|
||||
Value="{DynamicResource ButtonBorderColor}" />
|
||||
<Setter Property="CornerRadius"
|
||||
Value="5" />
|
||||
<Setter Property="Margin"
|
||||
Value="0, 5, 0, 0" />
|
||||
<Setter Property="HasShadow"
|
||||
Value="False" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Buttons -->
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource ButtonBackgroundColor}" />
|
||||
|
||||
@@ -505,17 +505,4 @@
|
||||
</Keyboard>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="controls:CircularProgressbarView">
|
||||
<Setter Property="ProgressColor"
|
||||
Value="{DynamicResource PrimaryColor}" />
|
||||
<Setter Property="EndingProgressColor"
|
||||
Value="{DynamicResource DangerColor}" />
|
||||
<Setter Property="BackgroundProgressColor"
|
||||
Value="{DynamicResource BackgroundColor}" />
|
||||
<Setter Property="StrokeWidth"
|
||||
Value="3" />
|
||||
<Setter Property="Radius"
|
||||
Value="15" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -71,6 +71,4 @@
|
||||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#52bdfb</Color>
|
||||
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -71,6 +71,4 @@
|
||||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#52bdfb</Color>
|
||||
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -71,6 +71,4 @@
|
||||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#175DDC</Color>
|
||||
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -71,6 +71,4 @@
|
||||
<Color x:Key="NavigationBarTextColor">#e5e9f0</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#81a1c1</Color>
|
||||
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -92,32 +92,6 @@
|
||||
<Setter Property="StepperForegroundColor"
|
||||
Value="{DynamicResource StepperForegroundColor}" />
|
||||
</Style>
|
||||
<Style TargetType="Frame"
|
||||
Class="btn-icon-row">
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource ButtonBackgroundColor}" />
|
||||
<Setter Property="BorderColor"
|
||||
Value="{DynamicResource ButtonBorderColor}" />
|
||||
<Setter Property="CornerRadius"
|
||||
Value="5" />
|
||||
<Setter Property="Margin"
|
||||
Value="0, 10, 0, 0" />
|
||||
<Setter Property="HasShadow"
|
||||
Value="False" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Buttons -->
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ namespace Bit.App.Utilities.AccountManagement
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
Func<AppOptions> _getOptionsFunc;
|
||||
private IAccountsManagerHost _accountsManagerHost;
|
||||
@@ -29,8 +28,7 @@ namespace Bit.App.Utilities.AccountManagement
|
||||
IStorageService secureStorageService,
|
||||
IStateService stateService,
|
||||
IPlatformUtilsService platformUtilsService,
|
||||
IAuthService authService,
|
||||
ILogger logger)
|
||||
IAuthService authService)
|
||||
{
|
||||
_broadcasterService = broadcasterService;
|
||||
_vaultTimeoutService = vaultTimeoutService;
|
||||
@@ -38,7 +36,6 @@ namespace Bit.App.Utilities.AccountManagement
|
||||
_stateService = stateService;
|
||||
_platformUtilsService = platformUtilsService;
|
||||
_authService = authService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true };
|
||||
@@ -112,49 +109,42 @@ namespace Bit.App.Utilities.AccountManagement
|
||||
|
||||
private async void OnMessage(Message message)
|
||||
{
|
||||
try
|
||||
switch (message.Command)
|
||||
{
|
||||
switch (message.Command)
|
||||
{
|
||||
case AccountsManagerMessageCommands.LOCKED:
|
||||
await Device.InvokeOnMainThreadAsync(() => LockedAsync(message.Data as Tuple<string, bool>));
|
||||
break;
|
||||
case AccountsManagerMessageCommands.LOCK_VAULT:
|
||||
await _vaultTimeoutService.LockAsync(true);
|
||||
break;
|
||||
case AccountsManagerMessageCommands.LOGOUT:
|
||||
var extras = message.Data as Tuple<string, bool, bool>;
|
||||
var userId = extras?.Item1;
|
||||
var userInitiated = extras?.Item2 ?? true;
|
||||
var expired = extras?.Item3 ?? false;
|
||||
await Device.InvokeOnMainThreadAsync(() => LogOutAsync(userId, userInitiated, expired));
|
||||
break;
|
||||
case AccountsManagerMessageCommands.LOGGED_OUT:
|
||||
// Clean up old migrated key if they ever log out.
|
||||
await _secureStorageService.RemoveAsync("oldKey");
|
||||
break;
|
||||
case AccountsManagerMessageCommands.ADD_ACCOUNT:
|
||||
await AddAccountAsync();
|
||||
break;
|
||||
case AccountsManagerMessageCommands.ACCOUNT_ADDED:
|
||||
await _accountsManagerHost.UpdateThemeAsync();
|
||||
break;
|
||||
case AccountsManagerMessageCommands.SWITCHED_ACCOUNT:
|
||||
await SwitchedAccountAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
case AccountsManagerMessageCommands.LOCKED:
|
||||
Locked(message.Data as Tuple<string, bool>);
|
||||
break;
|
||||
case AccountsManagerMessageCommands.LOCK_VAULT:
|
||||
await _vaultTimeoutService.LockAsync(true);
|
||||
break;
|
||||
case AccountsManagerMessageCommands.LOGOUT:
|
||||
LogOut(message.Data as Tuple<string, bool, bool>);
|
||||
break;
|
||||
case AccountsManagerMessageCommands.LOGGED_OUT:
|
||||
// Clean up old migrated key if they ever log out.
|
||||
await _secureStorageService.RemoveAsync("oldKey");
|
||||
break;
|
||||
case AccountsManagerMessageCommands.ADD_ACCOUNT:
|
||||
AddAccount();
|
||||
break;
|
||||
case AccountsManagerMessageCommands.ACCOUNT_ADDED:
|
||||
await _accountsManagerHost.UpdateThemeAsync();
|
||||
break;
|
||||
case AccountsManagerMessageCommands.SWITCHED_ACCOUNT:
|
||||
await SwitchedAccountAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LockedAsync(Tuple<string, bool> extras)
|
||||
private void Locked(Tuple<string, bool> extras)
|
||||
{
|
||||
var userId = extras?.Item1;
|
||||
var userInitiated = extras?.Item2 ?? false;
|
||||
Device.BeginInvokeOnMainThread(async () => await LockedAsync(userId, userInitiated));
|
||||
}
|
||||
|
||||
private async Task LockedAsync(string userId, bool userInitiated)
|
||||
{
|
||||
if (!await _stateService.IsActiveAccountAsync(userId))
|
||||
{
|
||||
_platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully);
|
||||
@@ -173,19 +163,27 @@ namespace Bit.App.Utilities.AccountManagement
|
||||
|
||||
await _accountsManagerHost.SetPreviousPageInfoAsync();
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(() => _accountsManagerHost.Navigate(NavigationTarget.Lock, new LockNavigationParams(autoPromptBiometric)));
|
||||
Device.BeginInvokeOnMainThread(() => _accountsManagerHost.Navigate(NavigationTarget.Lock, new LockNavigationParams(autoPromptBiometric)));
|
||||
}
|
||||
|
||||
private async Task AddAccountAsync()
|
||||
private void AddAccount()
|
||||
{
|
||||
await Device.InvokeOnMainThreadAsync(() =>
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
Options.HideAccountSwitcher = false;
|
||||
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task LogOutAsync(string userId, bool userInitiated, bool expired)
|
||||
private void LogOut(Tuple<string, bool, bool> extras)
|
||||
{
|
||||
var userId = extras?.Item1;
|
||||
var userInitiated = extras?.Item2 ?? true;
|
||||
var expired = extras?.Item3 ?? false;
|
||||
Device.BeginInvokeOnMainThread(async () => await LogOutAsync(userId, userInitiated, expired));
|
||||
}
|
||||
|
||||
private async Task LogOutAsync(string userId, bool userInitiated, bool expired)
|
||||
{
|
||||
await AppHelpers.LogOutAsync(userId, userInitiated);
|
||||
await NavigateOnAccountChangeAsync();
|
||||
|
||||
@@ -17,18 +17,13 @@ namespace Bit.App.Utilities
|
||||
|
||||
public static bool IsThemeDirty = false;
|
||||
|
||||
public const string Light = "light";
|
||||
public const string Dark = "dark";
|
||||
public const string Black = "black";
|
||||
public const string Nord = "nord";
|
||||
|
||||
public static void SetThemeStyle(string name, string autoDarkName, ResourceDictionary resources)
|
||||
public static void SetThemeStyle(string name, ResourceDictionary resources)
|
||||
{
|
||||
try
|
||||
{
|
||||
Resources = () => resources;
|
||||
|
||||
var newTheme = NeedsThemeUpdate(name, autoDarkName, resources);
|
||||
var newTheme = NeedsThemeUpdate(name, resources);
|
||||
if (newTheme is null)
|
||||
{
|
||||
return;
|
||||
@@ -90,30 +85,22 @@ namespace Bit.App.Utilities
|
||||
: Activator.CreateInstance(themeType) as ResourceDictionary;
|
||||
}
|
||||
|
||||
static ResourceDictionary NeedsThemeUpdate(string themeName, string autoDarkThemeName, ResourceDictionary resources)
|
||||
static ResourceDictionary NeedsThemeUpdate(string themeName, ResourceDictionary resources)
|
||||
{
|
||||
switch (themeName)
|
||||
{
|
||||
case Dark:
|
||||
case "dark":
|
||||
return CheckAndGetThemeForMergedDictionaries(typeof(Dark), resources);
|
||||
case Black:
|
||||
case "black":
|
||||
return CheckAndGetThemeForMergedDictionaries(typeof(Black), resources);
|
||||
case Nord:
|
||||
case "nord":
|
||||
return CheckAndGetThemeForMergedDictionaries(typeof(Nord), resources);
|
||||
case Light:
|
||||
case "light":
|
||||
return CheckAndGetThemeForMergedDictionaries(typeof(Light), resources);
|
||||
default:
|
||||
if (OsDarkModeEnabled())
|
||||
{
|
||||
switch (autoDarkThemeName)
|
||||
{
|
||||
case Black:
|
||||
return CheckAndGetThemeForMergedDictionaries(typeof(Black), resources);
|
||||
case Nord:
|
||||
return CheckAndGetThemeForMergedDictionaries(typeof(Nord), resources);
|
||||
default:
|
||||
return CheckAndGetThemeForMergedDictionaries(typeof(Dark), resources);
|
||||
}
|
||||
return CheckAndGetThemeForMergedDictionaries(typeof(Dark), resources);
|
||||
}
|
||||
return CheckAndGetThemeForMergedDictionaries(typeof(Light), resources);
|
||||
}
|
||||
@@ -121,7 +108,7 @@ namespace Bit.App.Utilities
|
||||
|
||||
public static void SetTheme(ResourceDictionary resources)
|
||||
{
|
||||
SetThemeStyle(GetTheme(), GetAutoDarkTheme(), resources);
|
||||
SetThemeStyle(GetTheme(), resources);
|
||||
}
|
||||
|
||||
public static string GetTheme()
|
||||
@@ -130,12 +117,6 @@ namespace Bit.App.Utilities
|
||||
return stateService.GetThemeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static string GetAutoDarkTheme()
|
||||
{
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
return stateService.GetAutoDarkThemeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static bool OsDarkModeEnabled()
|
||||
{
|
||||
if (Application.Current == null)
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Utilities
|
||||
{
|
||||
public class TimerTask
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly Action _action;
|
||||
private readonly CancellationTokenSource _cancellationToken;
|
||||
|
||||
public TimerTask(ILogger logger, Action action, CancellationTokenSource cancellationToken)
|
||||
{
|
||||
_logger = logger;
|
||||
_action = action ?? throw new ArgumentNullException();
|
||||
_cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public Task RunPeriodic(TimeSpan? interval = null)
|
||||
{
|
||||
interval = interval ?? TimeSpan.FromSeconds(1);
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!_cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Device.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
if (!_cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Exception(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
await Task.Delay(interval.Value, _cancellationToken.Token);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Exception(ex);
|
||||
}
|
||||
}, _cancellationToken.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Utilities
|
||||
{
|
||||
public class TotpHelper
|
||||
{
|
||||
private ITotpService _totpService;
|
||||
private CipherView _cipher;
|
||||
private int _interval;
|
||||
|
||||
public TotpHelper(CipherView cipher)
|
||||
{
|
||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
_cipher = cipher;
|
||||
_interval = _totpService.GetTimeInterval(cipher?.Login?.Totp);
|
||||
}
|
||||
|
||||
public string TotpSec { get; private set; }
|
||||
public string TotpCodeFormatted { get; private set; }
|
||||
public double Progress { get; private set; }
|
||||
|
||||
public async Task GenerateNewTotpValues()
|
||||
{
|
||||
var epoc = CoreHelpers.EpocUtcNow() / 1000;
|
||||
var mod = epoc % _interval;
|
||||
var totpSec = _interval - mod;
|
||||
TotpSec = totpSec.ToString();
|
||||
Progress = totpSec * 100 / 30;
|
||||
if (mod == 0 || string.IsNullOrEmpty(TotpCodeFormatted))
|
||||
{
|
||||
TotpCodeFormatted = await TotpUpdateCodeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> TotpUpdateCodeAsync()
|
||||
{
|
||||
var totpCode = await _totpService.GetCodeAsync(_cipher?.Login?.Totp);
|
||||
if (totpCode == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (totpCode.Length <= 4)
|
||||
{
|
||||
return totpCode;
|
||||
}
|
||||
|
||||
var half = (int)Math.Floor(totpCode.Length / 2M);
|
||||
return string.Format("{0} {1}", totpCode.Substring(0, half),
|
||||
totpCode.Substring(half));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IBroadcasterService
|
||||
{
|
||||
void Send(Message message);
|
||||
void Send(Message message, string id);
|
||||
void Send(Message message, string id = null);
|
||||
void Subscribe(string id, Action<Message> messageCallback);
|
||||
void Unsubscribe(string id);
|
||||
}
|
||||
|
||||
@@ -110,8 +110,6 @@ namespace Bit.Core.Abstractions
|
||||
Task SetRememberedOrgIdentifierAsync(string value);
|
||||
Task<string> GetThemeAsync(string userId = null);
|
||||
Task SetThemeAsync(string value, string userId = null);
|
||||
Task<string> GetAutoDarkThemeAsync(string userId = null);
|
||||
Task SetAutoDarkThemeAsync(string value, string userId = null);
|
||||
Task<bool?> GetAddSitePromptShownAsync(string userId = null);
|
||||
Task SetAddSitePromptShownAsync(bool? value, string userId = null);
|
||||
Task<bool?> GetPushInitialPromptShownAsync();
|
||||
@@ -148,8 +146,6 @@ namespace Bit.Core.Abstractions
|
||||
Task SetRefreshTokenAsync(string value, bool skipTokenStorage, string userId = null);
|
||||
Task<string> GetTwoFactorTokenAsync(string email = null);
|
||||
Task SetTwoFactorTokenAsync(string value, string email = null);
|
||||
Task<bool> GetScreenCaptureAllowedAsync(string userId = null);
|
||||
Task SetScreenCaptureAllowedAsync(bool value, string userId = null);
|
||||
Task SaveExtensionActiveUserIdToStorageAsync(string userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ namespace Bit.Core.Abstractions
|
||||
{
|
||||
Task<string> GetCodeAsync(string key);
|
||||
int GetTimeInterval(string key);
|
||||
Task<bool> IsAutoCopyEnabledAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
public static string DisableFaviconKey(string userId) => $"disableFavicon_{userId}";
|
||||
public static string DefaultUriMatchKey(string userId) => $"defaultUriMatch_{userId}";
|
||||
public static string ThemeKey(string userId) => $"theme_{userId}";
|
||||
public static string AutoDarkThemeKey(string userId) => $"autoDarkTheme_{userId}";
|
||||
public static string DisableAutoTotpCopyKey(string userId) => $"disableAutoTotpCopy_{userId}";
|
||||
public static string PreviousPageKey(string userId) => $"previousPage_{userId}";
|
||||
public static string PasswordRepromptAutofillKey(string userId) => $"passwordRepromptAutofillKey_{userId}";
|
||||
|
||||
@@ -94,13 +94,11 @@ namespace Bit.Core.Models.Domain
|
||||
EnvironmentUrls = copy.EnvironmentUrls;
|
||||
VaultTimeout = copy.VaultTimeout;
|
||||
VaultTimeoutAction = copy.VaultTimeoutAction;
|
||||
ScreenCaptureAllowed = copy.ScreenCaptureAllowed;
|
||||
}
|
||||
|
||||
public EnvironmentUrlData EnvironmentUrls;
|
||||
public int? VaultTimeout;
|
||||
public VaultTimeoutAction? VaultTimeoutAction;
|
||||
public bool ScreenCaptureAllowed;
|
||||
}
|
||||
|
||||
public class AccountVolatileData
|
||||
|
||||
@@ -589,19 +589,7 @@ namespace Bit.Core.Services
|
||||
{
|
||||
requestMessage.Version = new Version(1, 0);
|
||||
requestMessage.Method = method;
|
||||
|
||||
if (!Uri.IsWellFormedUriString(ApiBaseUrl, UriKind.Absolute))
|
||||
{
|
||||
throw new ApiException(new ErrorResponse
|
||||
{
|
||||
StatusCode = HttpStatusCode.BadGateway,
|
||||
//Note: This message is hardcoded until AppResources.resx gets moved into Core.csproj
|
||||
Message = "One or more URLs saved in the Settings are incorrect. Please revise it and try to log in again."
|
||||
});
|
||||
}
|
||||
|
||||
requestMessage.RequestUri = new Uri(string.Concat(ApiBaseUrl, path));
|
||||
|
||||
if (body != null)
|
||||
{
|
||||
var bodyType = body.GetType();
|
||||
|
||||
@@ -6,7 +6,6 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@@ -174,7 +173,7 @@ namespace Bit.Core.Services
|
||||
public void LogOut(Action callback)
|
||||
{
|
||||
callback.Invoke();
|
||||
_messagingService.Send(AccountsManagerMessageCommands.LOGGED_OUT);
|
||||
_messagingService.Send("loggedOut");
|
||||
}
|
||||
|
||||
public List<TwoFactorProvider> GetSupportedTwoFactorProviders()
|
||||
|
||||
@@ -8,58 +8,24 @@ namespace Bit.App.Services
|
||||
{
|
||||
public class BroadcasterService : IBroadcasterService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly Dictionary<string, Action<Message>> _subscribers = new Dictionary<string, Action<Message>>();
|
||||
private object _myLock = new object();
|
||||
|
||||
public BroadcasterService(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Send(Message message)
|
||||
public void Send(Message message, string id = null)
|
||||
{
|
||||
lock (_myLock)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
if (_subscribers.ContainsKey(id))
|
||||
{
|
||||
Task.Run(() => _subscribers[id].Invoke(message));
|
||||
}
|
||||
return;
|
||||
}
|
||||
foreach (var sub in _subscribers)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
sub.Value(message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(Message message, string id)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_myLock)
|
||||
{
|
||||
if (_subscribers.TryGetValue(id, out var action))
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action(message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
});
|
||||
Task.Run(() => sub.Value.Invoke(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,7 +34,14 @@ namespace Bit.App.Services
|
||||
{
|
||||
lock (_myLock)
|
||||
{
|
||||
_subscribers[id] = messageCallback;
|
||||
if (_subscribers.ContainsKey(id))
|
||||
{
|
||||
_subscribers[id] = messageCallback;
|
||||
}
|
||||
else
|
||||
{
|
||||
_subscribers.Add(id, messageCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ namespace Bit.Core.Services
|
||||
EnvironmentUrls = environmentUrls,
|
||||
VaultTimeout = vaultTimeout,
|
||||
VaultTimeoutAction =
|
||||
vaultTimeoutAction == "logout" ? VaultTimeoutAction.Logout : VaultTimeoutAction.Lock
|
||||
vaultTimeoutAction == "logout" ? VaultTimeoutAction.Logout : VaultTimeoutAction.Lock,
|
||||
};
|
||||
var state = new State { Accounts = new Dictionary<string, Account> { [userId] = account } };
|
||||
state.ActiveUserId = userId;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
@@ -559,27 +558,6 @@ namespace Bit.Core.Services
|
||||
await SaveAccountAsync(account, reconciledOptions);
|
||||
}
|
||||
|
||||
public async Task<bool> GetScreenCaptureAllowedAsync(string userId = null)
|
||||
{
|
||||
if (CoreHelpers.ForceScreenCaptureEnabled())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return (await GetAccountAsync(
|
||||
ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync())
|
||||
))?.Settings?.ScreenCaptureAllowed ?? false;
|
||||
}
|
||||
|
||||
public async Task SetScreenCaptureAllowedAsync(bool value, string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
await GetDefaultStorageOptionsAsync());
|
||||
var account = await GetAccountAsync(reconciledOptions);
|
||||
account.Settings.ScreenCaptureAllowed = value;
|
||||
await SaveAccountAsync(account, reconciledOptions);
|
||||
}
|
||||
|
||||
public async Task<DateTime?> GetLastFileCacheClearAsync()
|
||||
{
|
||||
var options = await GetDefaultStorageOptionsAsync();
|
||||
@@ -946,25 +924,6 @@ namespace Bit.Core.Services
|
||||
SetValueGloballyAsync(Constants.ThemeKey, value, reconciledOptions).FireAndForget();
|
||||
}
|
||||
|
||||
public async Task<string> GetAutoDarkThemeAsync(string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
await GetDefaultStorageOptionsAsync());
|
||||
var key = Constants.AutoDarkThemeKey(reconciledOptions.UserId);
|
||||
return await GetValueAsync<string>(key, reconciledOptions);
|
||||
}
|
||||
|
||||
public async Task SetAutoDarkThemeAsync(string value, string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
await GetDefaultStorageOptionsAsync());
|
||||
var key = Constants.AutoDarkThemeKey(reconciledOptions.UserId);
|
||||
await SetValueAsync(key, value, reconciledOptions);
|
||||
|
||||
// TODO remove this to restore per-account Theme support
|
||||
SetValueGloballyAsync(Constants.AutoDarkThemeKey, value, reconciledOptions).FireAndForget();
|
||||
}
|
||||
|
||||
public async Task<bool?> GetAddSitePromptShownAsync(string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
@@ -1455,7 +1414,6 @@ namespace Bit.Core.Services
|
||||
await SetPasswordVerifiedAutofillAsync(null, userId);
|
||||
await SetSyncOnRefreshAsync(null, userId);
|
||||
await SetThemeAsync(null, userId);
|
||||
await SetAutoDarkThemeAsync(null, userId);
|
||||
await SetAddSitePromptShownAsync(null, userId);
|
||||
await SetPasswordGenerationOptionsAsync(null, userId);
|
||||
}
|
||||
@@ -1465,7 +1423,6 @@ namespace Bit.Core.Services
|
||||
{
|
||||
await CheckStateAsync();
|
||||
var currentTheme = await GetThemeAsync();
|
||||
var currentAutoDarkTheme = await GetAutoDarkThemeAsync();
|
||||
var currentDisableFavicons = await GetDisableFaviconAsync();
|
||||
|
||||
account.Settings.EnvironmentUrls = await GetPreAuthEnvironmentUrlsAsync();
|
||||
@@ -1483,7 +1440,6 @@ namespace Bit.Core.Services
|
||||
var existingAccount = state.Accounts[account.Profile.UserId];
|
||||
account.Settings.VaultTimeout = existingAccount.Settings.VaultTimeout;
|
||||
account.Settings.VaultTimeoutAction = existingAccount.Settings.VaultTimeoutAction;
|
||||
account.Settings.ScreenCaptureAllowed = existingAccount.Settings.ScreenCaptureAllowed;
|
||||
}
|
||||
|
||||
// New account defaults
|
||||
@@ -1496,7 +1452,6 @@ namespace Bit.Core.Services
|
||||
account.Settings.VaultTimeoutAction = VaultTimeoutAction.Lock;
|
||||
}
|
||||
await SetThemeAsync(currentTheme, account.Profile.UserId);
|
||||
await SetAutoDarkThemeAsync(currentAutoDarkTheme, account.Profile.UserId);
|
||||
await SetDisableFaviconAsync(currentDisableFavicons, account.Profile.UserId);
|
||||
|
||||
state.Accounts[account.Profile.UserId] = account;
|
||||
|
||||
@@ -10,11 +10,14 @@ namespace Bit.Core.Services
|
||||
{
|
||||
private const string SteamChars = "23456789BCDFGHJKMNPQRTVWXY";
|
||||
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
|
||||
public TotpService(
|
||||
IStateService stateService,
|
||||
ICryptoFunctionService cryptoFunctionService)
|
||||
{
|
||||
_stateService = stateService;
|
||||
_cryptoFunctionService = cryptoFunctionService;
|
||||
}
|
||||
|
||||
@@ -129,5 +132,11 @@ namespace Bit.Core.Services
|
||||
}
|
||||
return period;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAutoCopyEnabledAsync()
|
||||
{
|
||||
var disabled = await _stateService.GetDisableAutoTotpCopyAsync();
|
||||
return !disabled.GetValueOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,25 +34,6 @@ namespace Bit.Core.Utilities
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether to force enabling the screen capture.
|
||||
/// On Debug it will allow screen capture by default but this method
|
||||
/// makes it easier to test the change on enabling/disabling the feature
|
||||
/// on debug.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To test enabling/disabling in DEBUG, just return <c>false</c> in the #if condition
|
||||
/// and that's it.
|
||||
/// </remarks>
|
||||
public static bool ForceScreenCaptureEnabled()
|
||||
{
|
||||
#if DEBUG
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static string GetHostname(string uriString)
|
||||
{
|
||||
var uri = GetUri(uriString);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
@@ -10,7 +8,7 @@ namespace Bit.Core.Utilities
|
||||
{
|
||||
public static class ServiceContainer
|
||||
{
|
||||
public static ConcurrentDictionary<string, object> RegisteredServices { get; set; } = new ConcurrentDictionary<string, object>();
|
||||
public static Dictionary<string, object> RegisteredServices { get; set; } = new Dictionary<string, object>();
|
||||
public static bool Inited { get; set; }
|
||||
|
||||
public static void Init(string customUserAgent = null, string clearCipherCacheKey = null,
|
||||
@@ -74,7 +72,7 @@ namespace Bit.Core.Utilities
|
||||
});
|
||||
var passwordGenerationService = new PasswordGenerationService(cryptoService, stateService,
|
||||
cryptoFunctionService, policyService);
|
||||
var totpService = new TotpService(cryptoFunctionService);
|
||||
var totpService = new TotpService(stateService, cryptoFunctionService);
|
||||
var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService,
|
||||
tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService,
|
||||
keyConnectorService);
|
||||
@@ -111,17 +109,18 @@ namespace Bit.Core.Utilities
|
||||
|
||||
public static void Register<T>(string serviceName, T obj)
|
||||
{
|
||||
if (!RegisteredServices.TryAdd(serviceName, obj))
|
||||
if (RegisteredServices.ContainsKey(serviceName))
|
||||
{
|
||||
throw new Exception($"Service {serviceName} has already been registered.");
|
||||
}
|
||||
RegisteredServices.Add(serviceName, obj);
|
||||
}
|
||||
|
||||
public static T Resolve<T>(string serviceName, bool dontThrow = false)
|
||||
{
|
||||
if (RegisteredServices.TryGetValue(serviceName, out var service))
|
||||
if (RegisteredServices.ContainsKey(serviceName))
|
||||
{
|
||||
return (T)service;
|
||||
return (T)RegisteredServices[serviceName];
|
||||
}
|
||||
if (dontThrow)
|
||||
{
|
||||
@@ -130,59 +129,6 @@ namespace Bit.Core.Utilities
|
||||
throw new Exception($"Service {serviceName} is not registered.");
|
||||
}
|
||||
|
||||
public static void Register<T>(T obj)
|
||||
where T : class
|
||||
{
|
||||
Register(typeof(T), obj);
|
||||
}
|
||||
|
||||
public static void Register(Type type, object obj)
|
||||
{
|
||||
var serviceName = GetServiceRegistrationName(type);
|
||||
if (!RegisteredServices.TryAdd(serviceName, obj))
|
||||
{
|
||||
throw new Exception($"Service {serviceName} has already been registered.");
|
||||
}
|
||||
}
|
||||
|
||||
public static T Resolve<T>()
|
||||
where T : class
|
||||
{
|
||||
return (T)Resolve(typeof(T));
|
||||
}
|
||||
|
||||
public static object Resolve(Type type)
|
||||
{
|
||||
var serviceName = GetServiceRegistrationName(type);
|
||||
if (RegisteredServices.TryGetValue(serviceName, out var service))
|
||||
{
|
||||
return service;
|
||||
}
|
||||
throw new Exception($"Service {serviceName} is not registered.");
|
||||
}
|
||||
|
||||
public static bool TryResolve<T>(out T service)
|
||||
where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
var toReturn = TryResolve(typeof(T), out var serviceObj);
|
||||
service = (T)serviceObj;
|
||||
return toReturn;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
service = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryResolve(Type type, out object service)
|
||||
{
|
||||
var serviceName = GetServiceRegistrationName(type);
|
||||
return RegisteredServices.TryGetValue(serviceName, out service);
|
||||
}
|
||||
|
||||
public static void Reset()
|
||||
{
|
||||
foreach (var service in RegisteredServices)
|
||||
@@ -194,33 +140,7 @@ namespace Bit.Core.Utilities
|
||||
}
|
||||
Inited = false;
|
||||
RegisteredServices.Clear();
|
||||
RegisteredServices = new ConcurrentDictionary<string, object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service registration name
|
||||
/// </summary>
|
||||
/// <param name="type">Type of the service</param>
|
||||
/// <remarks>
|
||||
/// In order to work with already register/resolve we need to maintain the naming convention
|
||||
/// of camelCase without the first "I" on the services interfaces
|
||||
/// e.g. "ITokenService" -> "tokenService"
|
||||
/// </remarks>
|
||||
static string GetServiceRegistrationName(Type type)
|
||||
{
|
||||
var typeName = type.Name;
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var indexToLowerCase = 0;
|
||||
if (typeName[0] == 'I' && char.IsUpper(typeName[1]))
|
||||
{
|
||||
// if it's an interface then we ignore the first char
|
||||
// and lower case the 2nd one (index 1)
|
||||
indexToLowerCase = 1;
|
||||
}
|
||||
sb.Append(char.ToLower(typeName[indexToLowerCase]));
|
||||
sb.Append(typeName.Substring(++indexToLowerCase));
|
||||
return sb.ToString();
|
||||
RegisteredServices = new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,10 @@ using Bit.App.Utilities;
|
||||
using Bit.App.Utilities.AccountManagement;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.iOS.Autofill.Models;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Bit.iOS.Core.Views;
|
||||
using CoreFoundation;
|
||||
using CoreNFC;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
@@ -38,128 +36,88 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
public override void ViewDidLoad()
|
||||
{
|
||||
try
|
||||
InitApp();
|
||||
base.ViewDidLoad();
|
||||
Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png");
|
||||
View.BackgroundColor = ThemeHelpers.SplashBackgroundColor;
|
||||
_context = new Context
|
||||
{
|
||||
InitApp();
|
||||
base.ViewDidLoad();
|
||||
Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png");
|
||||
View.BackgroundColor = ThemeHelpers.SplashBackgroundColor;
|
||||
_context = new Context
|
||||
{
|
||||
ExtContext = ExtensionContext
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
}
|
||||
ExtContext = ExtensionContext
|
||||
};
|
||||
}
|
||||
|
||||
public override async void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers)
|
||||
{
|
||||
try
|
||||
InitAppIfNeeded();
|
||||
_context.ServiceIdentifiers = serviceIdentifiers;
|
||||
if (serviceIdentifiers.Length > 0)
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
_context.ServiceIdentifiers = serviceIdentifiers;
|
||||
if (serviceIdentifiers.Length > 0)
|
||||
var uri = serviceIdentifiers[0].Identifier;
|
||||
if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain)
|
||||
{
|
||||
var uri = serviceIdentifiers[0].Identifier;
|
||||
if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain)
|
||||
{
|
||||
uri = string.Concat("https://", uri);
|
||||
}
|
||||
_context.UrlString = uri;
|
||||
uri = string.Concat("https://", uri);
|
||||
}
|
||||
if (!await IsAuthed())
|
||||
_context.UrlString = uri;
|
||||
}
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
}
|
||||
else if (await IsLocked())
|
||||
{
|
||||
PerformSegue("lockPasswordSegue", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
|
||||
{
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
}
|
||||
else if (await IsLocked())
|
||||
{
|
||||
PerformSegue("lockPasswordSegue", this);
|
||||
PerformSegue("loginSearchSegue", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
|
||||
{
|
||||
PerformSegue("loginSearchSegue", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
PerformSegue("loginListSegue", this);
|
||||
}
|
||||
PerformSegue("loginListSegue", this);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity)
|
||||
{
|
||||
try
|
||||
InitAppIfNeeded();
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
||||
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
||||
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
||||
ExtensionContext.CancelRequest(err);
|
||||
return;
|
||||
}
|
||||
_context.CredentialIdentity = credentialIdentity;
|
||||
await ProvideCredentialAsync(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
||||
ExtensionContext.CancelRequest(err);
|
||||
return;
|
||||
}
|
||||
_context.CredentialIdentity = credentialIdentity;
|
||||
await ProvideCredentialAsync(false);
|
||||
}
|
||||
|
||||
public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity)
|
||||
{
|
||||
try
|
||||
InitAppIfNeeded();
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
return;
|
||||
}
|
||||
_context.CredentialIdentity = credentialIdentity;
|
||||
await CheckLockAsync(async () => await ProvideCredentialAsync());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
return;
|
||||
}
|
||||
_context.CredentialIdentity = credentialIdentity;
|
||||
CheckLock(async () => await ProvideCredentialAsync());
|
||||
}
|
||||
|
||||
public override async void PrepareInterfaceForExtensionConfiguration()
|
||||
{
|
||||
try
|
||||
InitAppIfNeeded();
|
||||
_context.Configuring = true;
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
_context.Configuring = true;
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
return;
|
||||
}
|
||||
await CheckLockAsync(() => PerformSegue("setupSegue", this));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
return;
|
||||
}
|
||||
CheckLock(() => PerformSegue("setupSegue", this));
|
||||
}
|
||||
|
||||
public void CompleteRequest(string id = null, string username = null,
|
||||
@@ -201,43 +159,34 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||
{
|
||||
try
|
||||
if (segue.DestinationViewController is UINavigationController navController)
|
||||
{
|
||||
if (segue.DestinationViewController is UINavigationController navController)
|
||||
if (navController.TopViewController is LoginListViewController listLoginController)
|
||||
{
|
||||
if (navController.TopViewController is LoginListViewController listLoginController)
|
||||
{
|
||||
listLoginController.Context = _context;
|
||||
listLoginController.CPViewController = this;
|
||||
segue.DestinationViewController.PresentationController.Delegate =
|
||||
new CustomPresentationControllerDelegate(listLoginController.DismissModalAction);
|
||||
}
|
||||
else if (navController.TopViewController is LoginSearchViewController listSearchController)
|
||||
{
|
||||
listSearchController.Context = _context;
|
||||
listSearchController.CPViewController = this;
|
||||
segue.DestinationViewController.PresentationController.Delegate =
|
||||
new CustomPresentationControllerDelegate(listSearchController.DismissModalAction);
|
||||
}
|
||||
else if (navController.TopViewController is LockPasswordViewController passwordViewController)
|
||||
{
|
||||
passwordViewController.CPViewController = this;
|
||||
segue.DestinationViewController.PresentationController.Delegate =
|
||||
new CustomPresentationControllerDelegate(passwordViewController.DismissModalAction);
|
||||
}
|
||||
else if (navController.TopViewController is SetupViewController setupViewController)
|
||||
{
|
||||
setupViewController.CPViewController = this;
|
||||
segue.DestinationViewController.PresentationController.Delegate =
|
||||
new CustomPresentationControllerDelegate(setupViewController.DismissModalAction);
|
||||
}
|
||||
listLoginController.Context = _context;
|
||||
listLoginController.CPViewController = this;
|
||||
segue.DestinationViewController.PresentationController.Delegate =
|
||||
new CustomPresentationControllerDelegate(listLoginController.DismissModalAction);
|
||||
}
|
||||
else if (navController.TopViewController is LoginSearchViewController listSearchController)
|
||||
{
|
||||
listSearchController.Context = _context;
|
||||
listSearchController.CPViewController = this;
|
||||
segue.DestinationViewController.PresentationController.Delegate =
|
||||
new CustomPresentationControllerDelegate(listSearchController.DismissModalAction);
|
||||
}
|
||||
else if (navController.TopViewController is LockPasswordViewController passwordViewController)
|
||||
{
|
||||
passwordViewController.CPViewController = this;
|
||||
segue.DestinationViewController.PresentationController.Delegate =
|
||||
new CustomPresentationControllerDelegate(passwordViewController.DismissModalAction);
|
||||
}
|
||||
else if (navController.TopViewController is SetupViewController setupViewController)
|
||||
{
|
||||
setupViewController.CPViewController = this;
|
||||
segue.DestinationViewController.PresentationController.Delegate =
|
||||
new CustomPresentationControllerDelegate(setupViewController.DismissModalAction);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,109 +194,93 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
DismissViewController(false, async () =>
|
||||
{
|
||||
try
|
||||
if (_context.CredentialIdentity != null)
|
||||
{
|
||||
if (_context.CredentialIdentity != null)
|
||||
{
|
||||
await ProvideCredentialAsync();
|
||||
return;
|
||||
}
|
||||
if (_context.Configuring)
|
||||
{
|
||||
PerformSegue("setupSegue", this);
|
||||
return;
|
||||
}
|
||||
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
|
||||
{
|
||||
PerformSegue("loginSearchSegue", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
PerformSegue("loginListSegue", this);
|
||||
}
|
||||
await ProvideCredentialAsync();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
if (_context.Configuring)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
PerformSegue("setupSegue", this);
|
||||
return;
|
||||
}
|
||||
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
|
||||
{
|
||||
PerformSegue("loginSearchSegue", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
PerformSegue("loginListSegue", this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ProvideCredentialAsync(bool userInteraction = true)
|
||||
{
|
||||
try
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService", true);
|
||||
Bit.Core.Models.Domain.Cipher cipher = null;
|
||||
var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null;
|
||||
if (!cancel)
|
||||
{
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService", true);
|
||||
Bit.Core.Models.Domain.Cipher cipher = null;
|
||||
var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null;
|
||||
if (!cancel)
|
||||
{
|
||||
cipher = await cipherService.GetAsync(_context.CredentialIdentity.RecordIdentifier);
|
||||
cancel = cipher == null || cipher.Type != Bit.Core.Enums.CipherType.Login || cipher.Login == null;
|
||||
}
|
||||
if (cancel)
|
||||
cipher = await cipherService.GetAsync(_context.CredentialIdentity.RecordIdentifier);
|
||||
cancel = cipher == null || cipher.Type != Bit.Core.Enums.CipherType.Login || cipher.Login == null;
|
||||
}
|
||||
if (cancel)
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var decCipher = await cipher.DecryptAsync();
|
||||
if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None)
|
||||
{
|
||||
// Prompt for password using either the lock screen or dialog unless
|
||||
// already verified the password.
|
||||
if (!userInteraction)
|
||||
{
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(true);
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null);
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var decCipher = await cipher.DecryptAsync();
|
||||
if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None)
|
||||
else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
|
||||
{
|
||||
// Prompt for password using either the lock screen or dialog unless
|
||||
// already verified the password.
|
||||
if (!userInteraction)
|
||||
// Add a timeout to resolve keyboard not always showing up.
|
||||
await Task.Delay(250);
|
||||
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
if (!await passwordRepromptService.ShowPasswordPromptAsync())
|
||||
{
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(true);
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
return;
|
||||
}
|
||||
else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
|
||||
{
|
||||
// Add a timeout to resolve keyboard not always showing up.
|
||||
await Task.Delay(250);
|
||||
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
if (!await passwordRepromptService.ShowPasswordPromptAsync())
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
string totpCode = null;
|
||||
var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync();
|
||||
if (!disableTotpCopy.GetValueOrDefault(false))
|
||||
{
|
||||
var canAccessPremiumAsync = await _stateService.Value.CanAccessPremiumAsync();
|
||||
if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) &&
|
||||
(canAccessPremiumAsync || cipher.OrganizationUseTotp))
|
||||
{
|
||||
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp);
|
||||
}
|
||||
}
|
||||
|
||||
CompleteRequest(decCipher.Id, decCipher.Login.Username, decCipher.Login.Password, totpCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
string totpCode = null;
|
||||
var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync();
|
||||
if (!disableTotpCopy.GetValueOrDefault(false))
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
var canAccessPremiumAsync = await _stateService.Value.CanAccessPremiumAsync();
|
||||
if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) &&
|
||||
(canAccessPremiumAsync || cipher.OrganizationUseTotp))
|
||||
{
|
||||
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp);
|
||||
}
|
||||
}
|
||||
|
||||
CompleteRequest(decCipher.Id, decCipher.Login.Username, decCipher.Login.Password, totpCode);
|
||||
}
|
||||
|
||||
private async Task CheckLockAsync(Action notLockedAction)
|
||||
private async void CheckLock(Action notLockedAction)
|
||||
{
|
||||
if (await IsLocked() || await _stateService.Value.GetPasswordRepromptAutofillAsync())
|
||||
{
|
||||
DispatchQueue.MainQueue.DispatchAsync(() => PerformSegue("lockPasswordSegue", this));
|
||||
PerformSegue("lockPasswordSegue", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -370,21 +303,15 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
NSRunLoop.Main.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
try
|
||||
if (await IsAuthed())
|
||||
{
|
||||
if (await IsAuthed())
|
||||
await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync());
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
if (deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync());
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
}
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.8bit.bitwarden.autofill</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2022.6.2</string>
|
||||
<string>2022.6.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Pages;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Bit.iOS.Core.Views;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using Foundation;
|
||||
using Bit.iOS.Core.Views;
|
||||
using Bit.App.Resources;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.App.Pages;
|
||||
using Bit.App.Models;
|
||||
using Xamarin.Forms;
|
||||
using Bit.Core;
|
||||
|
||||
namespace Bit.iOS.Core.Controllers
|
||||
{
|
||||
@@ -28,7 +28,6 @@ namespace Bit.iOS.Core.Controllers
|
||||
private IPlatformUtilsService _platformUtilsService;
|
||||
private IBiometricService _biometricService;
|
||||
private IKeyConnectorService _keyConnectorService;
|
||||
private IAccountsManager _accountManager;
|
||||
private bool _isPinProtected;
|
||||
private bool _isPinProtectedWithKey;
|
||||
private bool _pinLock;
|
||||
@@ -40,10 +39,6 @@ namespace Bit.iOS.Core.Controllers
|
||||
|
||||
protected bool autofillExtension = false;
|
||||
|
||||
public BaseLockPasswordViewController()
|
||||
{
|
||||
}
|
||||
|
||||
public BaseLockPasswordViewController(IntPtr handle)
|
||||
: base(handle)
|
||||
{ }
|
||||
@@ -85,7 +80,7 @@ namespace Bit.iOS.Core.Controllers
|
||||
}
|
||||
|
||||
public abstract UITableView TableView { get; }
|
||||
|
||||
|
||||
public override async void ViewDidLoad()
|
||||
{
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
@@ -96,7 +91,6 @@ namespace Bit.iOS.Core.Controllers
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_accountManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||
|
||||
// We re-use the lock screen for autofill extension to verify master password
|
||||
// when trying to access protected items.
|
||||
@@ -174,12 +168,13 @@ namespace Bit.iOS.Core.Controllers
|
||||
{
|
||||
TableView.BackgroundColor = ThemeHelpers.BackgroundColor;
|
||||
TableView.SeparatorColor = ThemeHelpers.SeparatorColor;
|
||||
TableView.RowHeight = UITableView.AutomaticDimension;
|
||||
TableView.EstimatedRowHeight = 70;
|
||||
TableView.Source = new TableSource(this);
|
||||
TableView.AllowsSelection = true;
|
||||
}
|
||||
|
||||
TableView.RowHeight = UITableView.AutomaticDimension;
|
||||
TableView.EstimatedRowHeight = 70;
|
||||
TableView.Source = new TableSource(this);
|
||||
TableView.AllowsSelection = true;
|
||||
|
||||
base.ViewDidLoad();
|
||||
|
||||
if (_biometricLock)
|
||||
@@ -196,7 +191,7 @@ namespace Bit.iOS.Core.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
public override void ViewDidAppear(bool animated)
|
||||
public override async void ViewDidAppear(bool animated)
|
||||
{
|
||||
base.ViewDidAppear(animated);
|
||||
|
||||
@@ -267,7 +262,13 @@ namespace Bit.iOS.Core.Controllers
|
||||
}
|
||||
if (failed)
|
||||
{
|
||||
await HandleFailedCredentialsAsync();
|
||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||
if (invalidUnlockAttempts >= 5)
|
||||
{
|
||||
await LogOutAsync();
|
||||
return;
|
||||
}
|
||||
InvalidValue();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -302,22 +303,17 @@ namespace Bit.iOS.Core.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleFailedCredentialsAsync();
|
||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||
if (invalidUnlockAttempts >= 5)
|
||||
{
|
||||
await LogOutAsync();
|
||||
return;
|
||||
}
|
||||
InvalidValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleFailedCredentialsAsync()
|
||||
{
|
||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||
if (invalidUnlockAttempts >= 5)
|
||||
{
|
||||
await _accountManager.LogOutAsync(await _stateService.GetActiveUserIdAsync(), false, false);
|
||||
return;
|
||||
}
|
||||
InvalidValue();
|
||||
}
|
||||
|
||||
public async Task PromptBiometricAsync()
|
||||
{
|
||||
if (!_biometricLock || !_biometricIntegrityValid)
|
||||
@@ -396,43 +392,38 @@ namespace Bit.iOS.Core.Controllers
|
||||
PresentViewController(alert, true, null);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
private async Task LogOutAsync()
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
MasterPasswordCell?.Dispose();
|
||||
MasterPasswordCell = null;
|
||||
|
||||
TableView?.Dispose();
|
||||
await AppHelpers.LogOutAsync(await _stateService.GetActiveUserIdAsync());
|
||||
var authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||
authService.LogOut(() =>
|
||||
{
|
||||
Cancel?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
public class TableSource : ExtendedUITableViewSource
|
||||
{
|
||||
private readonly WeakReference<BaseLockPasswordViewController> _controller;
|
||||
private readonly BaseLockPasswordViewController _controller;
|
||||
|
||||
public TableSource(BaseLockPasswordViewController controller)
|
||||
{
|
||||
_controller = new WeakReference<BaseLockPasswordViewController>(controller);
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
if (!_controller.TryGetTarget(out var controller))
|
||||
{
|
||||
return new ExtendedUITableViewCell();
|
||||
}
|
||||
|
||||
if (indexPath.Section == 0)
|
||||
{
|
||||
if (indexPath.Row == 0)
|
||||
{
|
||||
if (controller._biometricUnlockOnly)
|
||||
if (_controller._biometricUnlockOnly)
|
||||
{
|
||||
return controller.BiometricCell;
|
||||
return _controller.BiometricCell;
|
||||
}
|
||||
else
|
||||
{
|
||||
return controller.MasterPasswordCell;
|
||||
return _controller.MasterPasswordCell;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -440,7 +431,7 @@ namespace Bit.iOS.Core.Controllers
|
||||
{
|
||||
if (indexPath.Row == 0)
|
||||
{
|
||||
if (controller._passwordReprompt)
|
||||
if (_controller._passwordReprompt)
|
||||
{
|
||||
var cell = new ExtendedUITableViewCell();
|
||||
cell.TextLabel.TextColor = ThemeHelpers.DangerColor;
|
||||
@@ -450,9 +441,9 @@ namespace Bit.iOS.Core.Controllers
|
||||
cell.TextLabel.Text = AppResources.PasswordConfirmationDesc;
|
||||
return cell;
|
||||
}
|
||||
else if (!controller._biometricUnlockOnly)
|
||||
else if (!_controller._biometricUnlockOnly)
|
||||
{
|
||||
return controller.BiometricCell;
|
||||
return _controller.BiometricCell;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,13 +457,8 @@ namespace Bit.iOS.Core.Controllers
|
||||
|
||||
public override nint NumberOfSections(UITableView tableView)
|
||||
{
|
||||
if (!_controller.TryGetTarget(out var controller))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (!controller._biometricUnlockOnly && controller._biometricLock) ||
|
||||
controller._passwordReprompt
|
||||
return (!_controller._biometricUnlockOnly && _controller._biometricLock) ||
|
||||
_controller._passwordReprompt
|
||||
? 2
|
||||
: 1;
|
||||
}
|
||||
@@ -498,18 +484,13 @@ namespace Bit.iOS.Core.Controllers
|
||||
|
||||
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
if (!_controller.TryGetTarget(out var controller))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tableView.DeselectRow(indexPath, true);
|
||||
tableView.EndEditing(true);
|
||||
if (indexPath.Row == 0 &&
|
||||
((controller._biometricUnlockOnly && indexPath.Section == 0) ||
|
||||
((_controller._biometricUnlockOnly && indexPath.Section == 0) ||
|
||||
indexPath.Section == 1))
|
||||
{
|
||||
var task = controller.PromptBiometricAsync();
|
||||
var task = _controller.PromptBiometricAsync();
|
||||
return;
|
||||
}
|
||||
var cell = tableView.CellAt(indexPath);
|
||||
|
||||
@@ -7,11 +7,7 @@ namespace Bit.iOS.Core.Controllers
|
||||
public class ExtendedUIViewController : UIViewController
|
||||
{
|
||||
public Action DismissModalAction { get; set; }
|
||||
|
||||
public ExtendedUIViewController()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public ExtendedUIViewController(IntPtr handle)
|
||||
: base(handle)
|
||||
{
|
||||
@@ -32,28 +28,16 @@ namespace Bit.iOS.Core.Controllers
|
||||
{
|
||||
View.BackgroundColor = ThemeHelpers.BackgroundColor;
|
||||
}
|
||||
UpdateNavigationBarTheme();
|
||||
}
|
||||
|
||||
protected virtual void UpdateNavigationBarTheme()
|
||||
{
|
||||
UpdateNavigationBarTheme(NavigationController?.NavigationBar);
|
||||
}
|
||||
|
||||
protected void UpdateNavigationBarTheme(UINavigationBar navBar)
|
||||
{
|
||||
if (navBar is null)
|
||||
if (NavigationController?.NavigationBar != null)
|
||||
{
|
||||
return;
|
||||
NavigationController.NavigationBar.BarTintColor = ThemeHelpers.NavBarBackgroundColor;
|
||||
NavigationController.NavigationBar.BackgroundColor = ThemeHelpers.NavBarBackgroundColor;
|
||||
NavigationController.NavigationBar.TintColor = ThemeHelpers.NavBarTextColor;
|
||||
NavigationController.NavigationBar.TitleTextAttributes = new UIStringAttributes
|
||||
{
|
||||
ForegroundColor = ThemeHelpers.NavBarTextColor
|
||||
};
|
||||
}
|
||||
|
||||
navBar.BarTintColor = ThemeHelpers.NavBarBackgroundColor;
|
||||
navBar.BackgroundColor = ThemeHelpers.NavBarBackgroundColor;
|
||||
navBar.TintColor = ThemeHelpers.NavBarTextColor;
|
||||
navBar.TitleTextAttributes = new UIStringAttributes
|
||||
{
|
||||
ForegroundColor = ThemeHelpers.NavBarTextColor
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ namespace Bit.iOS.Core.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
public override void ViewDidAppear(bool animated)
|
||||
public override async void ViewDidAppear(bool animated)
|
||||
{
|
||||
base.ViewDidAppear(animated);
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
namespace Bit.iOS.Core.Renderers.CollectionView
|
||||
{
|
||||
public class CollectionException : Exception
|
||||
{
|
||||
public CollectionException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public CollectionException(string message, Exception innerEx)
|
||||
: base(message, innerEx)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Services;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using Xamarin.Forms.Platform.iOS;
|
||||
|
||||
namespace Bit.iOS.Core.Renderers.CollectionView
|
||||
@@ -15,11 +13,6 @@ namespace Bit.iOS.Core.Renderers.CollectionView
|
||||
{
|
||||
}
|
||||
|
||||
protected override UICollectionViewDelegateFlowLayout CreateDelegator()
|
||||
{
|
||||
return new ExtendedGroupableItemsViewDelegator<TItemsView, ExtendedGroupableItemsViewController<TItemsView>>(ItemsViewLayout, this);
|
||||
}
|
||||
|
||||
protected override void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath indexPath)
|
||||
{
|
||||
try
|
||||
@@ -28,17 +21,7 @@ namespace Bit.iOS.Core.Renderers.CollectionView
|
||||
}
|
||||
catch (Exception ex) when (ItemsView?.ExtraDataForLogging != null)
|
||||
{
|
||||
var colEx = new CollectionException("Error in ExtendedCollectionView -> ExtendedGroupableItemsViewController, extra data: " + ItemsView.ExtraDataForLogging, ex);
|
||||
try
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(colEx);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing in here, this is temporary to get more info about the crash, if the logger fails, we want to get the info
|
||||
// by crashing with the original exception and not the logger one
|
||||
}
|
||||
throw colEx;
|
||||
throw new Exception("Error in ExtendedCollectionView, extra data: " + ItemsView.ExtraDataForLogging, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Services;
|
||||
using CoreGraphics;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using Xamarin.Forms.Platform.iOS;
|
||||
|
||||
namespace Bit.iOS.Core.Renderers.CollectionView
|
||||
{
|
||||
public class ExtendedGroupableItemsViewDelegator<TItemsView, TViewController> : GroupableItemsViewDelegator<TItemsView, TViewController>
|
||||
where TItemsView : ExtendedCollectionView
|
||||
where TViewController : GroupableItemsViewController<TItemsView>
|
||||
{
|
||||
public ExtendedGroupableItemsViewDelegator(ItemsViewLayout itemsViewLayout, TViewController itemsViewController)
|
||||
: base(itemsViewLayout, itemsViewController)
|
||||
{
|
||||
}
|
||||
|
||||
public override CGSize GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath)
|
||||
{
|
||||
// Added this to get extra information on a crash when getting the size for an item.
|
||||
try
|
||||
{
|
||||
return base.GetSizeForItem(collectionView, layout, indexPath);
|
||||
}
|
||||
catch (Exception ex) when (ViewController?.ItemsView?.ExtraDataForLogging != null)
|
||||
{
|
||||
var colEx = new CollectionException("Error in ExtendedCollectionView -> ExtendedGroupableItemsViewDelegator, extra data: " + ViewController.ItemsView.ExtraDataForLogging, ex);
|
||||
try
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(colEx);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing in here, this is temporary to get more info about the crash, if the logger fails, we want to get the info
|
||||
// by crashing with the original exception and not the logger one
|
||||
}
|
||||
throw colEx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace Bit.iOS.Core.Renderers
|
||||
public CustomTabbedRenderer()
|
||||
{
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_broadcasterService.Subscribe(nameof(CustomTabbedRenderer), (message) =>
|
||||
_broadcasterService.Subscribe(nameof(CustomTabbedRenderer), async (message) =>
|
||||
{
|
||||
if (message.Command == "updatedTheme")
|
||||
{
|
||||
|
||||
@@ -604,12 +604,6 @@ namespace Bit.iOS.Core.Services
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
}
|
||||
|
||||
public Task SetScreenCaptureAllowedAsync()
|
||||
{
|
||||
// only used by Android. Not possible in iOS
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public class PickerDelegate : UIDocumentPickerDelegate
|
||||
{
|
||||
private readonly DeviceActionService _deviceActionService;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -11,11 +10,9 @@ namespace Bit.iOS.Core.Utilities
|
||||
{
|
||||
public class AccountSwitchingOverlayHelper
|
||||
{
|
||||
const string DEFAULT_SYSTEM_AVATAR_IMAGE = "person.2";
|
||||
|
||||
readonly IStateService _stateService;
|
||||
readonly IMessagingService _messagingService;
|
||||
readonly ILogger _logger;
|
||||
IStateService _stateService;
|
||||
IMessagingService _messagingService;
|
||||
ILogger _logger;
|
||||
|
||||
public AccountSwitchingOverlayHelper()
|
||||
{
|
||||
@@ -26,24 +23,9 @@ namespace Bit.iOS.Core.Utilities
|
||||
|
||||
public async Task<UIImage> CreateAvatarImageAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_stateService is null)
|
||||
{
|
||||
throw new NullReferenceException(nameof(_stateService));
|
||||
}
|
||||
|
||||
var avatarImageSource = new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
|
||||
using (var avatarUIImage = await avatarImageSource.GetNativeImageAsync())
|
||||
{
|
||||
return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
return UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE);
|
||||
}
|
||||
var avatarImageSource = new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
|
||||
var avatarUIImage = await avatarImageSource.GetNativeImageAsync();
|
||||
return avatarUIImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
|
||||
}
|
||||
|
||||
public AccountSwitchingOverlayView CreateAccountSwitchingOverlayView(UIView containerView)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user