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

Compare commits

..

77 Commits

Author SHA1 Message Date
André Bispo
bcc260183e [PS-416] Fixed a bug where the item details page was not updating after saving. 2022-07-26 21:28:32 +01:00
André Bispo
2f2b90acee [SSG-416] PR fixes 2022-07-18 22:13:44 +01:00
André Bispo
1b82249d44 Merge branch 'master' into feature/totp-tab
# Conflicts:
#	src/App/Resources/AppResources.Designer.cs
#	src/App/Resources/AppResources.resx
2022-07-18 14:21:15 +01:00
Federico Maccaroni
5e61fb0a14 EC-325 fix format (#1995) 2022-07-15 17:35:21 +01:00
Pedro da Rocha Pires
cf222bd0c3 [EC-325] Settings option to allow screen capture on Android (#1914)
* settings option to allow screen capture on Android

* Improved code on Screen Capture and added prompt to the user to allow screen capture

* EC-325 Removed async on OnCreate of MainActivity given that's not necessary anymore

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2022-07-15 16:13:10 +01:00
Micaiah Martin
cb0c52fb26 Add publish options to release workflow (#1994) 2022-07-15 07:30:14 -06:00
Michał Chęciński
c07c305384 Add version change check in the version bump workflow (#1992) 2022-07-15 11:08:38 +02:00
Federico Maccaroni
d2fbf5bdea EC-312 Fix crash on entering invalid credentials five times on Autofill (#1988) 2022-07-14 23:17:04 +01:00
Federico Maccaroni
2d2a883b96 EC-306 Fix crash happening on vietnamise when trying to go to Password Autofill on iOS given that the string was the same as Autofill Services and the comparison was misleading. Also refactored so that the action is on each item instead of having to compare to act (#1989) 2022-07-14 23:04:13 +01:00
Federico Maccaroni
1f2fb3f796 [EC-324] Added more logging for information on list crash (#1993)
* EC-324 Added more logging for trying to get more information on list out of range crash on AppCenter

* EC-324 Fix include on iOS.Core.csproj on iOS CollectionView files
2022-07-14 22:54:45 +01:00
Federico Maccaroni
8f3a4b98a5 EC-323 sanitize data on get first letters for avatar image creation (#1990) 2022-07-14 21:33:30 +01:00
Donkeykong307
70cf7431f7 Opera GX Autofill Support (#1855)
Added Opera GX Support for autofill
2022-07-14 21:27:53 +01:00
vincentvidal
f2ba86a62b Add support for iodé Browser (#1886) 2022-07-14 21:24:02 +01:00
André Bispo
f432a58e9f [SSG-416] run dotnet tool run dotnet-format 2022-07-13 12:53:54 +01:00
André Bispo
7e6935f21c [SSG-416] Add to remove a11y text from totp toggle because on android it places an helper text next to the switch making it invisible. Also removed from the label because it already reads the text from the label 2022-07-13 11:13:50 +01:00
André Bispo
7cf34b845e [SSG-416] Mobile PR Fixes 2022-07-12 23:00:31 +01:00
Federico Maccaroni
292908f53f [EC-259] Added Account Switching to Share extension on iOS (#1971)
* EC-259 Added Account switching on share extension on iOS, also improved performance for this and exception handling

* EC-259 code formatting

* EC-259 Added account switching to Share extension Send view

* EC-259 Fixed navigation on share extension when a forms page is already presented

* EC-259 Fix send text UI update when going from the iOS extension

* EC-259 Improved DateTimeViewModel with helper property to easily setup date and time at the same time and applied on usage
2022-07-12 14:12:23 -03:00
Carlos Gonçalves
d621a5d2f3 [PS 920] Fix selfhosted url validations (#1967)
* PS-920 - Added feedback to user when saving bad formed URLs
* Added feedback to user when trying to perform login with bad formed URL

* PS-920 - Refactor to use AsyncCommand
*(missing file from previous commit)

* PS-920 - Fixed whitespace formatting

* PS-920 - Removed unused method

* PS-920 - Fixed validation
* Added comment for hard coded string

* PS-920 - Removed unused properties
* Fixed url validations
* Refactored method to local function

* PS-920 - Added exception handling and logging
* Added generic error message string to AppResources
2022-07-11 18:02:11 +01:00
Patrick H. Lauke
75e8276784 Use correct icon for checked/unchecked boolean (#1986)
Closes https://github.com/bitwarden/mobile/issues/1985
2022-07-11 10:48:19 -03:00
Andreas Coroiu
67f49a0591 [PS-686] Mobile update negative copy in settings (#1961)
* feat: update auto totp copy setting

* feat: update show icons settings

* feat: update auto add settings

* feat: update settings and options to sentence case

* feat: update translation keys

With the latest changes the translation keys had diverged from their contents.
This commit fixes that.

* fix: revert AndroidManifest changes

* chore: add todo comments to fix negative functions
2022-07-11 08:45:42 +02:00
Rui Tomé
cceded2a0f Updated the wording on the modal warning when deleting the account (#1982) 2022-07-08 09:28:23 +01:00
Federico Maccaroni
846d3a85a2 EC-308 Fix crash produced by creating avatar image on AccountSwitchingOverlayHelper and also added more logging to see when it happens. (#1983) 2022-07-07 20:24:29 +01:00
André Bispo
8ed909eb91 [SG-416] Updated scanner mode toggle text color to be inline with figma designs 2022-07-07 17:58:39 +01:00
André Bispo
a57dc50a50 Merge branch 'master' into feature/totp-tab
# Conflicts:
#	src/App/Resources/AppResources.Designer.cs
#	src/App/Resources/AppResources.resx
2022-07-07 17:13:04 +01:00
github-actions[bot]
7802da2b9c Bumped version to 2022.6.2 (#1981)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-07-07 09:07:08 -07:00
Rui Tomé
cd56a124d5 [EC-303] Add warning when vault timeout is set to "Never" (#1976)
* Added to AppResources.resx the message warning the user about setting the lockout option to "Never"

* Added the condition to check the newly selected option on the vault timeout settings

* Changed the wording on the warning as to reflect the mobile version

* Changed the vault timeout modal to have the ability to cancel

* Simplified the reversion of value if the user cancels the change
2022-07-07 16:35:58 +01:00
André Bispo
9074f533f9 Merge branch 'master' into feature/totp-tab 2022-07-07 15:33:08 +01:00
André Bispo
9499fa0fb3 [SSG-416] Updated ViewPageViewModel and code refactoring. 2022-07-07 15:27:44 +01:00
Matt Gibson
58a3662d0f Add user verification to reset password request (#1980)
We only need master password hash because this is currently
only used for sso password setting after auto-provisioning. Key
Connector is not involved in these accounts
2022-07-06 17:23:20 -05:00
André Bispo
69e47b651d [SSG-416] removed unnecessary using. 2022-07-06 19:50:09 +01:00
André Bispo
dbe72c68a8 [SSG-416] removed unnecessary changes to the TabsPage file 2022-07-06 19:36:12 +01:00
André Bispo
3822373b98 [SSG-416] Removed the dashes from free user and just left the Premium subscription required. 2022-07-06 18:00:05 +01:00
Ben Pearo
6c7413e38c replace link to mobile section of contributing documentation with working link (#1978) 2022-07-06 11:43:07 -03:00
André Bispo
a51c2efdae PS-70 changed premium required label. fixed bug when to show premium required label. 2022-06-29 11:29:29 +01:00
André Bispo
0ce01ddd84 PS-70 PR fix for AppResource vs code behind generation. 2022-06-28 12:51:45 +01:00
André Bispo
6b33266721 PS-70 removed update to premium uri launch 2022-06-24 19:50:21 +01:00
André Bispo
6f61f1f4d8 PS-70 code format 2022-06-24 19:31:43 +01:00
André Bispo
d3d935fff6 Merge branch 'master' into feature/totp-tab
# Conflicts:
#	src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs
#	src/App/Pages/Vault/ScanPage.xaml.cs
2022-06-24 17:39:24 +01:00
André Bispo
a97abc61d5 PS-70 removed shadow for newer versions of android 2022-06-24 16:17:25 +01:00
André Bispo
6e30ec2dc7 PS-70 fixed font clipping bug on iOS 2022-06-24 12:19:04 +01:00
André Bispo
472f251714 PS-70 Added frame styling for iOS, since frame view has different base configuration for android and iOS. 2022-06-24 11:57:57 +01:00
André Bispo
a6069ef37b PS-70 Fixed android button overlapping bug by adding button styling to a Frame view and placing a label inside. Fixed Color on scanner page. 2022-06-23 17:17:59 +01:00
André Bispo
6aeec4a89a PS-70 Added label on top of button to solve UI bug. 2022-06-23 11:31:50 +01:00
André Bispo
9ddbe400f8 PS-70 changed labels on manual scanner screen 2022-06-22 19:02:16 +01:00
André Bispo
9bc8e6912d PS-70 added copy button to totp edit cipher. Added row button when totp is null. 2022-06-22 19:01:23 +01:00
André Bispo
64beb23239 PS-70 fixed totp cell title label font 2022-06-22 16:12:22 +01:00
André Bispo
94f2c5a4f8 PS-70 added vibrate permission to manifest. refactored scanpage code. added manual authentication key feature in scanner. 2022-06-22 12:26:51 +01:00
André Bispo
3522953b8c PS-70 fixed scanner animation for android devices 2022-06-22 10:57:39 +01:00
André Bispo
3a91bc7eb1 PS-70 let zxing scanner camera feed on until screen is closed. 2022-06-22 10:13:58 +01:00
André Bispo
277dd5942f PS-70 Added scanner square corner overlay. Added scanning animation. Added scan success animation. 2022-06-21 22:28:13 +01:00
André Bispo
46c73dafbe PS-70 Splited totp code to adjust spacing. 2022-06-21 17:07:45 +01:00
André Bispo
c564a34317 PS-70 added new UI to enter code manually in the QR Code scanner screen. Changed existing labels on scanner screen. 2022-06-20 18:41:50 +01:00
André Bispo
e606ff88a4 PS-70 Renamed TOTP to Totp to have consistency in naming. Removed a11y text of switch because android was overlapping text. 2022-06-20 14:42:33 +01:00
André Bispo
67fbd55ae9 PS-70 added text labels to resource files 2022-06-20 12:39:53 +01:00
André Bispo
c67229d235 PS-70 show upgrade to premium text on details to free user. 2022-06-20 12:28:00 +01:00
André Bispo
e8d31b8a22 PS-70 add copy to clipboard. 2022-06-20 12:27:11 +01:00
André Bispo
3b763e454c PS-70 removed unnecessary code 2022-06-20 11:18:15 +01:00
André Bispo
c9c3e6d98d PS-70 show toggle only if it's premium 2022-06-20 11:17:20 +01:00
André Bispo
751fdf2db6 PS-70 Added new props to custom control. 2022-06-17 10:23:25 +01:00
André Bispo
6ae7d6dd8d PS-70 Fixed grid cell width. Added red progress at 20 percent. Refactored circular progress view. 2022-06-15 20:54:00 +01:00
André Bispo
a3218aed26 PS-70 Added circular progress to the OTP count down 2022-06-13 12:57:07 +01:00
André Bispo
77c8156515 fixed formatting 2022-06-07 19:06:26 +01:00
André Bispo
1c0375ef05 removed unnecessary code on vm 2022-06-07 18:59:17 +01:00
André Bispo
b3f2730c71 PS-70 removed old authentication tab 2022-06-07 18:44:07 +01:00
André Bispo
5e2142fba7 PS-70 Added toggle to quickly filter TOTP cypher items and show their details, Added new text resource 2022-06-07 18:38:48 +01:00
André Bispo
c91277bc43 Merge branch 'beeep-totp' into feature/totp-tab
# Conflicts:
#	src/App/Pages/Authenticator/AuthenticatorPage.xaml
#	src/App/Pages/Authenticator/AuthenticatorPage.xaml.cs
#	src/App/Pages/Authenticator/AuthenticatorPageViewModel.cs
2022-06-06 14:16:40 +01:00
Jacob Fink
a484ca633c Merge branch 'beeep-totp' of https://github.com/bitwarden/mobile into beeep-totp 2022-06-06 08:58:52 -04:00
André Bispo
31d1a2e083 enable context loading and vm init 2022-06-04 20:19:03 +01:00
André Bispo
655b51b6a5 Merge branch 'master' into beeep-totp
# Conflicts:
#	src/App/App.csproj
2022-06-04 18:05:14 +01:00
Jacob Fink
0b626cedc7 got the countdown working 2022-03-04 16:44:22 -05:00
Jacob Fink
3ac2580742 add toolbar icons 2022-02-17 11:26:14 -05:00
Jacob Fink
008ed8eb56 add authentication view cell 2022-02-16 15:55:02 -05:00
Jacob Fink
26e0e43bb4 Merge branch 'master' into beeep-totp 2022-02-16 09:25:29 -05:00
Jacob Fink
0ad992faec add tab page 2022-02-16 09:25:19 -05:00
Jacob Fink
98dd8298ea clear extra code and fix build 2022-02-15 14:15:42 -05:00
Jacob Fink
bb37bac620 Revert config files from previous commit
This reverts commit b02c58e362.
2022-02-15 13:36:32 -05:00
Carlos J. Muentes
b02c58e362 Initial commit of new TOTP page 2022-02-11 15:22:51 -05:00
99 changed files with 3681 additions and 1049 deletions

View File

@@ -13,6 +13,11 @@ on:
- Initial Release
- Redeploy
- Dry Run
fdroid_publish:
description: 'Publish to f-droid store'
required: true
default: true
type: boolean
jobs:
release:
@@ -78,6 +83,7 @@ jobs:
name: F-Droid Release
runs-on: ubuntu-20.04
needs: release
if: inputs.fdroid_publish
steps:
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0

View File

@@ -19,12 +19,6 @@ jobs:
- name: Create Version Branch
run: |
git switch -c version_bump_${{ github.event.inputs.version_number }}
git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Checkout Version Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
with:
ref: version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - Android XML
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
@@ -56,16 +50,32 @@ jobs:
version: ${{ github.event.inputs.version_number }}
file_path: "./src/iOS/Info.plist"
- name: Commit files
- name: Setup git
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
- name: Check if version changed
id: version-changed
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "::set-output name=changes_to_commit::TRUE"
else
echo "::set-output name=changes_to_commit::FALSE"
echo "No changes to commit!";
fi
- name: Commit files
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: |
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
- name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Create Version PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -12,7 +12,7 @@ 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.
Please refer to the [Mobile section](https://contributing.bitwarden.com/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
# We're Hiring!

View File

@@ -54,6 +54,7 @@ namespace Bit.Droid.Accessibility
new Browser("com.google.android.apps.chrome", "url_bar"),
new Browser("com.google.android.apps.chrome_dev", "url_bar"),
// Rem. for "com.google.android.captiveportallogin": URL displayed in ActionBar subtitle without viewId.
new Browser("com.iode.firefox", "mozac_browser_toolbar_url_view"),
new Browser("com.jamal2367.styx", "search"),
new Browser("com.kiwibrowser.browser", "url_bar"),
new Browser("com.kiwibrowser.browser.dev", "url_bar"),
@@ -67,6 +68,7 @@ namespace Bit.Droid.Accessibility
new Browser("com.naver.whale", "url_bar"),
new Browser("com.opera.browser", "url_field"),
new Browser("com.opera.browser.beta", "url_field"),
new Browser("com.opera.gx", "addressbarEdit"),
new Browser("com.opera.mini.native", "url_field"),
new Browser("com.opera.mini.native.beta", "url_field"),
new Browser("com.opera.touch", "addressbarEdit"),

View File

@@ -73,6 +73,7 @@ namespace Bit.Droid.Autofill
"com.google.android.apps.chrome",
"com.google.android.apps.chrome_dev",
"com.google.android.captiveportallogin",
"com.iode.firefox",
"com.jamal2367.styx",
"com.kiwibrowser.browser",
"com.kiwibrowser.browser.dev",
@@ -86,6 +87,7 @@ namespace Bit.Droid.Autofill
"com.naver.whale",
"com.opera.browser",
"com.opera.browser.beta",
"com.opera.gx",
"com.opera.mini.native",
"com.opera.mini.native.beta",
"com.opera.touch",

View File

@@ -64,10 +64,11 @@ namespace Bit.Droid
Intent?.Validate();
base.OnCreate(savedInstanceState);
if (!CoreHelpers.InDebugMode())
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget(_ =>
{
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
}
});
ServiceContainer.Resolve<ILogger>("logger").InitAsync();

View File

@@ -20,6 +20,7 @@ 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
@@ -69,7 +70,8 @@ namespace Bit.Droid
ServiceContainer.Resolve<IStorageService>("secureStorageService"),
ServiceContainer.Resolve<IStateService>("stateService"),
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService"));
ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
}
#if !FDROID
@@ -160,6 +162,7 @@ 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

View File

@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.6.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.6.2" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30"/>
@@ -12,7 +12,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>

View File

@@ -77,6 +77,9 @@
<compatibility-package
android:name="com.google.android.captiveportallogin"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.iode.firefox"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.jamal2367.styx"
android:maxLongVersionCode="10000000000"/>
@@ -116,6 +119,9 @@
<compatibility-package
android:name="com.opera.browser.beta"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.opera.gx"
android:maxLongVersionCode="10000000000"/>
<compatibility-package
android:name="com.opera.mini.native"
android:maxLongVersionCode="10000000000"/>

View File

@@ -948,5 +948,21 @@ namespace Bit.Droid.Services
{
// for any Android-specific cleanup required after switching accounts
}
public async Task SetScreenCaptureAllowedAsync()
{
if (CoreHelpers.ForceScreenCaptureEnabled())
{
return;
}
var activity = CrossCurrentActivity.Current?.Activity;
if (await _stateService.GetScreenCaptureAllowedAsync())
{
activity.RunOnUiThread(() => activity.Window.ClearFlags(WindowManagerFlags.Secure));
return;
}
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
}
}
}

View File

@@ -8,5 +8,6 @@ namespace Bit.App.Abstractions
{
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
Task LogOutAsync(string userId, bool userInitiated, bool expired);
}
}

View File

@@ -48,5 +48,6 @@ namespace Bit.App.Abstractions
bool SupportsFido2();
float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync();
Task SetScreenCaptureAllowedAsync();
}
}

View File

@@ -129,12 +129,10 @@
<Folder Include="Behaviors\" />
<Folder Include="Controls\AccountSwitchingOverlay\" />
<Folder Include="Utilities\AccountManagement\" />
<Folder Include="Controls\DateTime\" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Controls\CipherViewCell\CipherViewCell.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Remove="Pages\Accounts\AccountsPopupPage.xaml" />
</ItemGroup>
@@ -162,12 +160,6 @@
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Styles\Base.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\AppResources.cs.Designer.cs">
<DependentUpon>AppResources.cs.resx</DependentUpon>
@@ -422,5 +414,6 @@
<None Remove="Xamarin.CommunityToolkit" />
<None Remove="Controls\AccountSwitchingOverlay\" />
<None Remove="Utilities\AccountManagement\" />
<None Remove="Controls\DateTime\" />
</ItemGroup>
</Project>

View File

@@ -301,7 +301,7 @@ namespace Bit.App
UpdateThemeAsync();
};
Current.MainPage = new NavigationPage(new HomePage(Options));
var mainPageTask = _accountsManager.NavigateOnAccountChangeAsync();
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
}

View File

@@ -13,7 +13,8 @@ namespace Bit.App.Controls
public AccountViewCellViewModel(AccountView accountView)
{
AccountView = accountView;
AvatarImageSource = new AvatarImageSource(AccountView.Name, AccountView.Email);
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
?.GetOrCreateAvatar(AccountView.Name, AccountView.Email);
}
public AccountView AccountView

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.AuthenticatorViewCell"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
StyleClass="list-row, list-row-platform"
HorizontalOptions="FillAndExpand"
x:DataType="pages:GroupingsPageTOTPListItem"
ColumnDefinitions="40,*,40,Auto,40"
RowSpacing="0"
Padding="0,10,0,0"
RowDefinitions="*,*">
<Grid.Resources>
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<u:InverseBoolConverter x:Key="inverseBool" />
</Grid.Resources>
<controls:IconLabel
Grid.Column="0"
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
Grid.RowSpan="2"
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
AutomationProperties.IsInAccessibleTree="False" />
<ff:CachedImage
Grid.Column="0"
BitmapOptimizations="True"
ErrorPlaceholder="login.png"
LoadingPlaceholder="login.png"
HorizontalOptions="Center"
VerticalOptions="Center"
WidthRequest="22"
HeightRequest="22"
Grid.RowSpan="2"
IsVisible="{Binding ShowIconImage}"
Source="{Binding IconImageSource, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="False" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="1"
Grid.Row="0"
VerticalTextAlignment="Center"
VerticalOptions="Fill"
StyleClass="list-title, list-title-platform"
Text="{Binding Cipher.Name}" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="1"
Grid.Row="1"
VerticalTextAlignment="Center"
VerticalOptions="Fill"
StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Cipher.SubTitle}" />
<controls:CircularProgressbarView
Progress="{Binding Progress}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand" />
<Label
Text="{Binding TotpSec, Mode=OneWay}"
Style="{DynamicResource textTotp}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
StyleClass="text-sm"
HorizontalTextAlignment="Center"
HorizontalOptions="Fill"
VerticalTextAlignment="Center"
VerticalOptions="Fill" />
<StackLayout
Grid.Row="0"
Grid.Column="3"
Margin="3,0,2,0"
Spacing="5"
Grid.RowSpan="2"
Orientation="Horizontal"
HorizontalOptions="Fill"
VerticalOptions="Fill">
<controls:MonoLabel
Text="{Binding TotpCodeFormattedStart, Mode=OneWay}"
Style="{DynamicResource textTotp}"
StyleClass="text-lg"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
HorizontalOptions="Center"
VerticalOptions="FillAndExpand" />
<controls:MonoLabel
Text="{Binding TotpCodeFormattedEnd, Mode=OneWay}"
Style="{DynamicResource textTotp}"
StyleClass="text-lg"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
HorizontalOptions="Center"
VerticalOptions="FillAndExpand" />
</StackLayout>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
CommandParameter="LoginTotp"
Grid.Row="0"
Grid.Column="4"
Grid.RowSpan="2"
Padding="0,0,1,0"
HorizontalOptions="Center"
VerticalOptions="Center"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyTotp}" />
</controls:ExtendedGrid>

View File

@@ -0,0 +1,67 @@
using System;
using Bit.App.Pages;
using Bit.App.Utilities;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class AuthenticatorViewCell : ExtendedGrid
{
public static readonly BindableProperty CipherProperty = BindableProperty.Create(
nameof(Cipher), typeof(CipherView), typeof(AuthenticatorViewCell), default(CipherView), BindingMode.TwoWay);
public static readonly BindableProperty WebsiteIconsEnabledProperty = BindableProperty.Create(
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(AuthenticatorViewCell));
public static readonly BindableProperty TotpSecProperty = BindableProperty.Create(
nameof(TotpSec), typeof(long), typeof(AuthenticatorViewCell));
public AuthenticatorViewCell()
{
InitializeComponent();
}
public Command CopyCommand { get; set; }
public CipherView Cipher
{
get => GetValue(CipherProperty) as CipherView;
set => SetValue(CipherProperty, value);
}
public bool? WebsiteIconsEnabled
{
get => (bool)GetValue(WebsiteIconsEnabledProperty);
set => SetValue(WebsiteIconsEnabledProperty, value);
}
public long TotpSec
{
get => (long)GetValue(TotpSecProperty);
set => SetValue(TotpSecProperty, value);
}
public bool ShowIconImage
{
get => WebsiteIconsEnabled ?? false
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
&& IconImageSource != null;
}
private string _iconImageSource = string.Empty;
public string IconImageSource
{
get
{
if (_iconImageSource == string.Empty) // default value since icon source can return null
{
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
}
return _iconImageSource;
}
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SkiaSharp;
@@ -50,7 +51,7 @@ namespace Bit.App.Controls
private Stream Draw()
{
string chars = null;
string chars;
string upperData = null;
if (string.IsNullOrEmpty(_data))
@@ -71,62 +72,83 @@ namespace Bit.App.Controls
var textColor = Color.White;
var size = 50;
var bitmap = new SKBitmap(
size * 2,
using (var bitmap = new SKBitmap(size * 2,
size * 2,
SKImageInfo.PlatformColorType,
SKAlphaType.Premul);
var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Transparent);
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
var radius = midX - midX / 5;
var circlePaint = new SKPaint
SKAlphaType.Premul))
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex())
};
canvas.DrawCircle(midX, midY, radius, circlePaint);
using (var canvas = new SKCanvas(bitmap))
{
canvas.Clear(SKColors.Transparent);
using (var paint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex())
})
{
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
var radius = midX - midX / 5;
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
var textSize = midX / 1.3f;
var textPaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
Color = SKColor.Parse(textColor.ToHex()),
TextSize = textSize,
TextAlign = SKTextAlign.Center,
Typeface = typeface
};
var rect = new SKRect();
textPaint.MeasureText(chars, ref rect);
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
using (var circlePaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex())
})
{
canvas.DrawCircle(midX, midY, radius, circlePaint);
return SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100).AsStream();
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
var textSize = midX / 1.3f;
using (var textPaint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
Color = SKColor.Parse(textColor.ToHex()),
TextSize = textSize,
TextAlign = SKTextAlign.Center,
Typeface = typeface
})
{
var rect = new SKRect();
textPaint.MeasureText(chars, ref rect);
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
using (var img = SKImage.FromBitmap(bitmap))
{
var data = img.Encode(SKEncodedImageFormat.Png, 100);
return data?.AsStream(true);
}
}
}
}
}
}
}
private string GetFirstLetters(string data, int charCount)
{
var parts = data.Split();
var sanitizedData = data.Trim();
var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 1 && charCount <= 2)
{
var text = "";
for (int i = 0; i < charCount; i++)
var text = string.Empty;
for (var i = 0; i < charCount; i++)
{
text += parts[i].Substring(0, 1);
text += parts[i][0];
}
return text;
}
if (data.Length > 2)
if (sanitizedData.Length > 2)
{
return data.Substring(0, 2);
return sanitizedData.Substring(0, 2);
}
return data;
return sanitizedData;
}
private Color StringToColor(string str)

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Concurrent;
namespace Bit.App.Controls
{
public interface IAvatarImageSourcePool
{
AvatarImageSource GetOrCreateAvatar(string name, string email);
}
public class AvatarImageSourcePool : IAvatarImageSourcePool
{
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
public AvatarImageSource GetOrCreateAvatar(string name, string email)
{
var key = $"{name}{email}";
if (!_cache.TryGetValue(key, out var avatar))
{
avatar = new AvatarImageSource(name, email);
if (!_cache.TryAdd(key, avatar)
&&
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.
{
// if add and get after fails, then something wrong is going on with this method.
throw new InvalidOperationException("Something is wrong creating the avatar image");
}
}
return avatar;
}
}
}

View File

@@ -0,0 +1,139 @@
using System;
using System.Runtime.CompilerServices;
using SkiaSharp;
using SkiaSharp.Views.Forms;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class CircularProgressbarView : SKCanvasView
{
private Circle _circle;
public static readonly BindableProperty ProgressProperty = BindableProperty.Create(
nameof(Progress), typeof(double), typeof(CircularProgressbarView), propertyChanged: OnProgressChanged);
public static readonly BindableProperty RadiusProperty = BindableProperty.Create(
nameof(Radius), typeof(float), typeof(CircularProgressbarView), 15f);
public static readonly BindableProperty StrokeWidthProperty = BindableProperty.Create(
nameof(StrokeWidth), typeof(float), typeof(CircularProgressbarView), 3f);
public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create(
nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
public static readonly BindableProperty EndingProgressColorProperty = BindableProperty.Create(
nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
public static readonly BindableProperty BackgroundProgressColorProperty = BindableProperty.Create(
nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
public double Progress
{
get { return (double)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
}
public float Radius
{
get => (float)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
public float StrokeWidth
{
get => (float)GetValue(StrokeWidthProperty);
set => SetValue(StrokeWidthProperty, value);
}
public Color ProgressColor
{
get => (Color)GetValue(ProgressColorProperty);
set => SetValue(ProgressColorProperty, value);
}
public Color EndingProgressColor
{
get => (Color)GetValue(EndingProgressColorProperty);
set => SetValue(EndingProgressColorProperty, value);
}
public Color BackgroundProgressColor
{
get => (Color)GetValue(BackgroundProgressColorProperty);
set => SetValue(BackgroundProgressColorProperty, value);
}
private static void OnProgressChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var context = bindable as CircularProgressbarView;
context.InvalidateSurface();
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(Progress))
{
_circle = new Circle(Radius * (float)DeviceDisplay.MainDisplayInfo.Density, (info) => new SKPoint((float)info.Width / 2, (float)info.Height / 2));
}
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
if (_circle != null)
{
_circle.CalculateCenter(e.Info);
e.Surface.Canvas.Clear();
DrawCircle(e.Surface.Canvas, _circle, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, BackgroundProgressColor.ToSKColor());
DrawArc(e.Surface.Canvas, _circle, () => (float)Progress, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, ProgressColor.ToSKColor(), EndingProgressColor.ToSKColor());
}
}
private void DrawCircle(SKCanvas canvas, Circle circle, float strokewidth, SKColor color)
{
canvas.DrawCircle(circle.Center, circle.Redius,
new SKPaint()
{
StrokeWidth = strokewidth,
Color = color,
IsStroke = true,
IsAntialias = true
});
}
private void DrawArc(SKCanvas canvas, Circle circle, Func<float> progress, float strokewidth, SKColor color, SKColor progressEndColor)
{
var progressValue = progress();
var angle = progressValue * 3.6f;
canvas.DrawArc(circle.Rect, 270, angle, false,
new SKPaint()
{
StrokeWidth = strokewidth,
Color = progressValue < 20f ? progressEndColor : color,
IsStroke = true,
IsAntialias = true
});
}
}
public class Circle
{
private readonly Func<SKImageInfo, SKPoint> _centerFunc;
public Circle(float redius, Func<SKImageInfo, SKPoint> centerFunc)
{
_centerFunc = centerFunc;
Redius = redius;
}
public SKPoint Center { get; set; }
public float Redius { get; set; }
public SKRect Rect => new SKRect(Center.X - Redius, Center.Y - Redius, Center.X + Redius, Center.Y + Redius);
public void CalculateCenter(SKImageInfo argsInfo)
{
Center = _centerFunc(argsInfo);
}
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Grid
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:Class="Bit.App.Controls.DateTimePicker"
ColumnDefinitions="*,*">
<controls:ExtendedDatePicker
x:Name="_datePicker"
Grid.Column="0"
NullableDate="{Binding Date, Mode=TwoWay}"
Format="d"
AutomationProperties.IsInAccessibleTree="True" />
<controls:ExtendedTimePicker
x:Name="_timePicker"
Grid.Column="1"
NullableTime="{Binding Time, Mode=TwoWay}"
Format="t"
AutomationProperties.IsInAccessibleTree="True" />
</Grid>

View File

@@ -0,0 +1,34 @@
using System.Runtime.CompilerServices;
using Xamarin.CommunityToolkit.UI.Views;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class DateTimePicker : Grid
{
public DateTimePicker()
{
InitializeComponent();
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(BindingContext)
&&
BindingContext is DateTimeViewModel dateTimeViewModel)
{
AutomationProperties.SetName(_datePicker, dateTimeViewModel.DateName);
AutomationProperties.SetName(_timePicker, dateTimeViewModel.TimeName);
_datePicker.PlaceHolder = dateTimeViewModel.DatePlaceholder;
_timePicker.PlaceHolder = dateTimeViewModel.TimePlaceholder;
}
}
}
public class LazyDateTimePicker : LazyView<DateTimePicker>
{
}
}

View File

@@ -0,0 +1,70 @@
using System;
using Bit.Core.Utilities;
namespace Bit.App.Controls
{
public class DateTimeViewModel : ExtendedViewModel
{
DateTime? _date;
TimeSpan? _time;
public DateTimeViewModel(string dateName, string timeName)
{
DateName = dateName;
TimeName = timeName;
}
public Action<DateTime?> OnDateChanged { get; set; }
public Action<TimeSpan?> OnTimeChanged { get; set; }
public DateTime? Date
{
get => _date;
set
{
if (SetProperty(ref _date, value))
{
OnDateChanged?.Invoke(value);
}
}
}
public TimeSpan? Time
{
get => _time;
set
{
if (SetProperty(ref _time, value))
{
OnTimeChanged?.Invoke(value);
}
}
}
public string DateName { get; }
public string TimeName { get; }
public string DatePlaceholder { get; set; }
public string TimePlaceholder { get; set; }
public DateTime? DateTime
{
get
{
if (Date.HasValue)
{
if (Time.HasValue)
{
return Date.Value.Add(Time.Value);
}
return Date;
}
return null;
}
set
{
Date = value?.Date;
Time = value?.Date.TimeOfDay;
}
}
}
}

View File

@@ -14,7 +14,7 @@
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n Save}" Clicked="Submit_Clicked" />
<ToolbarItem Text="{u:I18n Save}" Command="{Binding SubmitCommand}" />
</ContentPage.ToolbarItems>
<ScrollView>

View File

@@ -36,14 +36,6 @@ namespace Bit.App.Pages
};
}
private async void Submit_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.SubmitAsync();
}
}
private async Task SubmitSuccessAsync()
{
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);

View File

@@ -1,15 +1,17 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
using Xamarin.CommunityToolkit.ObjectModel;
namespace Bit.App.Pages
{
public class EnvironmentPageViewModel : BaseViewModel
{
private readonly IEnvironmentService _environmentService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
public EnvironmentPageViewModel()
{
@@ -22,10 +24,10 @@ namespace Bit.App.Pages
IdentityUrl = _environmentService.IdentityUrl;
IconsUrl = _environmentService.IconsUrl;
NotificationsUrls = _environmentService.NotificationsUrl;
SubmitCommand = new Command(async () => await SubmitAsync());
SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
}
public Command SubmitCommand { get; }
public ICommand SubmitCommand { get; }
public string BaseUrl { get; set; }
public string ApiUrl { get; set; }
public string IdentityUrl { get; set; }
@@ -37,6 +39,12 @@ namespace Bit.App.Pages
public async Task SubmitAsync()
{
if (!ValidateUrls())
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok);
return;
}
var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
{
Base = BaseUrl,
@@ -57,5 +65,25 @@ namespace Bit.App.Pages
SubmitSuccessAction?.Invoke();
}
public bool ValidateUrls()
{
bool IsUrlValid(string url)
{
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute);
}
return IsUrlValid(BaseUrl)
&& IsUrlValid(ApiUrl)
&& IsUrlValid(IdentityUrl)
&& IsUrlValid(WebVaultUrl)
&& IsUrlValid(IconsUrl);
}
private void OnSubmitException(Exception ex)
{
_logger.Value.Exception(ex);
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
}
}

View File

@@ -219,7 +219,8 @@ namespace Bit.App.Pages
// Request
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
{
ResetPasswordKey = encryptedKey.EncryptedString
ResetPasswordKey = encryptedKey.EncryptedString,
MasterPasswordHash = masterPasswordHash,
};
var userId = await _stateService.GetActiveUserIdAsync();
// Enroll user

View File

@@ -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 DeletionDate, Mode=TwoWay}"
NullableDate="{Binding DeletionDateTimeViewModel.Date, Mode=TwoWay}"
Format="d"
IsEnabled="{Binding SendEnabled}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionDate}"
Grid.Column="0" />
<controls:ExtendedTimePicker
NullableTime="{Binding DeletionTime, Mode=TwoWay}"
NullableTime="{Binding DeletionDateTimeViewModel.Time, Mode=TwoWay}"
Format="t"
IsEnabled="{Binding SendEnabled}"
AutomationProperties.IsInAccessibleTree="True"
@@ -343,7 +343,7 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:ExtendedDatePicker
NullableDate="{Binding ExpirationDate, Mode=TwoWay}"
NullableDate="{Binding ExpirationDateTimeViewModel.Date, Mode=TwoWay}"
PlaceHolder="mm/dd/yyyy"
Format="d"
IsEnabled="{Binding SendEnabled}"
@@ -351,7 +351,7 @@
AutomationProperties.Name="{u:I18n ExpirationDate}"
Grid.Column="0" />
<controls:ExtendedTimePicker
NullableTime="{Binding ExpirationTime, Mode=TwoWay}"
NullableTime="{Binding ExpirationDateTimeViewModel.Time, Mode=TwoWay}"
PlaceHolder="--:-- --"
Format="t"
IsEnabled="{Binding SendEnabled}"

View File

@@ -23,7 +23,6 @@ namespace Bit.App.Pages
private AppOptions _appOptions;
private SendAddEditPageViewModel _vm;
public Action OnClose { get; set; }
public Action AfterSubmit { get; set; }
public SendAddEditPage(
@@ -136,14 +135,7 @@ namespace Bit.App.Pages
private async Task CloseAsync()
{
if (OnClose is null)
{
await Navigation.PopModalAsync();
}
else
{
OnClose();
}
await Navigation.PopModalAsync();
}
protected override bool OnBackButtonPressed()

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
@@ -23,7 +24,7 @@ namespace Bit.App.Pages
private readonly IStateService _stateService;
private readonly ISendService _sendService;
private readonly ILogger _logger;
private bool _sendEnabled;
private bool _sendEnabled = true;
private bool _canAccessPremium;
private bool _emailVerified;
private SendView _send;
@@ -33,11 +34,7 @@ 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[]
@@ -89,8 +86,34 @@ 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; }
@@ -126,23 +149,14 @@ 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(OptionsAccessilibityText),
nameof(OptionsShowHideIcon)
});
}
public int ExpirationDateTypeSelectedIndex
@@ -156,28 +170,7 @@ namespace Bit.App.Pages
}
}
}
public DateTime? ExpirationDate
{
get => _expirationDate;
set
{
if (SetProperty(ref _expirationDate, value))
{
ExpirationDateChanged();
}
}
}
public TimeSpan? ExpirationTime
{
get => _expirationTime;
set
{
if (SetProperty(ref _expirationTime, value))
{
ExpirationTimeChanged();
}
}
}
public int? MaxAccessCount
{
get => _maxAccessCount;
@@ -205,7 +198,7 @@ namespace Bit.App.Pages
}
public string FileName
{
get => _fileName;
get => _fileName ?? AppResources.NoFileChosen;
set
{
if (SetProperty(ref _fileName, value))
@@ -240,10 +233,13 @@ namespace Bit.App.Pages
public bool IsFile => Send?.Type == SendType.File;
public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6;
public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7;
public DateTimeViewModel DeletionDateTimeViewModel { get; }
public DateTimeViewModel ExpirationDateTimeViewModel { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string FileTypeAccessibilityLabel => IsFile ? AppResources.FileTypeIsSelected : AppResources.FileTypeIsNotSelected;
public string TextTypeAccessibilityLabel => IsText ? AppResources.TextTypeIsSelected : AppResources.TextTypeIsNotSelected;
public string OptionsShowHideIcon => ShowOptions ? BitwardenIcons.ChevronUp : BitwardenIcons.AngleDown;
public async Task InitAsync()
{
@@ -268,10 +264,8 @@ namespace Bit.App.Pages
return false;
}
Send = await send.DecryptAsync();
DeletionDate = Send.DeletionDate.ToLocalTime();
DeletionTime = DeletionDate.TimeOfDay;
ExpirationDate = Send.ExpirationDate?.ToLocalTime();
ExpirationTime = ExpirationDate?.TimeOfDay;
DeletionDateTimeViewModel.DateTime = Send.DeletionDate.ToLocalTime();
ExpirationDateTimeViewModel.DateTime = Send.ExpirationDate?.ToLocalTime();
}
else
{
@@ -280,8 +274,7 @@ namespace Bit.App.Pages
{
Type = Type.GetValueOrDefault(defaultType),
};
_deletionDate = DateTimeNow().AddDays(7);
_deletionTime = DeletionDate.TimeOfDay;
DeletionDateTimeViewModel.DateTime = DateTimeNow().AddDays(7);
DeletionDateTypeSelectedIndex = 4;
ExpirationDateTypeSelectedIndex = 0;
}
@@ -305,23 +298,22 @@ namespace Bit.App.Pages
public void ClearExpirationDate()
{
_isOverridingPickers = true;
ExpirationDate = null;
ExpirationTime = null;
ExpirationDateTimeViewModel.DateTime = null;
_isOverridingPickers = false;
}
private void UpdateSendData()
{
// filename
if (Send.File != null && FileName != null)
if (Send.File != null && _fileName != null)
{
Send.File.FileName = FileName;
Send.File.FileName = _fileName;
}
// deletion date
if (ShowDeletionCustomPickers)
{
Send.DeletionDate = DeletionDate.Date.Add(DeletionTime).ToUniversalTime();
Send.DeletionDate = DeletionDateTimeViewModel.DateTime.Value.ToUniversalTime();
}
else
{
@@ -329,9 +321,9 @@ namespace Bit.App.Pages
}
// expiration date
if (ShowExpirationCustomPickers && ExpirationDate.HasValue && ExpirationTime.HasValue)
if (ShowExpirationCustomPickers && ExpirationDateTimeViewModel.DateTime.HasValue)
{
Send.ExpirationDate = ExpirationDate.Value.Date.Add(ExpirationTime.Value).ToUniversalTime();
Send.ExpirationDate = ExpirationDateTimeViewModel.DateTime.Value.ToUniversalTime();
}
else if (_simpleExpirationDateTime.HasValue)
{
@@ -484,7 +476,7 @@ namespace Bit.App.Pages
return;
}
if (Page is SendAddEditPage sendPage && sendPage.OnClose != null)
if (Page is SendAddOnlyPage sendPage && sendPage.OnClose != null)
{
sendPage.OnClose();
return;
@@ -625,24 +617,6 @@ namespace Bit.App.Pages
}
}
private void ExpirationDateChanged()
{
if (!_isOverridingPickers && !ExpirationTime.HasValue)
{
// auto-set time to current time upon setting date
ExpirationTime = DateTimeNow().TimeOfDay;
}
}
private void ExpirationTimeChanged()
{
if (!_isOverridingPickers && !ExpirationDate.HasValue)
{
// auto-set date to current date upon setting time
ExpirationDate = DateTime.Today;
}
}
private void MaxAccessCountChanged()
{
Send.MaxAccessCount = _maxAccessCount;
@@ -666,5 +640,10 @@ namespace Bit.App.Pages
DateTimeKind.Local
);
}
internal void TriggerSendTextPropertyChanged()
{
Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(Send)));
}
}
}

View File

@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
x:DataType="pages:SendAddEditPageViewModel"
x:Class="Bit.App.Pages.SendAddOnlyOptionsView">
<ContentView.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
</ResourceDictionary>
</ContentView.Resources>
<ContentView.Content>
<StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,10,0,0">
<Label
Text="{u:I18n DeletionDate}"
StyleClass="box-label" />
<Picker
x:Name="_deletionDateTypePicker"
ItemsSource="{Binding DeletionTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding DeletionDateTypeSelectedIndex}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
ItemDisplayBinding="{Binding Key}"
ios:Picker.UpdateMode="WhenFinished"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionTime}" />
<controls:LazyDateTimePicker
x:Name="_lazyDeletionDateTimePicker"
BindingContext="{Binding DeletionDateTimeViewModel}"
IsVisible="{Binding ShowDeletionCustomPickers}"
Margin="0,5,0,0" />
<Label
Text="{u:I18n DeletionDateInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout StyleClass="box-row" Margin="0,5,0,0">
<Label
Text="{u:I18n ExpirationDate}"
StyleClass="box-label" />
<Picker
x:Name="_expirationDateTypePicker"
ItemsSource="{Binding ExpirationTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding ExpirationDateTypeSelectedIndex}"
ItemDisplayBinding="{Binding Key}"
ios:Picker.UpdateMode="WhenFinished"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ExpirationTime}" />
<controls:LazyDateTimePicker
x:Name="_lazyExpirationDateTimePicker"
BindingContext="{Binding ExpirationDateTimeViewModel}"
IsVisible="{Binding ShowExpirationCustomPickers}"
Margin="0,5,0,0" />
<Label
Text="{u:I18n ExpirationDateInfo}"
StyleClass="box-footer-label"
HorizontalOptions="StartAndExpand"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,5,0,0">
<Label
Text="{u:I18n MaximumAccessCount}"
StyleClass="box-label" />
<StackLayout
StyleClass="box-row"
Orientation="Horizontal">
<Entry
Text="{Binding MaxAccessCount}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
Keyboard="Numeric"
MaxLength="9"
TextChanged="OnMaxAccessCountTextChanged"
HorizontalOptions="FillAndExpand" />
<controls:ExtendedStepper
x:Name="_maxAccessCountStepper"
Value="{Binding MaxAccessCount}"
Maximum="999999999"
IsEnabled="{Binding SendEnabled}"
Margin="10,0,0,0" />
</StackLayout>
<Label
Text="{u:I18n MaximumAccessCountInfo}"
StyleClass="box-footer-label" />
</StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,5,0,0">
<Label
Text="{u:I18n NewPassword}"
StyleClass="box-label" />
<StackLayout Orientation="Horizontal">
<Entry
Text="{Binding NewPassword}"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
HorizontalOptions="FillAndExpand" />
<controls:IconButton
IsEnabled="{Binding SendEnabled}"
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Margin="10,0,0,0"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</StackLayout>
<Label
Text="{u:I18n PasswordInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row"
Margin="0,5,0,0">
<Label
Text="{u:I18n Notes}"
StyleClass="box-label" />
<Editor
x:Name="_notesEditor"
AutoSize="TextChanges"
Text="{Binding Send.Notes}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
Margin="0,10,0,5"
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
<Editor.Effects>
<effects:ScrollEnabledEffect />
</Editor.Effects>
</Editor>
<BoxView
StyleClass="box-row-separator" />
<Label
Text="{u:I18n NotesInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row, box-row-switch"
Margin="0,5,0,0">
<Label
Text="{u:I18n HideEmail}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Send.HideEmail}"
IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row, box-row-switch"
Margin="0,5,0,0">
<Label
Text="{u:I18n DisableSend}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Send.Disabled}"
IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
</StackLayout>
</ContentView.Content>
</ContentView>

View File

@@ -0,0 +1,91 @@
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Bit.App.Behaviors;
using Xamarin.CommunityToolkit.UI.Views;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class SendAddOnlyOptionsView : ContentView
{
public SendAddOnlyOptionsView()
{
InitializeComponent();
}
private SendAddEditPageViewModel ViewModel => BindingContext as SendAddEditPageViewModel;
public void SetMainScrollView(ScrollView scrollView)
{
_notesEditor.Behaviors.Add(new EditorPreventAutoBottomScrollingOnFocusedBehavior { ParentScrollView = scrollView });
}
private void OnMaxAccessCountTextChanged(object sender, TextChangedEventArgs e)
{
if (ViewModel is null)
{
return;
}
if (string.IsNullOrWhiteSpace(e.NewTextValue))
{
ViewModel.MaxAccessCount = null;
_maxAccessCountStepper.Value = 0;
return;
}
// accept only digits
if (!int.TryParse(e.NewTextValue, out int _))
{
((Entry)sender).Text = e.OldTextValue;
}
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(BindingContext)
&&
ViewModel != null)
{
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
}
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (!_lazyDeletionDateTimePicker.IsLoaded
&&
e.PropertyName == nameof(SendAddEditPageViewModel.ShowDeletionCustomPickers)
&&
ViewModel.ShowDeletionCustomPickers)
{
_lazyDeletionDateTimePicker.LoadViewAsync();
}
if (!_lazyExpirationDateTimePicker.IsLoaded
&&
e.PropertyName == nameof(SendAddEditPageViewModel.ShowExpirationCustomPickers)
&&
ViewModel.ShowExpirationCustomPickers)
{
_lazyExpirationDateTimePicker.LoadViewAsync();
}
}
}
public class SendAddOnlyOptionsLazyView : LazyView<SendAddOnlyOptionsView>
{
public ScrollView MainScrollView { get; set; }
public override async ValueTask LoadViewAsync()
{
await base.LoadViewAsync();
if (Content is SendAddOnlyOptionsView optionsView)
{
optionsView.SetMainScrollView(MainScrollView);
}
}
}
}

View File

@@ -0,0 +1,190 @@
<?xml version="1.0" encoding="utf-8"?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.SendAddOnlyPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
xmlns:effects="clr-namespace:Bit.App.Effects"
x:DataType="pages:SendAddEditPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:SendAddEditPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<!--Order matters here or the avatar's image won't be updated correctly, check iOS CustomNavigationRenderer for more info-->
<controls:ExtendedToolbarItem
x:Name="_accountAvatar"
IconImageSource="{Binding AvatarImageSource}"
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
Order="Primary"
Priority="-2"
UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" />
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" />
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/>
</ContentPage.ToolbarItems>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
</ResourceDictionary>
</ContentPage.Resources>
<AbsoluteLayout>
<ScrollView
x:Name="_scrollView"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All">
<StackLayout x:Name="_mainContainer" StyleClass="box">
<Frame
IsVisible="{Binding SendEnabled, Converter={StaticResource inverseBool}}"
Padding="10"
Margin="0, 12, 0, 0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{u:I18n SendDisabledWarning}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
<Frame
IsVisible="{Binding SendOptionsPolicyInEffect}"
Padding="10"
Margin="0, 12, 0, 0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{u:I18n SendOptionsPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n Name}"
StyleClass="box-label" />
<Entry
x:Name="_nameEntry"
Text="{Binding Send.Name}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value" />
<Label
Text="{u:I18n NameInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout
StyleClass="box-row"
IsVisible="{Binding IsFile}">
<Label
Text="{u:I18n TypeFile}"
StyleClass="box-label" />
<StackLayout
StyleClass="box-row">
<Label
Text="{Binding FileName}"
LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
<Label
Margin="0, 5, 0, 0"
Text="{u:I18n MaxFileSize}"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
</StackLayout>
</StackLayout>
<StackLayout
StyleClass="box-row"
IsVisible="{Binding IsText}">
<Label
Text="{u:I18n TypeText}"
StyleClass="box-label" />
<Editor
x:Name="_textEditor"
AutoSize="TextChanges"
Text="{Binding Send.Text.Text}"
IsEnabled="{Binding SendEnabled}"
StyleClass="box-value"
Margin="{Binding EditorMargins}"
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
<Editor.Behaviors>
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
</Editor.Behaviors>
<Editor.Effects>
<effects:ScrollEnabledEffect />
</Editor.Effects>
</Editor>
<BoxView
StyleClass="box-row-separator" />
<Label
Text="{u:I18n TypeTextInfo}"
StyleClass="box-footer-label"
Margin="0,5,0,10" />
<StackLayout
StyleClass="box-row, box-row-switch"
Margin="0,10,0,0">
<Label
Text="{u:I18n HideTextByDefault}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Send.Text.Hidden}"
IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
</StackLayout>
<StackLayout
StyleClass="box-row, box-row-switch">
<Label
Text="{Binding ShareOnSaveText}"
StyleClass="box-label-regular"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding ShareOnSave}"
IsEnabled="{Binding SendEnabled}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
<StackLayout
Orientation="Horizontal"
Spacing="0"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding OptionsAccessilibityText}">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="OptionsHeader_Tapped" />
</StackLayout.GestureRecognizers>
<Label
Text="{u:I18n Options}"
TextColor="{DynamicResource PrimaryColor}"
Margin="0,0,5,0"
AutomationProperties.IsInAccessibleTree="False"/>
<controls:IconLabel
Text="{Binding OptionsShowHideIcon}"
TextColor="{DynamicResource PrimaryColor}"
AutomationProperties.IsInAccessibleTree="False"/>
</StackLayout>
<pages:SendAddOnlyOptionsLazyView x:Name="_lazyOptionsView" IsVisible="{Binding ShowOptions}" />
</StackLayout>
</ScrollView>
<controls:AccountSwitchingOverlayView
x:Name="_accountListOverlay"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All"
LongPressAccountEnabled="False"
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
</AbsoluteLayout>
</pages:BaseContentPage>

View File

@@ -0,0 +1,178 @@
using System;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
/// <summary>
/// This is a version of <see cref="SendAddEditPage"/> that is reduced for adding only and adapted
/// for performance for iOS Share extension.
/// </summary>
/// <remarks>
/// This should NOT be used in Android.
/// </remarks>
public partial class SendAddOnlyPage : BaseContentPage
{
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private AppOptions _appOptions;
private SendAddEditPageViewModel _vm;
public Action OnClose { get; set; }
public Action AfterSubmit { get; set; }
public SendAddOnlyPage(
AppOptions appOptions = null,
string sendId = null,
SendType? type = null)
{
if (appOptions?.IosExtension != true)
{
throw new InvalidOperationException(nameof(SendAddOnlyPage) + " is only prepared to be used in iOS share extension");
}
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_appOptions = appOptions;
InitializeComponent();
_vm = BindingContext as SendAddEditPageViewModel;
_vm.Page = this;
_vm.SendId = sendId;
_vm.Type = appOptions?.CreateSend?.Item1 ?? type;
if (_vm.IsText)
{
_nameEntry.ReturnType = ReturnType.Next;
_nameEntry.ReturnCommand = new Command(() => _textEditor.Focus());
}
}
protected override async void OnAppearing()
{
base.OnAppearing();
try
{
if (!await AppHelpers.IsVaultTimeoutImmediateAsync())
{
await _vaultTimeoutService.CheckVaultTimeoutAsync();
}
if (await _vaultTimeoutService.IsLockedAsync())
{
return;
}
await _vm.InitAsync();
if (!await _vm.LoadAsync())
{
await CloseAsync();
return;
}
_accountAvatar?.OnAppearing();
await Device.InvokeOnMainThreadAsync(async () => _vm.AvatarImageSource = await GetAvatarImageSourceAsync());
await HandleCreateRequest();
if (string.IsNullOrWhiteSpace(_vm.Send?.Name))
{
RequestFocus(_nameEntry);
}
AdjustToolbar();
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
await CloseAsync();
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_accountAvatar?.OnDisappearing();
}
private async Task CloseAsync()
{
if (OnClose is null)
{
await Navigation.PopModalAsync();
}
else
{
OnClose();
}
}
private async void Save_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
var submitted = await _vm.SubmitAsync();
if (submitted)
{
AfterSubmit?.Invoke();
}
}
}
private async void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await CloseAsync();
}
}
private void AdjustToolbar()
{
_saveItem.IsEnabled = _vm.SendEnabled;
}
private Task HandleCreateRequest()
{
if (_appOptions?.CreateSend == null)
{
return Task.CompletedTask;
}
_vm.IsAddFromShare = true;
_vm.CopyInsteadOfShareAfterSaving = _appOptions.CopyInsteadOfShareAfterSaving;
var name = _appOptions.CreateSend.Item2;
_vm.Send.Name = name;
var type = _appOptions.CreateSend.Item1;
if (type == SendType.File)
{
_vm.FileData = _appOptions.CreateSend.Item3;
_vm.FileName = name;
}
else
{
var text = _appOptions.CreateSend.Item4;
_vm.Send.Text.Text = text;
_vm.TriggerSendTextPropertyChanged();
}
_appOptions.CreateSend = null;
return Task.CompletedTask;
}
void OptionsHeader_Tapped(object sender, EventArgs e)
{
_vm.ToggleOptionsCommand.Execute(null);
if (!_lazyOptionsView.IsLoaded)
{
_lazyOptionsView.MainScrollView = _scrollView;
_lazyOptionsView.LoadViewAsync();
}
}
}
}

View File

@@ -83,31 +83,31 @@
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n DisableAutoTotpCopy}"
Text="{u:I18n CopyTotpAutomatically}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding DisableAutoTotpCopy}"
IsToggled="{Binding AutoTotpCopy}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
<Label
Text="{u:I18n DisableAutoTotpCopyDescription}"
Text="{u:I18n CopyTotpAutomaticallyDescription}"
StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n DisableWebsiteIcons}"
Text="{u:I18n ShowWebsiteIcons}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding DisableFavicon}"
IsToggled="{Binding Favicon}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
<Label
Text="{u:I18n DisableWebsiteIconsDescription}"
Text="{u:I18n ShowWebsiteIconsDescription}"
StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
@@ -117,16 +117,16 @@
</StackLayout>
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n DisableSavePrompt}"
Text="{u:I18n AskToAddLogin}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding AutofillDisableSavePrompt}"
IsToggled="{Binding AutofillSavePrompt}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>
<Label
Text="{u:I18n DisableSavePromptDescription}"
Text="{u:I18n AskToAddLoginDescription}"
StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">

View File

@@ -12,15 +12,14 @@ namespace Bit.App.Pages
{
public class OptionsPageViewModel : BaseViewModel
{
private readonly ITotpService _totpService;
private readonly IStateService _stateService;
private readonly IMessagingService _messagingService;
private bool _autofillDisableSavePrompt;
private bool _autofillSavePrompt;
private string _autofillBlacklistedUris;
private bool _disableFavicon;
private bool _disableAutoTotpCopy;
private bool _favicon;
private bool _autoTotpCopy;
private int _clearClipboardSelectedIndex;
private int _themeSelectedIndex;
private int _autoDarkThemeSelectedIndex;
@@ -31,7 +30,6 @@ namespace Bit.App.Pages
public OptionsPageViewModel()
{
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
@@ -133,38 +131,38 @@ namespace Bit.App.Pages
}
}
public bool DisableFavicon
public bool Favicon
{
get => _disableFavicon;
get => _favicon;
set
{
if (SetProperty(ref _disableFavicon, value))
if (SetProperty(ref _favicon, value))
{
UpdateDisableFaviconAsync().FireAndForget();
UpdateFaviconAsync().FireAndForget();
}
}
}
public bool DisableAutoTotpCopy
public bool AutoTotpCopy
{
get => _disableAutoTotpCopy;
get => _autoTotpCopy;
set
{
if (SetProperty(ref _disableAutoTotpCopy, value))
if (SetProperty(ref _autoTotpCopy, value))
{
UpdateAutoTotpCopyAsync().FireAndForget();
}
}
}
public bool AutofillDisableSavePrompt
public bool AutofillSavePrompt
{
get => _autofillDisableSavePrompt;
get => _autofillSavePrompt;
set
{
if (SetProperty(ref _autofillDisableSavePrompt, value))
if (SetProperty(ref _autofillSavePrompt, value))
{
UpdateAutofillDisableSavePromptAsync().FireAndForget();
UpdateAutofillSavePromptAsync().FireAndForget();
}
}
}
@@ -183,11 +181,11 @@ namespace Bit.App.Pages
public async Task InitAsync()
{
AutofillDisableSavePrompt = (await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
var blacklistedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
AutofillBlacklistedUris = blacklistedUrisList != null ? string.Join(", ", blacklistedUrisList) : null;
DisableAutoTotpCopy = !(await _totpService.IsAutoCopyEnabledAsync());
DisableFavicon = (await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
var theme = await _stateService.GetThemeAsync();
ThemeSelectedIndex = ThemeOptions.FindIndex(k => k.Key == theme);
var autoDarkTheme = await _stateService.GetAutoDarkThemeAsync() ?? "dark";
@@ -204,15 +202,17 @@ namespace Bit.App.Pages
{
if (_inited)
{
await _stateService.SetDisableAutoTotpCopyAsync(DisableAutoTotpCopy);
// TODO: [PS-961] Fix negative function names
await _stateService.SetDisableAutoTotpCopyAsync(!AutoTotpCopy);
}
}
private async Task UpdateDisableFaviconAsync()
private async Task UpdateFaviconAsync()
{
if (_inited)
{
await _stateService.SetDisableFaviconAsync(DisableFavicon);
// TODO: [PS-961] Fix negative function names
await _stateService.SetDisableFaviconAsync(!Favicon);
}
}
@@ -243,11 +243,12 @@ namespace Bit.App.Pages
}
}
private async Task UpdateAutofillDisableSavePromptAsync()
private async Task UpdateAutofillSavePromptAsync()
{
if (_inited)
{
await _stateService.SetAutofillDisableSavePromptAsync(AutofillDisableSavePrompt);
// TODO: [PS-961] Fix negative function names
await _stateService.SetAutofillDisableSavePromptAsync(!AutofillSavePrompt);
}
}

View File

@@ -2,18 +2,13 @@
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Pages.Accounts;
using Bit.App.Resources;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class SettingsPage : BaseContentPage
{
private readonly IDeviceActionService _deviceActionService;
private readonly TabsPage _tabsPage;
private SettingsPageViewModel _vm;
@@ -21,7 +16,6 @@ namespace Bit.App.Pages
{
_tabsPage = tabsPage;
InitializeComponent();
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_vm = BindingContext as SettingsPageViewModel;
_vm.Page = this;
}
@@ -67,122 +61,12 @@ namespace Bit.App.Pages
}
}
private async void RowSelected(object sender, SelectionChangedEventArgs e)
private void RowSelected(object sender, SelectionChangedEventArgs e)
{
((ExtendedCollectionView)sender).SelectedItem = null;
if (!DoOnce())
if (e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item)
{
return;
}
if (!(e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item))
{
return;
}
if (item.Name == AppResources.Sync)
{
await Navigation.PushModalAsync(new NavigationPage(new SyncPage()));
}
else if (item.Name == AppResources.AutofillServices)
{
await Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(this)));
}
else if (item.Name == AppResources.PasswordAutofill)
{
await Navigation.PushModalAsync(new NavigationPage(new AutofillPage()));
}
else if (item.Name == AppResources.AppExtension)
{
await Navigation.PushModalAsync(new NavigationPage(new ExtensionPage()));
}
else if (item.Name == AppResources.Options)
{
await Navigation.PushModalAsync(new NavigationPage(new OptionsPage()));
}
else if (item.Name == AppResources.Folders)
{
await Navigation.PushModalAsync(new NavigationPage(new FoldersPage()));
}
else if (item.Name == AppResources.About)
{
await _vm.AboutAsync();
}
else if (item.Name == AppResources.HelpAndFeedback)
{
_vm.Help();
}
else if (item.Name == AppResources.FingerprintPhrase)
{
await _vm.FingerprintAsync();
}
else if (item.Name == AppResources.RateTheApp)
{
_vm.Rate();
}
else if (item.Name == AppResources.ImportItems)
{
_vm.Import();
}
else if (item.Name == AppResources.ExportVault)
{
await Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()));
}
else if (item.Name == AppResources.LearnOrg)
{
await _vm.ShareAsync();
}
else if (item.Name == AppResources.WebVault)
{
_vm.WebVault();
}
else if (item.Name == AppResources.ChangeMasterPassword)
{
await _vm.ChangePasswordAsync();
}
else if (item.Name == AppResources.TwoStepLogin)
{
await _vm.TwoStepAsync();
}
else if (item.Name == AppResources.LogOut)
{
await _vm.LogOutAsync();
}
else if (item.Name == AppResources.DeleteAccount)
{
await Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage()));
}
else if (item.Name == AppResources.LockNow)
{
await _vm.LockAsync();
}
else if (item.Name == AppResources.VaultTimeout)
{
await _vm.VaultTimeoutAsync();
}
else if (item.Name == AppResources.VaultTimeoutAction)
{
await _vm.VaultTimeoutActionAsync();
}
else if (item.Name == AppResources.UnlockWithPIN)
{
await _vm.UpdatePinAsync();
}
else 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();
}
_vm?.ExecuteSettingItemCommand.Execute(item);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Bit.App.Resources;
using Bit.App.Utilities;
using Xamarin.Forms;
@@ -12,6 +13,8 @@ namespace Bit.App.Pages
public string SubLabel { get; set; }
public TimeSpan? Time { get; set; }
public bool UseFrame { get; set; }
public Func<Task> ExecuteAsync { get; set; }
public bool SubLabelTextEnabled => SubLabel == AppResources.Enabled;
public string LineBreakMode => SubLabel == null ? "TailTruncation" : "";
public bool ShowSubLabel => SubLabel.Length != 0;

View File

@@ -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,11 +30,13 @@ 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;
@@ -84,10 +86,14 @@ 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();
@@ -117,6 +123,7 @@ namespace Bit.App.Pages
var pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
_pin = pinSet.Item1 || pinSet.Item2;
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
_screenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
if (_vaultTimeoutDisplayValue == null)
{
@@ -257,6 +264,17 @@ namespace Bit.App.Pages
}
var cleanSelection = selection.Replace("✓ ", string.Empty);
var selectionOption = _vaultTimeouts.FirstOrDefault(o => o.Key == cleanSelection);
// Check if the selected Timeout action is "Never" and if it's different from the previous selected value
if (selectionOption.Value == null && selectionOption.Value != oldTimeout)
{
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.NeverLockWarning,
AppResources.Warning, AppResources.Yes, AppResources.Cancel);
if (!confirmed)
{
return;
}
}
_vaultTimeoutDisplayValue = selectionOption.Key;
newTimeout = selectionOption.Value;
}
@@ -423,6 +441,8 @@ namespace Bit.App.Pages
public void BuildList()
{
//TODO: Refactor this once navigation is abstracted so that it doesn't depend on Page, e.g. Page.Navigation.PushModalAsync...
var doUpper = Device.RuntimePlatform != Device.Android;
var autofillItems = new List<SettingsPageListItem>();
if (Device.RuntimePlatform == Device.Android)
@@ -430,38 +450,69 @@ namespace Bit.App.Pages
autofillItems.Add(new SettingsPageListItem
{
Name = AppResources.AutofillServices,
SubLabel = _deviceActionService.AutofillServicesEnabled() ?
AppResources.Enabled : AppResources.Disabled
SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.Enabled : AppResources.Disabled,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage)))
});
}
else
{
if (_deviceActionService.SystemMajorVersion() >= 12)
{
autofillItems.Add(new SettingsPageListItem { Name = AppResources.PasswordAutofill });
autofillItems.Add(new SettingsPageListItem
{
Name = AppResources.PasswordAutofill,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillPage()))
});
}
autofillItems.Add(new SettingsPageListItem { Name = AppResources.AppExtension });
autofillItems.Add(new SettingsPageListItem
{
Name = AppResources.AppExtension,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExtensionPage()))
});
}
var manageItems = new List<SettingsPageListItem>
{
new SettingsPageListItem { Name = AppResources.Folders },
new SettingsPageListItem { Name = AppResources.Sync, SubLabel = _lastSyncDate }
new SettingsPageListItem
{
Name = AppResources.Folders,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new FoldersPage()))
},
new SettingsPageListItem
{
Name = AppResources.Sync,
SubLabel = _lastSyncDate,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new SyncPage()))
}
};
var securityItems = new List<SettingsPageListItem>
{
new SettingsPageListItem { Name = AppResources.VaultTimeout, SubLabel = _vaultTimeoutDisplayValue },
new SettingsPageListItem
{
Name = AppResources.VaultTimeout,
SubLabel = _vaultTimeoutDisplayValue,
ExecuteAsync = () => VaultTimeoutAsync() },
new SettingsPageListItem
{
Name = AppResources.VaultTimeoutAction,
SubLabel = _vaultTimeoutActionDisplayValue
SubLabel = _vaultTimeoutActionDisplayValue,
ExecuteAsync = () => VaultTimeoutActionAsync()
},
new SettingsPageListItem
{
Name = AppResources.UnlockWithPIN,
SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled
SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled,
ExecuteAsync = () => UpdatePinAsync()
},
new SettingsPageListItem { Name = AppResources.LockNow },
new SettingsPageListItem { Name = AppResources.TwoStepLogin }
new SettingsPageListItem
{
Name = AppResources.LockNow,
ExecuteAsync = () => LockAsync()
},
new SettingsPageListItem
{
Name = AppResources.TwoStepLogin,
ExecuteAsync = () => TwoStepAsync()
}
};
if (_supportsBiometric || _biometric)
{
@@ -474,7 +525,8 @@ namespace Bit.App.Pages
var item = new SettingsPageListItem
{
Name = string.Format(AppResources.UnlockWith, biometricName),
SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled
SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled,
ExecuteAsync = () => UpdateBiometricAsync()
};
securityItems.Insert(2, item);
}
@@ -497,40 +549,98 @@ namespace Bit.App.Pages
UseFrame = true,
});
}
if (Device.RuntimePlatform == Device.Android)
{
securityItems.Add(new SettingsPageListItem
{
Name = AppResources.AllowScreenCapture,
SubLabel = _screenCaptureAllowed ? AppResources.Enabled : AppResources.Disabled,
ExecuteAsync = () => SetScreenCaptureAllowedAsync()
});
}
var accountItems = new List<SettingsPageListItem>
{
new SettingsPageListItem { Name = AppResources.FingerprintPhrase },
new SettingsPageListItem { Name = AppResources.LogOut }
new SettingsPageListItem
{
Name = AppResources.FingerprintPhrase,
ExecuteAsync = () => FingerprintAsync()
},
new SettingsPageListItem
{
Name = AppResources.LogOut,
ExecuteAsync = () => LogOutAsync()
}
};
if (_showChangeMasterPassword)
{
accountItems.Insert(0, new SettingsPageListItem { Name = AppResources.ChangeMasterPassword });
accountItems.Insert(0, new SettingsPageListItem
{
Name = AppResources.ChangeMasterPassword,
ExecuteAsync = () => ChangePasswordAsync()
});
}
var toolsItems = new List<SettingsPageListItem>
{
new SettingsPageListItem { Name = AppResources.ImportItems },
new SettingsPageListItem { Name = AppResources.ExportVault }
new SettingsPageListItem
{
Name = AppResources.ImportItems,
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Import())
},
new SettingsPageListItem
{
Name = AppResources.ExportVault,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()))
}
};
if (IncludeLinksWithSubscriptionInfo())
{
toolsItems.Add(new SettingsPageListItem { Name = AppResources.LearnOrg });
toolsItems.Add(new SettingsPageListItem { Name = AppResources.WebVault });
toolsItems.Add(new SettingsPageListItem
{
Name = AppResources.LearnOrg,
ExecuteAsync = () => ShareAsync()
});
toolsItems.Add(new SettingsPageListItem
{
Name = AppResources.WebVault,
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => WebVault())
});
}
var otherItems = new List<SettingsPageListItem>
{
new SettingsPageListItem { Name = AppResources.Options },
new SettingsPageListItem { Name = AppResources.About },
new SettingsPageListItem { Name = AppResources.HelpAndFeedback },
new SettingsPageListItem
{
Name = AppResources.Options,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new OptionsPage()))
},
new SettingsPageListItem
{
Name = AppResources.About,
ExecuteAsync = () => AboutAsync()
},
new SettingsPageListItem
{
Name = AppResources.HelpAndFeedback,
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Help())
},
#if !FDROID
new SettingsPageListItem
{
Name = AppResources.SubmitCrashLogs,
SubLabel = _reportLoggingEnabled ? AppResources.Enabled : AppResources.Disabled,
ExecuteAsync = () => LoggerReportingAsync()
},
#endif
new SettingsPageListItem { Name = AppResources.RateTheApp },
new SettingsPageListItem { Name = AppResources.DeleteAccount }
new SettingsPageListItem
{
Name = AppResources.RateTheApp,
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Rate())
},
new SettingsPageListItem
{
Name = AppResources.DeleteAccount,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage()))
}
};
// TODO: improve this. Leaving this as is to reduce error possibility on the hotfix.
@@ -610,5 +720,33 @@ 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);
}
}
}
}

View File

@@ -184,31 +184,61 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n AuthenticatorKey}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Frame
IsVisible="{Binding HasTotpValue, Converter={StaticResource inverseBool}}"
Margin="0,5,0,0"
StyleClass="btn-icon-row"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
Padding="0"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3">
<Frame.GestureRecognizers>
<TapGestureRecognizer Tapped="ScanTotp_Clicked" />
</Frame.GestureRecognizers>
<controls:IconLabel
Text="{Binding SetupTotpText}"
Padding="0,15"
HorizontalOptions="Center"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center" />
</Frame>
<controls:MonoEntry
x:Name="_loginTotpEntry"
Text="{Binding Cipher.Login.Totp}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
IsVisible="{Binding HasTotpValue}"
IsPassword="{Binding Cipher.ViewPassword, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding Cipher.ViewPassword}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="{Binding TotpColumnSpan}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
IsVisible="{Binding HasTotpValue}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Camera}}"
Clicked="ScanTotp_Clicked"
Grid.Row="0"
Grid.Column="1"
Grid.Column="2"
Grid.RowSpan="2"
IsVisible="{Binding Cipher.ViewPassword}"
IsVisible="{Binding HasTotpValue}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ScanQrTitle}" />
</Grid>

View File

@@ -11,6 +11,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -29,6 +30,7 @@ namespace Bit.App.Pages
private readonly IEventService _eventService;
private readonly IPolicyService _policyService;
private readonly ILogger _logger;
private readonly IClipboardService _clipboardService;
private CipherView _cipher;
private bool _showNotesSeparator;
@@ -53,6 +55,7 @@ namespace Bit.App.Pages
nameof(ShowUris),
nameof(ShowAttachments),
nameof(ShowCollections),
nameof(HasTotpValue)
};
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
new List<KeyValuePair<UriMatchType?, string>>
@@ -80,6 +83,7 @@ namespace Bit.App.Pages
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
@@ -89,6 +93,7 @@ namespace Bit.App.Pages
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
FieldOptionsCommand = new Command<AddEditPageFieldViewModel>(FieldOptions);
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
Uris = new ExtendedObservableCollection<LoginUriView>();
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
Collections = new ExtendedObservableCollection<CollectionViewModel>();
@@ -150,6 +155,7 @@ namespace Bit.App.Pages
public Command UriOptionsCommand { get; set; }
public Command FieldOptionsCommand { get; set; }
public Command PasswordPromptHelpCommand { get; set; }
public AsyncCommand CopyCommand { get; set; }
public string CipherId { get; set; }
public string OrganizationId { get; set; }
public string FolderId { get; set; }
@@ -300,7 +306,8 @@ 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;
@@ -858,6 +865,19 @@ namespace Bit.App.Pages
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
}
}
private async Task CopyTotpClipboardAsync()
{
try
{
await _clipboardService.CopyTextAsync(_cipher.Login.Totp);
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.AuthenticatorKeyScanner));
}
catch (Exception ex)
{
_logger.Exception(ex);
}
}
}
public class AddEditPageFieldViewModel : ExtendedViewModel

View File

@@ -6,6 +6,7 @@
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}"
@@ -53,6 +54,14 @@
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" />
</DataTemplate>
<DataTemplate x:Key="authenticatorTemplate"
x:DataType="pages:GroupingsPageTOTPListItem">
<controls:AuthenticatorViewCell
Cipher="{Binding Cipher}"
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}"
TotpSec="{Binding TotpSec}"/>
</DataTemplate>
<DataTemplate x:Key="groupTemplate"
x:DataType="pages:GroupingsPageListItem">
<controls:ExtendedStackLayout Orientation="Horizontal"
@@ -104,6 +113,7 @@
<pages:GroupingsPageListItemSelector x:Key="listItemDataTemplateSelector"
HeaderTemplate="{StaticResource headerTemplate}"
CipherTemplate="{StaticResource cipherTemplate}"
AuthenticatorTemplate="{StaticResource authenticatorTemplate}"
GroupTemplate="{StaticResource groupTemplate}" />
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
@@ -131,6 +141,29 @@
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"

View File

@@ -189,10 +189,11 @@ namespace Bit.App.Pages
return false;
}
protected override void OnDisappearing()
protected override async void OnDisappearing()
{
base.OnDisappearing();
IsBusy = false;
_vm.StopCiphersTotpTick();
_broadcasterService.Unsubscribe(_pageName);
_vm.DisableRefreshing();
_accountAvatar?.OnDisappearing();
@@ -205,6 +206,13 @@ namespace Bit.App.Pages
{
return;
}
if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageTOTPListItem totpItem)
{
await _vm.SelectCipherAsync(totpItem.Cipher);
return;
}
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
{
return;

View File

@@ -2,13 +2,13 @@
namespace Bit.App.Pages
{
public class GroupingsPageListGroup : List<GroupingsPageListItem>
public class GroupingsPageListGroup : List<IGroupingsPageListItem>
{
public GroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false)
: this(new List<GroupingsPageListItem>(), name, count, doUpper, first)
: this(new List<IGroupingsPageListItem>(), name, count, doUpper, first)
{ }
public GroupingsPageListGroup(List<GroupingsPageListItem> groupItems, string name, int count,
public GroupingsPageListGroup(IEnumerable<IGroupingsPageListItem> groupItems, string name, int count,
bool doUpper = true, bool first = false)
{
AddRange(groupItems);

View File

@@ -7,6 +7,7 @@ namespace Bit.App.Pages
public DataTemplate HeaderTemplate { get; set; }
public DataTemplate CipherTemplate { get; set; }
public DataTemplate GroupTemplate { get; set; }
public DataTemplate AuthenticatorTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
@@ -15,10 +16,16 @@ namespace Bit.App.Pages
return HeaderTemplate;
}
if (item is GroupingsPageTOTPListItem)
{
return AuthenticatorTemplate;
}
if (item is GroupingsPageListItem listItem)
{
return listItem.Cipher != null ? CipherTemplate : GroupTemplate;
}
return null;
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Threading.Tasks;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class GroupingsPageTOTPListItem : ExtendedViewModel, IGroupingsPageListItem
{
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private readonly ITotpService _totpService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IClipboardService _clipboardService;
private CipherView _cipher;
private bool _websiteIconsEnabled;
private string _iconImageSource = string.Empty;
public int interval { get; set; }
private double _progress;
private string _totpSec;
private string _totpCodeFormatted;
private TotpHelper _totpTickHelper;
public GroupingsPageTOTPListItem(CipherView cipherView, bool websiteIconsEnabled)
{
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
Cipher = cipherView;
WebsiteIconsEnabled = websiteIconsEnabled;
interval = _totpService.GetTimeInterval(Cipher.Login.Totp);
CopyCommand = new AsyncCommand(CopyToClipboardAsync,
onException: ex => _logger.Value.Exception(ex),
allowsMultipleExecutions: false);
_totpTickHelper = new TotpHelper(cipherView);
}
public AsyncCommand CopyCommand { get; set; }
public CipherView Cipher
{
get => _cipher;
set => SetProperty(ref _cipher, value);
}
public string TotpCodeFormatted
{
get => _totpCodeFormatted;
set => SetProperty(ref _totpCodeFormatted, value,
additionalPropertyNames: new string[]
{
nameof(TotpCodeFormattedStart),
nameof(TotpCodeFormattedEnd),
});
}
public string TotpSec
{
get => _totpSec;
set => SetProperty(ref _totpSec, value);
}
public double Progress
{
get => _progress;
set => SetProperty(ref _progress, value);
}
public bool WebsiteIconsEnabled
{
get => _websiteIconsEnabled;
set => SetProperty(ref _websiteIconsEnabled, value);
}
public bool ShowIconImage
{
get => WebsiteIconsEnabled
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
&& IconImageSource != null;
}
public string IconImageSource
{
get
{
if (_iconImageSource == string.Empty) // default value since icon source can return null
{
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
}
return _iconImageSource;
}
}
public string TotpCodeFormattedStart => TotpCodeFormatted?.Split(' ')[0];
public string TotpCodeFormattedEnd => TotpCodeFormatted?.Split(' ')[1];
public async Task CopyToClipboardAsync()
{
await _clipboardService.CopyTextAsync(TotpCodeFormatted);
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
}
public async Task TotpTickAsync()
{
await _totpTickHelper.GenerateNewTotpValues();
MainThread.BeginInvokeOnMainThread(() =>
{
TotpSec = _totpTickHelper.TotpSec;
Progress = _totpTickHelper.Progress;
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
});
}
}
}

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
@@ -29,13 +31,16 @@ namespace Bit.App.Pages
private bool _showList;
private bool _websiteIconsEnabled;
private bool _syncRefreshing;
private bool _showTotpFilter;
private bool _totpFilterEnable;
private string _noDataText;
private List<CipherView> _allCiphers;
private Dictionary<string, int> _folderCounts = new Dictionary<string, int>();
private Dictionary<string, int> _collectionCounts = new Dictionary<string, int>();
private Dictionary<CipherType, int> _typeCounts = new Dictionary<CipherType, int>();
private int _deletedCount = 0;
private CancellationTokenSource _totpTickCts;
private Task _totpTickTask;
private readonly ICipherService _cipherService;
private readonly IFolderService _folderService;
private readonly ICollectionService _collectionService;
@@ -74,6 +79,12 @@ namespace Bit.App.Pages
await LoadAsync();
});
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
onException: ex => _logger.Exception(ex),
allowsMultipleExecutions: false);
TotpFilterCommand = new AsyncCommand(LoadAsync,
onException: ex => _logger.Exception(ex),
allowsMultipleExecutions: false);
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
{
@@ -90,6 +101,9 @@ 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());
@@ -151,12 +165,21 @@ namespace Bit.App.Pages
get => _websiteIconsEnabled;
set => SetProperty(ref _websiteIconsEnabled, value);
}
public bool ShowTotpFilter
{
get => _showTotpFilter;
set => SetProperty(ref _showTotpFilter, value);
}
public bool TotpFilterEnable
{
get => _totpFilterEnable;
set => SetProperty(ref _totpFilterEnable, value);
}
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
public Command RefreshCommand { get; set; }
public Command<CipherView> CipherOptionsCommand { get; set; }
public ICommand TotpFilterCommand { get; }
public bool LoadedOnce { get; set; }
public async Task LoadAsync()
@@ -181,18 +204,22 @@ namespace Bit.App.Pages
return;
}
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget();
await InitVaultFilterAsync(MainPage);
if (MainPage)
{
PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault;
}
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
_doingLoad = true;
LoadedOnce = true;
ShowNoData = false;
Loading = true;
ShowList = false;
ShowAddCipherButton = !Deleted;
ShowTotpFilter = Type == CipherType.Login && canAccessPremium;
var groupedItems = new List<GroupingsPageListGroup>();
var page = Page as GroupingsPage;
@@ -272,10 +299,7 @@ namespace Bit.App.Pages
}
if (Ciphers?.Any() ?? false)
{
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted)
.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
CreateCipherGroupedItems(groupedItems);
}
if (ShowNoFolderCipherGroup)
{
@@ -363,6 +387,44 @@ 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;

View File

@@ -1,13 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage
<?xml version="1.0" encoding="utf-8"?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.ScanPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:forms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms"
x:Name="_page"
Title="{u:I18n ScanQrTitle}">
Title="{Binding ScanQrPageTitle}">
<ContentPage.BindingContext>
<pages:ScanPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
@@ -16,67 +29,114 @@
<Grid
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<zxing:ZXingScannerView
x:Name="_zxing"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
AutomationId="zxingScannerView"
OnScanResult="OnScanResult">
</zxing:ZXingScannerView>
<Grid
VerticalOptions="FillAndExpand"
IsVisible="{Binding ShowScanner}"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="3"
OnScanResult="OnScanResult"/>
<StackLayout
VerticalOptions="Center"
HorizontalOptions="FillAndExpand"
AutomationId="zxingDefaultOverlay">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
IsVisible="{Binding ShowScanner}"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
Margin="30,0">
<BoxView
Grid.Column="0"
Grid.Row="0"
VerticalOptions="Fill"
HorizontalOptions="FillAndExpand"
BackgroundColor="Black"
Opacity="0.7" />
<Label
Text="{u:I18n CameraInstructionTop}"
AutomationId="zxingDefaultOverlay_TopTextLabel"
Grid.Column="0"
Grid.Row="0"
VerticalOptions="Center"
<forms:SKCanvasView
x:Name="SkCanvasView"
Margin="0,50,0,0"
WidthRequest="250"
HeightRequest="250"
IsVisible="{Binding ShowScanner}"
VerticalOptions="Center"
HorizontalOptions="Center"
TextColor="White" />
PaintSurface="OnCanvasViewPaintSurface"/>
<BoxView
Grid.Column="0"
Grid.Row="1"
VerticalOptions="Fill"
<controls:IconButton
x:Name="_checkIcon"
IsVisible="{Binding ShowScanner}"
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
HorizontalOptions="Center"
VerticalOptions="Start"
FontSize="Title"
TextColor="Transparent"/>
</StackLayout>
<BoxView
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
IsVisible="{Binding ShowScanner, Converter={StaticResource inverseBool}}"
BackgroundColor="{DynamicResource BackgroundColor}"/>
<StackLayout
VerticalOptions="Center"
HorizontalOptions="FillAndExpand"
BackgroundColor="Transparent" />
<BoxView
IsVisible="{Binding ShowScanner, Converter={StaticResource inverseBool}}"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
Margin="30,0">
<Label
Text="{u:I18n EnterKeyManually}"
FontSize="Title" />
<Label
Text="{u:I18n AuthenticatorKeyScanner}"
StyleClass="box-label" />
<controls:MonoEntry
x:Name="_authenticationKeyEntry"
Text="{Binding TotpAuthenticationKey}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
StyleClass="box-value" />
<Button
Text="{u:I18n AddTotp}"
StyleClass="box-button-row"
Clicked="AddAuthenticationKey_OnClicked"/>
</StackLayout>
<BoxView
Grid.Column="0"
Grid.Row="2"
VerticalOptions="Fill"
HorizontalOptions="FillAndExpand"
BackgroundColor="Black"
Opacity="0.7" />
<StackLayout
VerticalOptions="Start"
HorizontalOptions="Center"
Grid.Column="0"
Grid.Row="2">
<Label
Text="{u:I18n CameraInstructionBottom}"
AutomationId="zxingDefaultOverlay_BottomTextLabel"
Text="{Binding CameraInstructionTop}"
AutomationId="zxingDefaultOverlay_TopTextLabel"
Margin="30,15,30,0"
HorizontalOptions="Center"
HorizontalTextAlignment="Center"
StyleClass="text-sm"
TextColor="White" />
</StackLayout>
<Label
FormattedText="{Binding ToggleScanModeLabel}"
Grid.Column="0"
Grid.Row="2"
VerticalOptions="Center"
HorizontalOptions="Center"
TextColor="White" />
</Grid>
Margin="0,15"
StyleClass="text-sm"
FontAttributes="Bold"
VerticalOptions="End"
HorizontalOptions="Center" >
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="ToggleScanMode_OnTapped" />
</Label.GestureRecognizers>
</Label>
</Grid>
</pages:BaseContentPage>
</pages:BaseContentPage>

View File

@@ -1,19 +1,31 @@
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");
@@ -32,6 +44,12 @@ 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()
@@ -58,7 +76,14 @@ namespace Bit.App.Pages
{
if (!autofocusCts.IsCancellationRequested)
{
_zxing.AutoFocus();
try
{
_zxing.AutoFocus();
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
}
}
});
}
@@ -69,27 +94,83 @@ 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 void OnScanResult(ZXing.Result result)
private async void OnScanResult(ZXing.Result result)
{
// Stop analysis until we navigate away so we don't keep reading barcodes
_zxing.IsAnalyzing = false;
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);
_zxing.IsScanning = false;
var text = result?.Text;
}
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;
if (!string.IsNullOrWhiteSpace(text))
{
if (text.StartsWith("otpauth://totp"))
@@ -98,7 +179,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)
@@ -114,11 +195,77 @@ namespace Bit.App.Pages
_callback(null);
}
private async void Close_Clicked(object sender, System.EventArgs e)
private void ToggleScanMode_OnTapped(object sender, EventArgs e)
{
if (DoOnce())
ViewModel.ToggleScanModeCommand.Execute(null);
if (!ViewModel.ShowScanner)
{
await Navigation.PopModalAsync();
_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();
}
}
}

View File

@@ -0,0 +1,61 @@
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;
}
}
}
}

View File

@@ -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,10 +165,11 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="40" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
@@ -178,29 +179,49 @@
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" />
<Label
Text="{Binding TotpSec, Mode=OneWay}"
Style="{DynamicResource textTotp}"
Margin="0, 0, 10, 0"
Grid.Column="0"
VerticalTextAlignment="Start"
VerticalOptions="Start" />
<controls:CircularProgressbarView
Progress="{Binding TotpProgress}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
HorizontalOptions="End"
HorizontalTextAlignment="End"
VerticalOptions="CenterAndExpand" />
<controls:IconButton
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" />
<Label
Text="{Binding TotpSec, Mode=OneWay}"
Style="{DynamicResource textTotp}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
StyleClass="text-sm"
VerticalTextAlignment="Center"
HorizontalTextAlignment="Center"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" />
<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>
@@ -244,7 +265,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}"
@@ -253,7 +274,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}"
@@ -316,7 +337,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}"
@@ -325,7 +346,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}"
@@ -529,7 +550,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>
@@ -588,7 +609,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}"
@@ -636,7 +657,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}}"
@@ -700,4 +721,4 @@
</Button>
</AbsoluteLayout>
</pages:BaseContentPage>
</pages:BaseContentPage>

View File

@@ -112,7 +112,7 @@ namespace Bit.App.Pages
base.OnDisappearing();
IsBusy = false;
_broadcasterService.Unsubscribe(nameof(ViewPage));
_vm.CleanUp();
_vm.StopCiphersTotpTick();
}
private async void PasswordHistory_Tapped(object sender, System.EventArgs e)

View File

@@ -1,6 +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;
@@ -22,7 +23,6 @@ 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;
@@ -47,13 +47,15 @@ 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");
@@ -106,6 +108,7 @@ namespace Bit.App.Pages
nameof(ShowIdentityAddress),
nameof(IsDeleted),
nameof(CanEdit),
nameof(ShowUpgradePremiumTotpText)
});
}
public List<ViewPageFieldViewModel> Fields
@@ -207,21 +210,22 @@ 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) &&
!string.IsNullOrWhiteSpace(TotpCodeFormatted);
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp);
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 => _totpCodeFormatted;
get => _canAccessPremium ? _totpCodeFormatted : string.Empty;
set => SetProperty(ref _totpCodeFormatted, value,
additionalPropertyNames: new string[]
{
@@ -231,7 +235,11 @@ namespace Bit.App.Pages
public string TotpSec
{
get => _totpSec;
set => SetProperty(ref _totpSec, value);
set => SetProperty(ref _totpSec, value,
additionalPropertyNames: new string[]
{
nameof(TotpProgress)
});
}
public bool TotpLow
{
@@ -242,12 +250,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)
{
@@ -261,19 +269,10 @@ namespace Bit.App.Pages
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
(Cipher.OrganizationUseTotp || CanAccessPremium))
{
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;
});
_totpTickHelper = new TotpHelper(Cipher);
_totpTickCancellationToken?.Cancel();
_totpTickCancellationToken = new CancellationTokenSource();
_totpTickTask = new TimerTask(_logger, StartCiphersTotpTick, _totpTickCancellationToken).RunPeriodic();
}
if (_previousCipherId != CipherId)
{
@@ -284,9 +283,27 @@ namespace Bit.App.Pages
return true;
}
public void CleanUp()
private async void StartCiphersTotpTick()
{
_totpInterval = null;
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;
}
}
public async void TogglePassword()
@@ -414,47 +431,6 @@ 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())
@@ -640,7 +616,7 @@ namespace Bit.App.Pages
}
else if (id == "LoginTotp")
{
text = _totpCode;
text = TotpCodeFormatted.Replace(" ", string.Empty);
name = AppResources.VerificationCodeTotp;
}
else if (id == "LoginUri")
@@ -750,7 +726,7 @@ namespace Bit.App.Pages
{
if (IsBooleanType)
{
return _field.Value == "true" ? BitwardenIcons.Square : BitwardenIcons.CheckSquare;
return _field.Value == "true" ? BitwardenIcons.CheckSquare : BitwardenIcons.Square;
}
else if (IsLinkedType)
{

View File

@@ -1,6 +1,7 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -9,9 +10,10 @@
namespace Bit.App.Resources {
using System;
using System.Reflection;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.1.0.0")]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class AppResources {
@@ -351,6 +353,12 @@ namespace Bit.App.Resources {
}
}
public static string Authenticator {
get {
return ResourceManager.GetString("Authenticator", resourceCulture);
}
}
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
@@ -1443,15 +1451,9 @@ namespace Bit.App.Resources {
}
}
public static string CameraInstructionBottom {
public static string PointYourCameraAtTheQRCode {
get {
return ResourceManager.GetString("CameraInstructionBottom", resourceCulture);
}
}
public static string CameraInstructionTop {
get {
return ResourceManager.GetString("CameraInstructionTop", resourceCulture);
return ResourceManager.GetString("PointYourCameraAtTheQRCode", resourceCulture);
}
}
@@ -1479,15 +1481,15 @@ namespace Bit.App.Resources {
}
}
public static string DisableAutoTotpCopyDescription {
public static string CopyTotpAutomaticallyDescription {
get {
return ResourceManager.GetString("DisableAutoTotpCopyDescription", resourceCulture);
return ResourceManager.GetString("CopyTotpAutomaticallyDescription", resourceCulture);
}
}
public static string DisableAutoTotpCopy {
public static string CopyTotpAutomatically {
get {
return ResourceManager.GetString("DisableAutoTotpCopy", resourceCulture);
return ResourceManager.GetString("CopyTotpAutomatically", resourceCulture);
}
}
@@ -1917,15 +1919,15 @@ namespace Bit.App.Resources {
}
}
public static string DisableWebsiteIcons {
public static string ShowWebsiteIcons {
get {
return ResourceManager.GetString("DisableWebsiteIcons", resourceCulture);
return ResourceManager.GetString("ShowWebsiteIcons", resourceCulture);
}
}
public static string DisableWebsiteIconsDescription {
public static string ShowWebsiteIconsDescription {
get {
return ResourceManager.GetString("DisableWebsiteIconsDescription", resourceCulture);
return ResourceManager.GetString("ShowWebsiteIconsDescription", resourceCulture);
}
}
@@ -2757,15 +2759,15 @@ namespace Bit.App.Resources {
}
}
public static string DisableSavePrompt {
public static string AskToAddLogin {
get {
return ResourceManager.GetString("DisableSavePrompt", resourceCulture);
return ResourceManager.GetString("AskToAddLogin", resourceCulture);
}
}
public static string DisableSavePromptDescription {
public static string AskToAddLoginDescription {
get {
return ResourceManager.GetString("DisableSavePromptDescription", resourceCulture);
return ResourceManager.GetString("AskToAddLoginDescription", resourceCulture);
}
}
@@ -4034,5 +4036,113 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("All", resourceCulture);
}
}
public static string DisplayItemsContainingTOTP {
get {
return ResourceManager.GetString("DisplayItemsContainingTOTP", resourceCulture);
}
}
public static string PremiumSubscriptionRequired {
get {
return ResourceManager.GetString("PremiumSubscriptionRequired", resourceCulture);
}
}
public static string CannotAddAuthenticatorKey {
get {
return ResourceManager.GetString("CannotAddAuthenticatorKey", resourceCulture);
}
}
public static string ScanQRCode {
get {
return ResourceManager.GetString("ScanQRCode", resourceCulture);
}
}
public static string CannotScanQRCode {
get {
return ResourceManager.GetString("CannotScanQRCode", resourceCulture);
}
}
public static string AuthenticatorKeyScanner {
get {
return ResourceManager.GetString("AuthenticatorKeyScanner", resourceCulture);
}
}
public static string EnterKeyManually {
get {
return ResourceManager.GetString("EnterKeyManually", resourceCulture);
}
}
public static string AddTotp {
get {
return ResourceManager.GetString("AddTotp", resourceCulture);
}
}
public static string SetupTotp {
get {
return ResourceManager.GetString("SetupTotp", resourceCulture);
}
}
public static string OnceTheKeyIsSuccessfullyEntered {
get {
return ResourceManager.GetString("OnceTheKeyIsSuccessfullyEntered", resourceCulture);
}
}
public static string SelectAddTotpToStoreTheKeySafely {
get {
return ResourceManager.GetString("SelectAddTotpToStoreTheKeySafely", resourceCulture);
}
}
public static string NeverLockWarning {
get {
return ResourceManager.GetString("NeverLockWarning", resourceCulture);
}
}
public static string CipherListIsVisibleActivateToShowAuthenticationCodesList {
get {
return ResourceManager.GetString("CipherListIsVisibleActivateToShowAuthenticationCodesList", resourceCulture);
}
}
public static string AuthenticationCodesListIsVisibleActivateToShowCipherList {
get {
return ResourceManager.GetString("AuthenticationCodesListIsVisibleActivateToShowCipherList", resourceCulture);
}
}
public static string EnvironmentPageUrlsError {
get {
return ResourceManager.GetString("EnvironmentPageUrlsError", resourceCulture);
}
}
public static string GenericErrorMessage {
get {
return ResourceManager.GetString("GenericErrorMessage", resourceCulture);
}
}
public static string AllowScreenCapture {
get {
return ResourceManager.GetString("AllowScreenCapture", resourceCulture);
}
}
public static string AreYouSureYouWantToEnableScreenCapture {
get {
return ResourceManager.GetString("AreYouSureYouWantToEnableScreenCapture", resourceCulture);
}
}
}
}

View File

@@ -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,6 +299,10 @@
<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>
@@ -412,7 +416,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>
@@ -516,7 +520,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>
@@ -549,10 +553,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>
@@ -647,7 +651,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>
@@ -701,7 +705,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>
@@ -710,7 +714,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>
@@ -889,11 +893,9 @@
<data name="AuthenticatorKeyReadError" xml:space="preserve">
<value>Cannot read authenticator key.</value>
</data>
<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 name="PointYourCameraAtTheQRCode" xml:space="preserve">
<value>Point your camera at the QR Code.
Scanning will happen automatically.</value>
</data>
<data name="ScanQrTitle" xml:space="preserve">
<value>Scan QR Code</value>
@@ -907,11 +909,11 @@
<data name="CopyTotp" xml:space="preserve">
<value>Copy TOTP</value>
</data>
<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 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>
<data name="DisableAutoTotpCopy" xml:space="preserve">
<value>Disable Automatic TOTP Copy</value>
<data name="CopyTotpAutomatically" xml:space="preserve">
<value>Copy TOTP automatically</value>
</data>
<data name="PremiumRequired" xml:space="preserve">
<value>A premium membership is required to use this feature.</value>
@@ -1128,11 +1130,11 @@
<data name="Expiration" xml:space="preserve">
<value>Expiration</value>
</data>
<data name="DisableWebsiteIcons" xml:space="preserve">
<value>Disable Website Icons</value>
<data name="ShowWebsiteIcons" xml:space="preserve">
<value>Show website icons</value>
</data>
<data name="DisableWebsiteIconsDescription" xml:space="preserve">
<value>Website Icons provide a recognizable image next to each login item in your vault.</value>
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
<value>Show a recognizable image next to each login.</value>
</data>
<data name="IconsUrl" xml:space="preserve">
<value>Icons Server URL</value>
@@ -1311,7 +1313,7 @@
<value>5. Select "Bitwarden"</value>
</data>
<data name="PasswordAutofill" xml:space="preserve">
<value>Password AutoFill</value>
<value>Password auto-fill</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>
@@ -1449,7 +1451,7 @@
<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">
@@ -1460,10 +1462,10 @@
<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>
@@ -1517,7 +1519,7 @@
<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">
@@ -1525,7 +1527,7 @@
<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">
@@ -1573,14 +1575,14 @@
<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="DisableSavePrompt" xml:space="preserve">
<value>Disable Save Prompt</value>
<data name="AskToAddLogin" xml:space="preserve">
<value>Ask to add login</value>
</data>
<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 name="AskToAddLoginDescription" xml:space="preserve">
<value>Ask to add an item if one isn't found in your vault.</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>
@@ -2159,13 +2161,13 @@
<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 associated data will be erased and unrecoverable. Are you sure you want to continue?</value>
<value>Your account and all vault 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>
@@ -2254,4 +2256,59 @@
<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>

View File

@@ -79,9 +79,34 @@
<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}" />

View File

@@ -505,4 +505,17 @@
</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>

View File

@@ -71,4 +71,6 @@
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
<Color x:Key="HyperlinkColor">#52bdfb</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

@@ -71,4 +71,6 @@
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
<Color x:Key="HyperlinkColor">#52bdfb</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

@@ -71,4 +71,6 @@
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
<Color x:Key="HyperlinkColor">#175DDC</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

@@ -71,4 +71,6 @@
<Color x:Key="NavigationBarTextColor">#e5e9f0</Color>
<Color x:Key="HyperlinkColor">#81a1c1</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

@@ -92,6 +92,32 @@
<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 -->

View File

@@ -19,6 +19,7 @@ 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;
@@ -28,7 +29,8 @@ namespace Bit.App.Utilities.AccountManagement
IStorageService secureStorageService,
IStateService stateService,
IPlatformUtilsService platformUtilsService,
IAuthService authService)
IAuthService authService,
ILogger logger)
{
_broadcasterService = broadcasterService;
_vaultTimeoutService = vaultTimeoutService;
@@ -36,6 +38,7 @@ namespace Bit.App.Utilities.AccountManagement
_stateService = stateService;
_platformUtilsService = platformUtilsService;
_authService = authService;
_logger = logger;
}
private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true };
@@ -109,42 +112,49 @@ namespace Bit.App.Utilities.AccountManagement
private async void OnMessage(Message message)
{
switch (message.Command)
try
{
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;
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);
}
}
private void Locked(Tuple<string, bool> extras)
private async Task LockedAsync(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);
@@ -163,27 +173,19 @@ namespace Bit.App.Utilities.AccountManagement
await _accountsManagerHost.SetPreviousPageInfoAsync();
Device.BeginInvokeOnMainThread(() => _accountsManagerHost.Navigate(NavigationTarget.Lock, new LockNavigationParams(autoPromptBiometric)));
await Device.InvokeOnMainThreadAsync(() => _accountsManagerHost.Navigate(NavigationTarget.Lock, new LockNavigationParams(autoPromptBiometric)));
}
private void AddAccount()
private async Task AddAccountAsync()
{
Device.BeginInvokeOnMainThread(() =>
await Device.InvokeOnMainThreadAsync(() =>
{
Options.HideAccountSwitcher = false;
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin);
});
}
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)
public async Task LogOutAsync(string userId, bool userInitiated, bool expired)
{
await AppHelpers.LogOutAsync(userId, userInitiated);
await NavigateOnAccountChangeAsync();

View File

@@ -0,0 +1,56 @@
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);
}
}
}

View File

@@ -0,0 +1,57 @@
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));
}
}
}

View File

@@ -148,6 +148,8 @@ 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);
}
}

View File

@@ -6,6 +6,5 @@ namespace Bit.Core.Abstractions
{
Task<string> GetCodeAsync(string key);
int GetTimeInterval(string key);
Task<bool> IsAutoCopyEnabledAsync();
}
}

View File

@@ -94,11 +94,13 @@ 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

View File

@@ -2,6 +2,7 @@
{
public class OrganizationUserResetPasswordEnrollmentRequest
{
public string MasterPasswordHash { get; set; }
public string ResetPasswordKey { get; set; }
}
}

View File

@@ -589,7 +589,19 @@ 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();

View File

@@ -6,6 +6,7 @@ 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
{
@@ -173,7 +174,7 @@ namespace Bit.Core.Services
public void LogOut(Action callback)
{
callback.Invoke();
_messagingService.Send("loggedOut");
_messagingService.Send(AccountsManagerMessageCommands.LOGGED_OUT);
}
public List<TwoFactorProvider> GetSupportedTwoFactorProviders()

View File

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

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
@@ -558,6 +559,27 @@ 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();
@@ -1461,6 +1483,7 @@ 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

View File

@@ -10,14 +10,11 @@ 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;
}
@@ -132,11 +129,5 @@ namespace Bit.Core.Services
}
return period;
}
public async Task<bool> IsAutoCopyEnabledAsync()
{
var disabled = await _stateService.GetDisableAutoTotpCopyAsync();
return !disabled.GetValueOrDefault();
}
}
}

View File

@@ -34,6 +34,25 @@ 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);

View File

@@ -74,7 +74,7 @@ namespace Bit.Core.Utilities
});
var passwordGenerationService = new PasswordGenerationService(cryptoService, stateService,
cryptoFunctionService, policyService);
var totpService = new TotpService(stateService, cryptoFunctionService);
var totpService = new TotpService(cryptoFunctionService);
var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService,
tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService,
keyConnectorService);

View File

@@ -8,10 +8,12 @@ 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;
@@ -36,88 +38,128 @@ namespace Bit.iOS.Autofill
public override void ViewDidLoad()
{
InitApp();
base.ViewDidLoad();
Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png");
View.BackgroundColor = ThemeHelpers.SplashBackgroundColor;
_context = new Context
try
{
ExtContext = ExtensionContext
};
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;
}
}
public override async void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers)
{
InitAppIfNeeded();
_context.ServiceIdentifiers = serviceIdentifiers;
if (serviceIdentifiers.Length > 0)
try
{
var uri = serviceIdentifiers[0].Identifier;
if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain)
InitAppIfNeeded();
_context.ServiceIdentifiers = serviceIdentifiers;
if (serviceIdentifiers.Length > 0)
{
uri = string.Concat("https://", uri);
var uri = serviceIdentifiers[0].Identifier;
if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain)
{
uri = string.Concat("https://", uri);
}
_context.UrlString = uri;
}
_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)
if (!await IsAuthed())
{
PerformSegue("loginSearchSegue", this);
await _accountsManager.NavigateOnAccountChangeAsync(false);
}
else if (await IsLocked())
{
PerformSegue("lockPasswordSegue", this);
}
else
{
PerformSegue("loginListSegue", this);
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
{
PerformSegue("loginSearchSegue", this);
}
else
{
PerformSegue("loginListSegue", this);
}
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
}
}
public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity)
{
InitAppIfNeeded();
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
if (!await IsAuthed() || await IsLocked())
try
{
var err = new NSError(new NSString("ASExtensionErrorDomain"),
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
ExtensionContext.CancelRequest(err);
return;
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;
}
_context.CredentialIdentity = credentialIdentity;
await ProvideCredentialAsync(false);
}
public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity)
{
InitAppIfNeeded();
if (!await IsAuthed())
try
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
return;
InitAppIfNeeded();
if (!await IsAuthed())
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
return;
}
_context.CredentialIdentity = credentialIdentity;
await CheckLockAsync(async () => await ProvideCredentialAsync());
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
}
_context.CredentialIdentity = credentialIdentity;
CheckLock(async () => await ProvideCredentialAsync());
}
public override async void PrepareInterfaceForExtensionConfiguration()
{
InitAppIfNeeded();
_context.Configuring = true;
if (!await IsAuthed())
try
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
return;
InitAppIfNeeded();
_context.Configuring = true;
if (!await IsAuthed())
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
return;
}
await CheckLockAsync(() => PerformSegue("setupSegue", this));
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
}
CheckLock(() => PerformSegue("setupSegue", this));
}
public void CompleteRequest(string id = null, string username = null,
@@ -159,34 +201,43 @@ namespace Bit.iOS.Autofill
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
{
if (segue.DestinationViewController is UINavigationController navController)
try
{
if (navController.TopViewController is LoginListViewController listLoginController)
if (segue.DestinationViewController is UINavigationController navController)
{
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);
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);
}
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
}
}
@@ -194,93 +245,109 @@ namespace Bit.iOS.Autofill
{
DismissViewController(false, async () =>
{
if (_context.CredentialIdentity != null)
try
{
await ProvideCredentialAsync();
return;
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);
}
}
if (_context.Configuring)
catch (Exception ex)
{
PerformSegue("setupSegue", this);
return;
}
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
{
PerformSegue("loginSearchSegue", this);
}
else
{
PerformSegue("loginListSegue", this);
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
}
});
}
private async Task ProvideCredentialAsync(bool userInteraction = true)
{
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService", true);
Bit.Core.Models.Domain.Cipher cipher = null;
var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null;
if (!cancel)
try
{
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)
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)
{
await _stateService.Value.SetPasswordRepromptAutofillAsync(true);
var err = new NSError(new NSString("ASExtensionErrorDomain"),
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null);
ExtensionContext?.CancelRequest(err);
return;
}
else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
var decCipher = await cipher.DecryptAsync();
if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None)
{
// Add a timeout to resolve keyboard not always showing up.
await Task.Delay(250);
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
if (!await passwordRepromptService.ShowPasswordPromptAsync())
// 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.UserCanceled), null);
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), 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))
string totpCode = null;
var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync();
if (!disableTotpCopy.GetValueOrDefault(false))
{
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp);
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);
CompleteRequest(decCipher.Id, decCipher.Login.Username, decCipher.Login.Password, totpCode);
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
}
}
private async void CheckLock(Action notLockedAction)
private async Task CheckLockAsync(Action notLockedAction)
{
if (await IsLocked() || await _stateService.Value.GetPasswordRepromptAutofillAsync())
{
PerformSegue("lockPasswordSegue", this);
DispatchQueue.MainQueue.DispatchAsync(() => PerformSegue("lockPasswordSegue", this));
}
else
{
@@ -303,15 +370,21 @@ namespace Bit.iOS.Autofill
{
NSRunLoop.Main.BeginInvokeOnMainThread(async () =>
{
if (await IsAuthed())
try
{
await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync());
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
if (deviceActionService.SystemMajorVersion() >= 12)
if (await IsAuthed())
{
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync());
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
}
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
}

View File

@@ -11,7 +11,7 @@
<key>CFBundleIdentifier</key>
<string>com.8bit.bitwarden.autofill</string>
<key>CFBundleShortVersionString</key>
<string>2022.6.1</string>
<string>2022.6.2</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleLocalizations</key>

View File

@@ -1,20 +1,20 @@
using System;
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.Abstractions;
using Bit.App.Models;
using Xamarin.Forms;
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 Xamarin.Forms;
namespace Bit.iOS.Core.Controllers
{
@@ -28,6 +28,7 @@ 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;
@@ -39,6 +40,10 @@ namespace Bit.iOS.Core.Controllers
protected bool autofillExtension = false;
public BaseLockPasswordViewController()
{
}
public BaseLockPasswordViewController(IntPtr handle)
: base(handle)
{ }
@@ -80,7 +85,7 @@ namespace Bit.iOS.Core.Controllers
}
public abstract UITableView TableView { get; }
public override async void ViewDidLoad()
{
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
@@ -91,6 +96,7 @@ 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.
@@ -168,13 +174,12 @@ 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)
@@ -191,7 +196,7 @@ namespace Bit.iOS.Core.Controllers
}
}
public override async void ViewDidAppear(bool animated)
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
@@ -262,13 +267,7 @@ namespace Bit.iOS.Core.Controllers
}
if (failed)
{
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
if (invalidUnlockAttempts >= 5)
{
await LogOutAsync();
return;
}
InvalidValue();
await HandleFailedCredentialsAsync();
}
}
else
@@ -303,17 +302,22 @@ namespace Bit.iOS.Core.Controllers
}
else
{
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
if (invalidUnlockAttempts >= 5)
{
await LogOutAsync();
return;
}
InvalidValue();
await HandleFailedCredentialsAsync();
}
}
}
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)
@@ -392,38 +396,43 @@ namespace Bit.iOS.Core.Controllers
PresentViewController(alert, true, null);
}
private async Task LogOutAsync()
protected override void Dispose(bool disposing)
{
await AppHelpers.LogOutAsync(await _stateService.GetActiveUserIdAsync());
var authService = ServiceContainer.Resolve<IAuthService>("authService");
authService.LogOut(() =>
{
Cancel?.Invoke();
});
base.Dispose(disposing);
MasterPasswordCell?.Dispose();
MasterPasswordCell = null;
TableView?.Dispose();
}
public class TableSource : ExtendedUITableViewSource
{
private readonly BaseLockPasswordViewController _controller;
private readonly WeakReference<BaseLockPasswordViewController> _controller;
public TableSource(BaseLockPasswordViewController controller)
{
_controller = controller;
_controller = new WeakReference<BaseLockPasswordViewController>(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;
}
}
}
@@ -431,7 +440,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;
@@ -441,9 +450,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;
}
}
}
@@ -457,8 +466,13 @@ namespace Bit.iOS.Core.Controllers
public override nint NumberOfSections(UITableView tableView)
{
return (!_controller._biometricUnlockOnly && _controller._biometricLock) ||
_controller._passwordReprompt
if (!_controller.TryGetTarget(out var controller))
{
return 0;
}
return (!controller._biometricUnlockOnly && controller._biometricLock) ||
controller._passwordReprompt
? 2
: 1;
}
@@ -484,13 +498,18 @@ 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);

View File

@@ -7,7 +7,11 @@ namespace Bit.iOS.Core.Controllers
public class ExtendedUIViewController : UIViewController
{
public Action DismissModalAction { get; set; }
public ExtendedUIViewController()
{
}
public ExtendedUIViewController(IntPtr handle)
: base(handle)
{
@@ -28,16 +32,28 @@ namespace Bit.iOS.Core.Controllers
{
View.BackgroundColor = ThemeHelpers.BackgroundColor;
}
if (NavigationController?.NavigationBar != null)
UpdateNavigationBarTheme();
}
protected virtual void UpdateNavigationBarTheme()
{
UpdateNavigationBarTheme(NavigationController?.NavigationBar);
}
protected void UpdateNavigationBarTheme(UINavigationBar navBar)
{
if (navBar is null)
{
NavigationController.NavigationBar.BarTintColor = ThemeHelpers.NavBarBackgroundColor;
NavigationController.NavigationBar.BackgroundColor = ThemeHelpers.NavBarBackgroundColor;
NavigationController.NavigationBar.TintColor = ThemeHelpers.NavBarTextColor;
NavigationController.NavigationBar.TitleTextAttributes = new UIStringAttributes
{
ForegroundColor = ThemeHelpers.NavBarTextColor
};
return;
}
navBar.BarTintColor = ThemeHelpers.NavBarBackgroundColor;
navBar.BackgroundColor = ThemeHelpers.NavBarBackgroundColor;
navBar.TintColor = ThemeHelpers.NavBarTextColor;
navBar.TitleTextAttributes = new UIStringAttributes
{
ForegroundColor = ThemeHelpers.NavBarTextColor
};
}
}
}

View File

@@ -184,7 +184,7 @@ namespace Bit.iOS.Core.Controllers
}
}
public override async void ViewDidAppear(bool animated)
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);

View File

@@ -0,0 +1,16 @@
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)
{
}
}
}

View File

@@ -1,6 +1,8 @@
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
@@ -13,6 +15,11 @@ 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
@@ -21,7 +28,17 @@ namespace Bit.iOS.Core.Renderers.CollectionView
}
catch (Exception ex) when (ItemsView?.ExtraDataForLogging != null)
{
throw new Exception("Error in ExtendedCollectionView, extra data: " + ItemsView.ExtraDataForLogging, ex);
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;
}
}
}

View File

@@ -0,0 +1,43 @@
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;
}
}
}
}

View File

@@ -604,6 +604,12 @@ 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;

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Bit.App.Controls;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
@@ -10,9 +11,11 @@ namespace Bit.iOS.Core.Utilities
{
public class AccountSwitchingOverlayHelper
{
IStateService _stateService;
IMessagingService _messagingService;
ILogger _logger;
const string DEFAULT_SYSTEM_AVATAR_IMAGE = "person.2";
readonly IStateService _stateService;
readonly IMessagingService _messagingService;
readonly ILogger _logger;
public AccountSwitchingOverlayHelper()
{
@@ -23,9 +26,24 @@ namespace Bit.iOS.Core.Utilities
public async Task<UIImage> CreateAvatarImageAsync()
{
var avatarImageSource = new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
var avatarUIImage = await avatarImageSource.GetNativeImageAsync();
return avatarUIImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
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);
}
}
public AccountSwitchingOverlayView CreateAccountSwitchingOverlayView(UIView containerView)

View File

@@ -1,9 +1,9 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Services;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Internals;
using Xamarin.Forms.Platform.iOS;
namespace Bit.iOS.Core.Utilities
@@ -17,25 +17,29 @@ namespace Bit.iOS.Core.Utilities
public static async Task<UIImage> GetNativeImageAsync(this ImageSource source, CancellationToken cancellationToken = default(CancellationToken))
{
if (source == null || source.IsEmpty)
{
return null;
}
var handler = Xamarin.Forms.Internals.Registrar.Registered.GetHandlerForObject<IImageSourceHandler>(source);
if (handler == null)
{
LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("GetNativeImageAsync failed cause IImageSourceHandler couldn't be found"));
return null;
}
try
{
float scale = (float)UIScreen.MainScreen.Scale;
return await handler.LoadImageAsync(source, scale: scale, cancelationToken: cancellationToken);
}
catch (OperationCanceledException)
{
Log.Warning("Image loading", "Image load cancelled");
LoggerHelper.LogEvenIfCantBeResolved(new OperationCanceledException("GetNativeImageAsync was cancelled"));
}
catch (Exception ex)
{
Log.Warning("Image loading", $"Image load failed: {ex}");
LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("GetNativeImageAsync failed", ex));
}
return null;

View File

@@ -2,6 +2,7 @@
using System.IO;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Models;
using Bit.App.Pages;
using Bit.App.Resources;
@@ -15,6 +16,7 @@ using Bit.iOS.Core.Services;
using CoreNFC;
using Foundation;
using UIKit;
using Xamarin.Forms;
namespace Bit.iOS.Core.Utilities
{
@@ -26,6 +28,42 @@ namespace Bit.iOS.Core.Utilities
public static string AppGroupId = "group.com.8bit.bitwarden";
public static string AccessGroup = "LTZ2PFU5D6.com.8bit.bitwarden";
public static void InitApp<T>(T rootController,
string clearCipherCacheKey,
NFCNdefReaderSession nfcSession,
out NFCReaderDelegate nfcDelegate,
out IAccountsManager accountsManager)
where T : UIViewController, IAccountsManagerHost
{
Forms.Init();
if (ServiceContainer.RegisteredServices.Count > 0)
{
ServiceContainer.Reset();
}
RegisterLocalServices();
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
ServiceContainer.Init(deviceActionService.DeviceUserAgent,
clearCipherCacheKey,
Bit.Core.Constants.iOSAllClearCipherCacheKeys);
InitLogger();
Bootstrap();
var appOptions = new AppOptions { IosExtension = true };
var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
AppearanceAdjustments();
nfcDelegate = new Core.NFCReaderDelegate((success, message) =>
messagingService.Send("gotYubiKeyOTP", message));
SubscribeBroadcastReceiver(rootController, nfcSession, nfcDelegate);
accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
accountsManager.Init(() => appOptions, rootController);
}
public static void InitLogger()
{
ServiceContainer.Resolve<ILogger>("logger").InitAsync();
@@ -89,6 +127,7 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
}
public static void Bootstrap(Func<Task> postBootstrapFunc = null)
@@ -181,7 +220,8 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Resolve<IStorageService>("secureStorageService"),
ServiceContainer.Resolve<IStateService>("stateService"),
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService"));
ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
if (postBootstrapFunc != null)

View File

@@ -201,6 +201,8 @@
<Compile Include="Renderers\CollectionView\ExtendedCollectionViewRenderer.cs" />
<Compile Include="Renderers\CollectionView\ExtendedGroupableItemsViewController.cs" />
<Compile Include="Utilities\UISearchBarExtensions.cs" />
<Compile Include="Renderers\CollectionView\CollectionException.cs" />
<Compile Include="Renderers\CollectionView\ExtendedGroupableItemsViewDelegator.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\App\App.csproj">

View File

@@ -11,7 +11,7 @@
<key>CFBundleIdentifier</key>
<string>com.8bit.bitwarden.find-login-action-extension</string>
<key>CFBundleShortVersionString</key>
<string>2022.6.1</string>
<string>2022.6.2</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>

View File

@@ -0,0 +1,27 @@
// This file has been autogenerated from a class added in the UI designer.
using System;
using UIKit;
namespace Bit.iOS.ShareExtension
{
public partial class ExtensionNavigationController : UINavigationController
{
public ExtensionNavigationController (IntPtr handle) : base (handle)
{
}
public override UIViewController PopViewController(bool animated)
{
TopViewController?.Dispose();
return base.PopViewController(animated);
}
public override void DismissModalViewController(bool animated)
{
ModalViewController?.Dispose();
base.DismissModalViewController(animated);
}
}
}

View File

@@ -0,0 +1,20 @@
// WARNING
//
// This file has been generated automatically by Visual Studio to store outlets and
// actions made in the UI designer. If it is removed, they will be lost.
// Manual changes to this file may not be handled correctly.
//
using Foundation;
using System.CodeDom.Compiler;
namespace Bit.iOS.ShareExtension
{
[Register ("ExtensionNavigationController")]
partial class ExtensionNavigationController
{
void ReleaseDesignerOutlets ()
{
}
}
}

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>2022.6.1</string>
<string>2022.6.2</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>MinimumOSVersion</key>

View File

@@ -7,11 +7,11 @@ using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Pages;
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.Core;
using Bit.iOS.Core.Controllers;
using Bit.iOS.Core.Utilities;
using Bit.iOS.Core.Views;
@@ -24,16 +24,33 @@ using Xamarin.Forms;
namespace Bit.iOS.ShareExtension
{
public partial class LoadingViewController : ExtendedUIViewController
public partial class LoadingViewController : ExtendedUIViewController, IAccountsManagerHost
{
const string STORYBOARD_NAME = "MainInterface";
private Context _context = new Context();
private NFCNdefReaderSession _nfcSession = null;
private Core.NFCReaderDelegate _nfcDelegate = null;
private IAccountsManager _accountsManager;
readonly LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>("stateService");
readonly LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>("vaultTimeoutService");
readonly LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>("deviceActionService");
readonly LazyResolve<IEventService> _eventService = new LazyResolve<IEventService>("eventService");
Lazy<UIStoryboard> _storyboard = new Lazy<UIStoryboard>(() => UIStoryboard.FromName(STORYBOARD_NAME, null));
private App.App _app = null;
private UIViewController _currentModalController;
private bool _presentingOnNavigationPage;
private ExtensionNavigationController ExtNavigationController
{
get
{
NavigationController.PresentationController.Delegate =
new CustomPresentationControllerDelegate(CompleteRequest);
return NavigationController as ExtensionNavigationController;
}
}
public LoadingViewController(IntPtr handle)
: base(handle)
@@ -41,39 +58,38 @@ namespace Bit.iOS.ShareExtension
public override void ViewDidLoad()
{
InitApp();
iOSCoreHelpers.InitApp(this, Bit.Core.Constants.iOSShareExtensionClearCiphersCacheKey,
_nfcSession, out _nfcDelegate, out _accountsManager);
base.ViewDidLoad();
Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png");
View.BackgroundColor = ThemeHelpers.SplashBackgroundColor;
_context.ExtensionContext = ExtensionContext;
_context.ProviderType = GetProviderTypeFromExtensionInputItems();
}
/// <summary>
/// Gets the provider <see cref="UTType"/> given the input items
/// </summary>
private string GetProviderTypeFromExtensionInputItems()
{
foreach (var item in ExtensionContext.InputItems)
{
var processed = false;
foreach (var itemProvider in item.Attachments)
{
if (itemProvider.HasItemConformingTo(UTType.PlainText))
{
_context.ProviderType = UTType.PlainText;
processed = true;
break;
return UTType.PlainText;
}
else if (itemProvider.HasItemConformingTo(UTType.Data))
if (itemProvider.HasItemConformingTo(UTType.Data))
{
_context.ProviderType = UTType.Data;
processed = true;
break;
return UTType.Data;
}
}
if (processed)
{
break;
}
}
return null;
}
public override async void ViewDidAppear(bool animated)
@@ -84,12 +100,12 @@ namespace Bit.iOS.ShareExtension
{
if (!await IsAuthed())
{
LaunchHomePage();
await _accountsManager.NavigateOnAccountChangeAsync(false);
return;
}
else if (await IsLocked())
{
PerformSegue("lockPasswordSegue", this);
NavigateToLockViewController();
}
else
{
@@ -102,24 +118,52 @@ namespace Bit.iOS.ShareExtension
}
}
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
void NavigateToLockViewController()
{
if (segue.DestinationViewController is UINavigationController navController
&&
navController.TopViewController is LockPasswordViewController passwordViewController)
var viewController = _storyboard.Value.InstantiateViewController("lockVC") as LockPasswordViewController;
viewController.LoadingController = this;
viewController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
if (_presentingOnNavigationPage)
{
passwordViewController.LoadingController = this;
segue.DestinationViewController.PresentationController.Delegate =
new CustomPresentationControllerDelegate(passwordViewController.DismissModalAction);
_presentingOnNavigationPage = false;
DismissViewController(true, () => ExtNavigationController.PushViewController(viewController, true));
}
else
{
ExtNavigationController.PushViewController(viewController, true);
}
}
public void DismissLockAndContinue()
{
Debug.WriteLine("BW Log, Dismissing lock controller.");
ClearBeforeNavigating();
DismissViewController(false, () => ContinueOnAsync().FireAndForget());
}
private void DismissAndLaunch(Action pageToLaunch)
{
ClearBeforeNavigating();
DismissViewController(false, pageToLaunch);
}
void ClearBeforeNavigating()
{
_currentModalController?.Dispose();
_currentModalController = null;
if (_storyboard.IsValueCreated)
{
_storyboard.Value.Dispose();
_storyboard = null;
_storyboard = new Lazy<UIStoryboard>(() => UIStoryboard.FromName(STORYBOARD_NAME, null));
}
}
private async Task ContinueOnAsync()
{
Tuple<SendType, string, byte[], string> createSend = null;
@@ -140,20 +184,24 @@ namespace Bit.iOS.ShareExtension
CreateSend = createSend,
CopyInsteadOfShareAfterSaving = true
};
var sendAddEditPage = new SendAddEditPage(appOptions)
var sendPage = new SendAddOnlyPage(appOptions)
{
OnClose = () => CompleteRequest(),
AfterSubmit = () => CompleteRequest()
};
var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(sendAddEditPage);
SetupAppAndApplyResources(sendPage);
var navigationPage = new NavigationPage(sendAddEditPage);
var sendAddEditController = navigationPage.CreateViewController();
sendAddEditController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(sendAddEditController, true, null);
NavigateToPage(sendPage);
}
private void NavigateToPage(ContentPage page)
{
var navigationPage = new NavigationPage(page);
_currentModalController = navigationPage.CreateViewController();
_currentModalController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
_presentingOnNavigationPage = true;
PresentViewController(_currentModalController, true, null);
}
private async Task<(string, byte[])> LoadDataBytesAsync()
@@ -202,31 +250,6 @@ namespace Bit.iOS.ShareExtension
});
}
private void InitApp()
{
// Init Xamarin Forms
Forms.Init();
if (ServiceContainer.RegisteredServices.Count > 0)
{
ServiceContainer.Reset();
}
iOSCoreHelpers.RegisterLocalServices();
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
ServiceContainer.Init(_deviceActionService.Value.DeviceUserAgent,
Bit.Core.Constants.iOSShareExtensionClearCiphersCacheKey, Bit.Core.Constants.iOSAllClearCipherCacheKeys);
iOSCoreHelpers.InitLogger();
iOSCoreHelpers.Bootstrap();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
iOSCoreHelpers.AppearanceAdjustments();
_nfcDelegate = new NFCReaderDelegate((success, message) =>
messagingService.Send("gotYubiKeyOTP", message));
iOSCoreHelpers.SubscribeBroadcastReceiver(this, _nfcSession, _nfcDelegate);
}
private Task<bool> IsLocked()
{
return _vaultTimeoutService.Value.IsLockedAsync();
@@ -244,7 +267,7 @@ namespace Bit.iOS.ShareExtension
if (await IsAuthed())
{
await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync());
if (_deviceActionService.Value.SystemMajorVersion() >= 12)
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
}
@@ -252,83 +275,75 @@ namespace Bit.iOS.ShareExtension
});
}
private App.App SetupAppAndApplyResources(ContentPage page)
{
if (_app is null)
{
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
}
ThemeManager.ApplyResourcesToPage(page);
return _app;
}
private void LaunchHomePage()
{
var homePage = new HomePage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(homePage);
SetupAppAndApplyResources(homePage);
if (homePage.BindingContext is HomeViewModel vm)
{
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow());
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
vm.StartLoginAction = () => DismissAndLaunch(() => LaunchLoginFlow());
vm.StartRegisterAction = () => DismissAndLaunch(() => LaunchRegisterFlow());
vm.StartSsoLoginAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());
vm.StartEnvironmentAction = () => DismissAndLaunch(() => LaunchEnvironmentFlow());
vm.CloseAction = () => CompleteRequest();
}
var navigationPage = new NavigationPage(homePage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
NavigateToPage(homePage);
LogoutIfAuthed();
}
private void LaunchEnvironmentFlow()
{
var environmentPage = new EnvironmentPage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
SetupAppAndApplyResources(environmentPage);
ThemeManager.ApplyResourcesToPage(environmentPage);
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
{
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.SubmitSuccessAction = () => DismissAndLaunch(() => LaunchHomePage());
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
}
var navigationPage = new NavigationPage(environmentPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
NavigateToPage(environmentPage);
}
private void LaunchRegisterFlow()
{
var registerPage = new RegisterPage(null);
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(registerPage);
SetupAppAndApplyResources(registerPage);
if (registerPage.BindingContext is RegisterPageViewModel vm)
{
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.RegistrationSuccess = () => DismissAndLaunch(() => LaunchLoginFlow(vm.Email));
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
}
var navigationPage = new NavigationPage(registerPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
NavigateToPage(registerPage);
}
private void LaunchLoginFlow(string email = null)
{
var loginPage = new LoginPage(email);
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(loginPage);
SetupAppAndApplyResources(loginPage);
if (loginPage.BindingContext is LoginPageViewModel vm)
{
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
vm.LogInSuccessAction = () => DismissLockAndContinue();
vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(false));
vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
vm.LogInSuccessAction = () =>
{
DismissLockAndContinue();
};
vm.CloseAction = () => CompleteRequest();
}
var navigationPage = new NavigationPage(loginPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
NavigateToPage(loginPage);
LogoutIfAuthed();
}
@@ -336,22 +351,16 @@ namespace Bit.iOS.ShareExtension
private void LaunchLoginSsoFlow()
{
var loginPage = new LoginSsoPage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(loginPage);
SetupAppAndApplyResources(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
{
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(true));
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(true));
vm.StartSetPasswordAction = () => DismissAndLaunch(() => LaunchSetPasswordFlow());
vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
vm.SsoAuthSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
}
var navigationPage = new NavigationPage(loginPage);
var loginController = navigationPage.CreateViewController();
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
NavigateToPage(loginPage);
LogoutIfAuthed();
}
@@ -359,65 +368,97 @@ namespace Bit.iOS.ShareExtension
private void LaunchTwoFactorFlow(bool authingWithSso)
{
var twoFactorPage = new TwoFactorPage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(twoFactorPage);
SetupAppAndApplyResources(twoFactorPage);
if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm)
{
vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue();
vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow());
vm.StartSetPasswordAction = () => DismissAndLaunch(() => LaunchSetPasswordFlow());
if (authingWithSso)
{
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
vm.CloseAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());
}
else
{
vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow());
vm.CloseAction = () => DismissAndLaunch(() => LaunchLoginFlow());
}
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
}
var navigationPage = new NavigationPage(twoFactorPage);
var twoFactorController = navigationPage.CreateViewController();
twoFactorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(twoFactorController, true, null);
NavigateToPage(twoFactorPage);
}
private void LaunchSetPasswordFlow()
{
var setPasswordPage = new SetPasswordPage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(setPasswordPage);
SetupAppAndApplyResources(setPasswordPage);
if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm)
{
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
vm.SetPasswordSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
}
var navigationPage = new NavigationPage(setPasswordPage);
var setPasswordController = navigationPage.CreateViewController();
setPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(setPasswordController, true, null);
NavigateToPage(setPasswordPage);
}
private void LaunchUpdateTempPasswordFlow()
{
var updateTempPasswordPage = new UpdateTempPasswordPage();
var app = new App.App(new AppOptions { IosExtension = true });
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesToPage(updateTempPasswordPage);
SetupAppAndApplyResources(updateTempPasswordPage);
if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm)
{
vm.UpdateTempPasswordSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
vm.LogOutAction = () => DismissViewController(false, () => LaunchHomePage());
vm.UpdateTempPasswordSuccessAction = () => DismissAndLaunch(() => LaunchHomePage());
vm.LogOutAction = () => DismissAndLaunch(() => LaunchHomePage());
}
NavigateToPage(updateTempPasswordPage);
}
public void Navigate(NavigationTarget navTarget, INavigationParams navParams = null)
{
if (ExtNavigationController?.ViewControllers?.Any() ?? false)
{
ExtNavigationController.PopViewController(false);
}
else if (ExtNavigationController?.ModalViewController != null)
{
ExtNavigationController.DismissModalViewController(false);
}
var navigationPage = new NavigationPage(updateTempPasswordPage);
var updateTempPasswordController = navigationPage.CreateViewController();
updateTempPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(updateTempPasswordController, true, null);
switch (navTarget)
{
case NavigationTarget.HomeLogin:
ExecuteLaunch(LaunchHomePage);
break;
case NavigationTarget.Login:
if (navParams is LoginNavigationParams loginParams)
{
ExecuteLaunch(() => LaunchLoginFlow(loginParams.Email));
}
else
{
ExecuteLaunch(() => LaunchLoginFlow());
}
break;
case NavigationTarget.Lock:
NavigateToLockViewController();
break;
case NavigationTarget.Home:
DismissLockAndContinue();
break;
}
}
private void ExecuteLaunch(Action launchAction)
{
if (_presentingOnNavigationPage)
{
DismissAndLaunch(launchAction);
}
else
{
launchAction();
}
}
public Task SetPreviousPageInfoAsync() => Task.CompletedTask;
public Task UpdateThemeAsync() => Task.CompletedTask;
}
}

View File

@@ -1,11 +1,22 @@
using Bit.App.Controls;
using Bit.Core.Utilities;
using Bit.iOS.Core.Utilities;
using System;
using UIKit;
namespace Bit.iOS.ShareExtension
{
public partial class LockPasswordViewController : Core.Controllers.LockPasswordViewController
public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController
{
AccountSwitchingOverlayView _accountSwitchingOverlayView;
AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper;
public LockPasswordViewController()
{
BiometricIntegrityKey = Bit.Core.Constants.iOSShareExtensionBiometricIntegrityKey;
DismissModalAction = Cancel;
}
public LockPasswordViewController(IntPtr handle)
: base(handle)
{
@@ -17,24 +28,80 @@ namespace Bit.iOS.ShareExtension
public override UINavigationItem BaseNavItem => _navItem;
public override UIBarButtonItem BaseCancelButton => _cancelButton;
public override UIBarButtonItem BaseSubmitButton => _submitButton;
public override Action Success => () => LoadingController.DismissLockAndContinue();
public override Action Cancel => () => LoadingController.CompleteRequest();
public override Action Success => () =>
{
LoadingController?.Navigate(Bit.Core.Enums.NavigationTarget.Home);
LoadingController = null;
};
public override Action Cancel => () =>
{
LoadingController?.CompleteRequest();
LoadingController = null;
};
public override void ViewDidLoad()
public override UITableView TableView => _mainTableView;
public override async void ViewDidLoad()
{
base.ViewDidLoad();
_cancelButton.TintColor = ThemeHelpers.NavBarTextColor;
_submitButton.TintColor = ThemeHelpers.NavBarTextColor;
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
_accountSwitchingButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync();
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(_overlayView);
}
protected override void UpdateNavigationBarTheme()
{
UpdateNavigationBarTheme(_navBar);
}
partial void AccountSwitchingButton_Activated(UIBarButtonItem sender)
{
_accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, _overlayView);
}
partial void SubmitButton_Activated(UIBarButtonItem sender)
{
var task = CheckPasswordAsync();
CheckPasswordAsync().FireAndForget();
}
partial void CancelButton_Activated(UIBarButtonItem sender)
{
Cancel();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (TableView != null)
{
TableView.Source?.Dispose();
}
if (_accountSwitchingButton?.Image != null)
{
var img = _accountSwitchingButton.Image;
_accountSwitchingButton.Image = null;
img.Dispose();
}
if (_accountSwitchingOverlayView != null && _overlayView?.Subviews != null)
{
foreach (var subView in _overlayView.Subviews)
{
subView.RemoveFromSuperview();
subView.Dispose();
}
_accountSwitchingOverlayView = null;
_overlayView.RemoveFromSuperview();
}
_accountSwitchingOverlayHelper = null;
}
base.Dispose(disposing);
}
}
}

View File

@@ -12,18 +12,30 @@ namespace Bit.iOS.ShareExtension
[Register ("LockPasswordViewController")]
partial class LockPasswordViewController
{
[Outlet]
UIKit.UIBarButtonItem _accountSwitchingButton { get; set; }
[Outlet]
UIKit.UIBarButtonItem _cancelButton { get; set; }
[Outlet]
UIKit.UITableView _mainTableView { get; set; }
[Outlet]
UIKit.UINavigationBar _navBar { get; set; }
[Outlet]
UIKit.UINavigationItem _navItem { get; set; }
[Outlet]
UIKit.UIView _overlayView { get; set; }
[Outlet]
UIKit.UIBarButtonItem _submitButton { get; set; }
[Action ("AccountSwitchingButton_Activated:")]
partial void AccountSwitchingButton_Activated (UIKit.UIBarButtonItem sender);
[Action ("CancelButton_Activated:")]
partial void CancelButton_Activated (UIKit.UIBarButtonItem sender);
@@ -32,6 +44,11 @@ namespace Bit.iOS.ShareExtension
void ReleaseDesignerOutlets ()
{
if (_accountSwitchingButton != null) {
_accountSwitchingButton.Dispose ();
_accountSwitchingButton = null;
}
if (_cancelButton != null) {
_cancelButton.Dispose ();
_cancelButton = null;
@@ -47,10 +64,20 @@ namespace Bit.iOS.ShareExtension
_navItem = null;
}
if (_overlayView != null) {
_overlayView.Dispose ();
_overlayView = null;
}
if (_submitButton != null) {
_submitButton.Dispose ();
_submitButton = null;
}
if (_navBar != null) {
_navBar.Dispose ();
_navBar = null;
}
}
}
}

View File

@@ -1,9 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="2vH-Do-uhk">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="2vH-Do-uhk">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -11,10 +13,6 @@
<scene sceneID="kFr-IN-5GS">
<objects>
<viewController id="bHU-LX-EpF" customClass="LoadingViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="8LE-gl-yDT"/>
<viewControllerLayoutGuide type="bottom" id="MuK-nA-9iu"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="z2O-Vp-jY9">
<rect key="frame" x="0.0" y="0.0" width="414" height="808"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
@@ -23,25 +21,25 @@
<rect key="frame" x="66" y="352" width="282" height="44"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="jNx-Vd-K6U"/>
<constraints>
<constraint firstItem="Zdy-yw-n0p" firstAttribute="centerX" secondItem="z2O-Vp-jY9" secondAttribute="centerX" id="6DT-HB-vS5"/>
<constraint firstItem="Zdy-yw-n0p" firstAttribute="centerX" secondItem="jNx-Vd-K6U" secondAttribute="centerX" id="6DT-HB-vS5"/>
<constraint firstItem="Zdy-yw-n0p" firstAttribute="centerY" secondItem="z2O-Vp-jY9" secondAttribute="centerY" constant="-30" id="o9N-Tv-Iwq"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="74l-Va-Vqa"/>
<connections>
<outlet property="Logo" destination="Zdy-yw-n0p" id="1Qk-EK-0BO"/>
<segue destination="rh6-Mf-4Ja" kind="presentation" identifier="lockPasswordSegue" id="ZUl-jv-5se"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="yJx-cc-wzs" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="560"/>
</scene>
<!--Navigation Controller-->
<!--Extension Navigation Controller-->
<scene sceneID="Wgx-vz-XqL">
<objects>
<navigationController definesPresentationContext="YES" id="2vH-Do-uhk" sceneMemberID="viewController">
<navigationController definesPresentationContext="YES" id="2vH-Do-uhk" customClass="ExtensionNavigationController" sceneMemberID="viewController">
<navigationBar key="navigationBar" hidden="YES" contentMode="scaleToFill" translucent="NO" id="JoO-jQ-16M">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
@@ -54,60 +52,89 @@
</objects>
<point key="canvasLocation" x="-1097" y="564"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="Tzp-2o-9k7">
<!--Lock Password View Controller-->
<scene sceneID="vQB-cT-8IC">
<objects>
<navigationController definesPresentationContext="YES" id="rh6-Mf-4Ja" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" translucent="NO" id="UDq-kw-Ue7">
<rect key="frame" x="0.0" y="0.0" width="414" height="56"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
</navigationBar>
<connections>
<segue destination="85y-W9-d8q" kind="relationship" relationship="rootViewController" id="TeA-GE-A22"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="BVV-5B-aim" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-375" y="1262"/>
</scene>
<!--Verify Master Password-->
<scene sceneID="OEb-ak-BVc">
<objects>
<tableViewController id="85y-W9-d8q" customClass="LockPasswordViewController" sceneMemberID="viewController">
<tableView key="view" opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="9on-wf-zdb">
<rect key="frame" x="0.0" y="0.0" width="414" height="786"/>
<viewController storyboardIdentifier="lockVC" id="Vi7-LV-nWW" customClass="LockPasswordViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Vfd-7B-19G">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<outlet property="dataSource" destination="85y-W9-d8q" id="3il-RO-S3K"/>
<outlet property="delegate" destination="85y-W9-d8q" id="bLb-h4-pr3"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Verify Master Password" id="qL3-iV-6Ld">
<barButtonItem key="leftBarButtonItem" title="Cancel" id="d8j-HZ-erD">
<connections>
<action selector="CancelButton_Activated:" destination="85y-W9-d8q" id="p54-B0-Vyf"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" title="Submit" id="8a7-Vz-SJA">
<connections>
<action selector="SubmitButton_Activated:" destination="85y-W9-d8q" id="P8A-7O-lpY"/>
</connections>
</barButtonItem>
</navigationItem>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" estimatedSectionHeaderHeight="-1" sectionFooterHeight="18" estimatedSectionFooterHeight="-1" translatesAutoresizingMaskIntoConstraints="NO" id="M1A-84-x5l">
<rect key="frame" x="0.0" y="88" width="414" height="774"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableView>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ijE-Pa-OBq" userLabel="OverlayView">
<rect key="frame" x="0.0" y="88" width="414" height="774"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<navigationBar contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fav-Fz-6ZK">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<items>
<navigationItem title="Verify Master Password" id="aka-In-IYk">
<leftBarButtonItems>
<barButtonItem title="Cancel" id="LrG-Qx-w4Q">
<connections>
<action selector="CancelButton_Activated:" destination="Vi7-LV-nWW" id="qyZ-i9-Dwz"/>
</connections>
</barButtonItem>
<barButtonItem title="Item" image="person.2" catalog="system" style="plain" id="nlD-Xn-HtM" userLabel="Account Switching Button">
<color key="tintColor" systemColor="systemBackgroundColor"/>
<connections>
<action selector="AccountSwitchingButton_Activated:" destination="Vi7-LV-nWW" id="G3U-rv-UOl"/>
</connections>
</barButtonItem>
</leftBarButtonItems>
<barButtonItem key="rightBarButtonItem" title="Submit" id="oQD-QK-YPB">
<connections>
<action selector="SubmitButton_Activated:" destination="Vi7-LV-nWW" id="DgO-TS-MPf"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="barPosition">
<integer key="value" value="3"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</navigationBar>
</subviews>
<viewLayoutGuide key="safeArea" id="SSW-s3-JwL"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="M1A-84-x5l" firstAttribute="leading" secondItem="SSW-s3-JwL" secondAttribute="leading" id="3Es-aL-5Og"/>
<constraint firstItem="ijE-Pa-OBq" firstAttribute="leading" secondItem="SSW-s3-JwL" secondAttribute="leading" id="6Lj-CR-OFz"/>
<constraint firstItem="fav-Fz-6ZK" firstAttribute="leading" secondItem="SSW-s3-JwL" secondAttribute="leading" id="BEJ-gh-NAq"/>
<constraint firstItem="fav-Fz-6ZK" firstAttribute="top" secondItem="SSW-s3-JwL" secondAttribute="top" id="CLE-2p-LI3"/>
<constraint firstItem="SSW-s3-JwL" firstAttribute="trailing" secondItem="M1A-84-x5l" secondAttribute="trailing" id="GaL-B0-2Lg"/>
<constraint firstItem="SSW-s3-JwL" firstAttribute="bottom" secondItem="M1A-84-x5l" secondAttribute="bottom" id="LG1-vj-VhW"/>
<constraint firstItem="SSW-s3-JwL" firstAttribute="trailing" secondItem="ijE-Pa-OBq" secondAttribute="trailing" id="Q3J-Wa-mnY"/>
<constraint firstItem="ijE-Pa-OBq" firstAttribute="top" secondItem="fav-Fz-6ZK" secondAttribute="bottom" id="h8T-rn-ZPU"/>
<constraint firstItem="SSW-s3-JwL" firstAttribute="trailing" secondItem="fav-Fz-6ZK" secondAttribute="trailing" id="tux-AN-Z92"/>
<constraint firstItem="SSW-s3-JwL" firstAttribute="bottom" secondItem="ijE-Pa-OBq" secondAttribute="bottom" id="zLh-RX-eSc"/>
<constraint firstItem="M1A-84-x5l" firstAttribute="top" secondItem="fav-Fz-6ZK" secondAttribute="bottom" id="zgM-he-DYl"/>
</constraints>
</view>
<connections>
<outlet property="_cancelButton" destination="d8j-HZ-erD" id="wlI-el-Snh"/>
<outlet property="_mainTableView" destination="9on-wf-zdb" id="ltj-yY-5ue"/>
<outlet property="_navItem" destination="qL3-iV-6Ld" id="Grb-Ta-NCF"/>
<outlet property="_submitButton" destination="8a7-Vz-SJA" id="LS8-6Y-Wkp"/>
<outlet property="_accountSwitchingButton" destination="nlD-Xn-HtM" id="SSG-zv-bAc"/>
<outlet property="_cancelButton" destination="LrG-Qx-w4Q" id="aag-ZZ-Ifs"/>
<outlet property="_mainTableView" destination="M1A-84-x5l" id="pA4-ao-Fhu"/>
<outlet property="_navBar" destination="fav-Fz-6ZK" id="Q9p-Dw-ipx"/>
<outlet property="_navItem" destination="aka-In-IYk" id="www-Lt-x1g"/>
<outlet property="_overlayView" destination="ijE-Pa-OBq" id="n9e-Lg-4WO"/>
<outlet property="_submitButton" destination="oQD-QK-YPB" id="SEp-KK-YeP"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="5by-Sa-d9m" userLabel="First Responder" sceneMemberID="firstResponder"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Czu-9n-yKC" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="335" y="1260"/>
<point key="canvasLocation" x="403" y="560"/>
</scene>
</scenes>
<resources>
<image name="logo.png" width="282" height="44"/>
<image name="person.2" catalog="system" width="128" height="81"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@@ -26,7 +26,6 @@
<MtouchLink>None</MtouchLink>
<MtouchArch>x86_64</MtouchArch>
<MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler>
<DeviceSpecificBuild>false</DeviceSpecificBuild>
<MtouchVerbosity></MtouchVerbosity>
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
<AssemblyName>BitwardeniOSShareExtension</AssemblyName>
@@ -193,6 +192,10 @@
<Compile Include="LockPasswordViewController.designer.cs">
<DependentUpon>LockPasswordViewController.cs</DependentUpon>
</Compile>
<Compile Include="ExtensionNavigationController.cs" />
<Compile Include="ExtensionNavigationController.designer.cs">
<DependentUpon>ExtensionNavigationController.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\App\App.csproj">

View File

@@ -11,7 +11,7 @@
<key>CFBundleIdentifier</key>
<string>com.8bit.bitwarden</string>
<key>CFBundleShortVersionString</key>
<string>2022.6.1</string>
<string>2022.6.2</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleIconName</key>