mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
138 Commits
PM-2456-fi
...
feature/tr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9368be106 | ||
|
|
b2df06a7a1 | ||
|
|
3f4892fcc8 | ||
|
|
5f4cd62f66 | ||
|
|
9b5cde6a46 | ||
|
|
62b6d21371 | ||
|
|
9001fa1ccf | ||
|
|
af016cd13c | ||
|
|
3b9a9fc049 | ||
|
|
8205c0763c | ||
|
|
e9afc75f0a | ||
|
|
446b8c2d35 | ||
|
|
24378d937e | ||
|
|
6b2700bcaa | ||
|
|
aac66b23bd | ||
|
|
8221d55647 | ||
|
|
53e3429f6d | ||
|
|
faa9b1a9f7 | ||
|
|
c34d1da6e6 | ||
|
|
c4e64e082b | ||
|
|
4f62f5e5bc | ||
|
|
9bddc1d456 | ||
|
|
78004dbdb9 | ||
|
|
e820408a64 | ||
|
|
c595b1626e | ||
|
|
270a395d9f | ||
|
|
4fa8d2ba28 | ||
|
|
c75bcccf20 | ||
|
|
e076c9fe04 | ||
|
|
78788276ef | ||
|
|
5aaff1ea20 | ||
|
|
ee0dcd23f5 | ||
|
|
1e8ed1b5ce | ||
|
|
7fb89fa1a5 | ||
|
|
b1eb263fef | ||
|
|
61aac20555 | ||
|
|
3e87d74061 | ||
|
|
89a9185b20 | ||
|
|
e323e196c0 | ||
|
|
c793260689 | ||
|
|
c2ddbb7eff | ||
|
|
bb5a7383a8 | ||
|
|
0271a4db4c | ||
|
|
6fe8fc39ab | ||
|
|
375718f945 | ||
|
|
9eda015371 | ||
|
|
de5113ede7 | ||
|
|
ba6d260565 | ||
|
|
7562c688c5 | ||
|
|
c25906206e | ||
|
|
dfc7c55b77 | ||
|
|
ea81acb3bf | ||
|
|
174549e5bc | ||
|
|
87b1d18872 | ||
|
|
ae9ba810ff | ||
|
|
10574a7117 | ||
|
|
a2f1ca583a | ||
|
|
813ac841c6 | ||
|
|
0da3d25955 | ||
|
|
f8c9cde2ed | ||
|
|
080aabfe82 | ||
|
|
c0688c584e | ||
|
|
c09672ff88 | ||
|
|
635b6bc184 | ||
|
|
da7a1964ef | ||
|
|
73b8d8e6b8 | ||
|
|
c61f9f0357 | ||
|
|
a3183857b9 | ||
|
|
bedbca841d | ||
|
|
0ff314f076 | ||
|
|
c9a7c29190 | ||
|
|
546bf8dcb1 | ||
|
|
7fdc5597fc | ||
|
|
7c664f58b3 | ||
|
|
bdfe806846 | ||
|
|
5ed567ab90 | ||
|
|
cd4f44e6f6 | ||
|
|
d58f0b281b | ||
|
|
5ba3fac0c0 | ||
|
|
1e30524985 | ||
|
|
515decb4c9 | ||
|
|
bf28d373e9 | ||
|
|
69d38d4d75 | ||
|
|
c1619536aa | ||
|
|
079e02e4e5 | ||
|
|
15d3da607b | ||
|
|
b5cf9fd79d | ||
|
|
dd52ff0dcc | ||
|
|
c678c17ebc | ||
|
|
cd9e49b13b | ||
|
|
6d7970f767 | ||
|
|
9adc4d3080 | ||
|
|
1f20f70d13 | ||
|
|
a25da68437 | ||
|
|
fdc0313d10 | ||
|
|
b688b85d0f | ||
|
|
a5df6c0c65 | ||
|
|
f31c87b52e | ||
|
|
c2d4fa4429 | ||
|
|
1e79e1182f | ||
|
|
548bd12a8e | ||
|
|
58542fd255 | ||
|
|
800b4c71de | ||
|
|
11947ce99a | ||
|
|
3053eaa036 | ||
|
|
4abb472998 | ||
|
|
6268f0776b | ||
|
|
cbbc41be67 | ||
|
|
1d541e5b8e | ||
|
|
175b9936b6 | ||
|
|
72e67bd6f2 | ||
|
|
e164fb9823 | ||
|
|
87866304a6 | ||
|
|
216c6abcf6 | ||
|
|
1014563c75 | ||
|
|
3506269811 | ||
|
|
31487a31bb | ||
|
|
1407aa5655 | ||
|
|
16f59e2698 | ||
|
|
d876b54f45 | ||
|
|
6644e3b449 | ||
|
|
8d98d1d5bd | ||
|
|
3e9711f8f2 | ||
|
|
3af37f01d3 | ||
|
|
43d2d386b1 | ||
|
|
bc5c11b47f | ||
|
|
52843b4181 | ||
|
|
98705e443f | ||
|
|
1332ef7b43 | ||
|
|
04e30c2146 | ||
|
|
f604da13a1 | ||
|
|
dcf9acb51c | ||
|
|
3b087c50ae | ||
|
|
1c13ed9895 | ||
|
|
eeb634e698 | ||
|
|
8bc2df6c8a | ||
|
|
7cd40d4d89 | ||
|
|
84a82f0876 |
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -71,6 +71,11 @@ jobs:
|
||||
with:
|
||||
nuget-version: 5.9.0
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '3.1.x'
|
||||
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@1ff57057b5cfdc39105cd07a01d78e9b0ea0c14c # v1.3.1
|
||||
|
||||
@@ -793,7 +798,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Upload Sources
|
||||
uses: crowdin/github-action@ecd7eb0ef6f3cfa16293c79e9cbc4bc5b5fd9c49 # v1.4.9
|
||||
uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@ecd7eb0ef6f3cfa16293c79e9cbc4bc5b5fd9c49 # v1.4.9
|
||||
uses: crowdin/github-action@965d501f160af7b1f88aed4c29154b0caf1e94b9 # v1.9.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
2
.github/workflows/enforce-labels.yml
vendored
2
.github/workflows/enforce-labels.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Enforce Label
|
||||
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # v2.2.2
|
||||
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
|
||||
with:
|
||||
BANNED_LABELS: "hold,needs-qa"
|
||||
BANNED_LABELS_DESCRIPTION: "PRs with the hold or needs-qa labels cannot be merged"
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
|
||||
- name: Download all artifacts
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
|
||||
- name: Dry Run - Download all artifacts
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
|
||||
- name: Download F-Droid .apk artifact
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
|
||||
- name: Dry Run - Download F-Droid .apk artifact
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
|
||||
8
.github/workflows/version-auto-bump.yml
vendored
8
.github/workflows/version-auto-bump.yml
vendored
@@ -32,12 +32,8 @@ jobs:
|
||||
echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT
|
||||
|
||||
trigger_version_bump:
|
||||
name: "Version bump"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- setup
|
||||
steps:
|
||||
- name: Bump version to ${{ needs.setup.outputs.version_number }}
|
||||
name: Bump version to ${{ needs.setup.outputs.version_number }}
|
||||
needs: setup
|
||||
uses: ./.github/workflows/version-bump.yml
|
||||
secrets:
|
||||
AZURE_PROD_KV_CREDENTIALS: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# Bitwarden Mobile Application
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden" target="_blank"><img alt="Get it on Google Play" src="https://imgur.com/YQzmZi9.png" width="153" height="46"></a> <a href="https://mobileapp.bitwarden.com/fdroid/" target="_blank"><img alt="Get it on Google Play" src="https://i.imgur.com/HDicnzz.png" width="154" height="46"></a> <a href="https://itunes.apple.com/us/app/bitwarden-free-password-manager/id1137397744?mt=8" target="_blank"><img src="https://imgur.com/GdGqPMY.png" width="135" height="40"></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden" target="_blank"><img alt="Get it on Google Play" src="https://imgur.com/YQzmZi9.png" width="153" height="46"></a> <a href="https://mobileapp.bitwarden.com/fdroid/" target="_blank"><img alt="Get it on F-Droid" src="https://i.imgur.com/HDicnzz.png" width="154" height="46"></a> <a href="https://itunes.apple.com/us/app/bitwarden-free-password-manager/id1137397744?mt=8" target="_blank"><img src="https://imgur.com/GdGqPMY.png" width="135" height="40"></a>
|
||||
|
||||
The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin iOS, and Xamarin Forms.
|
||||
|
||||
|
||||
12
crowdin.yml
12
crowdin.yml
@@ -38,3 +38,15 @@ files:
|
||||
pt-PT: pt-PT
|
||||
en-GB: en-GB
|
||||
en-IN: en-IN
|
||||
- source: "/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/Localizable.strings"
|
||||
dest: "/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/en.lproj/%original_file_name%"
|
||||
translation: "/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization//%two_letters_code%.lproj/%original_file_name%"
|
||||
update_option: update_as_unapproved
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
zh-CN: zh-Hans
|
||||
zh-TW: zh-Hant
|
||||
pt-BR: pt-BR
|
||||
pt-PT: pt-PT
|
||||
en-GB: en-GB
|
||||
en-IN: en-IN
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
<Compile Include="Constants.cs" />
|
||||
<Compile Include="Effects\RemoveFontPaddingEffect.cs" />
|
||||
<Compile Include="Services\WatchDeviceService.cs" />
|
||||
<Compile Include="Renderers\CustomLabelRenderer.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
||||
@@ -232,6 +233,18 @@
|
||||
<SubType></SubType>
|
||||
<Generator></Generator>
|
||||
</AndroidResource>
|
||||
<AndroidResource Include="Resources\layout\validatable_input_dialog_layout.xml">
|
||||
<SubType></SubType>
|
||||
<Generator></Generator>
|
||||
</AndroidResource>
|
||||
<AndroidResource Include="Resources\drawable\empty_uris_placeholder.xml">
|
||||
<SubType></SubType>
|
||||
<Generator></Generator>
|
||||
</AndroidResource>
|
||||
<AndroidResource Include="Resources\drawable\empty_uris_placeholder_dark.xml">
|
||||
<SubType></SubType>
|
||||
<Generator></Generator>
|
||||
</AndroidResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
|
||||
|
||||
Binary file not shown.
@@ -54,16 +54,15 @@ namespace Bit.Droid.Autofill
|
||||
if (HintToFieldsMap.ContainsKey(View.AutofillHintPassword))
|
||||
{
|
||||
_passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]);
|
||||
return _passwordFields;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
_passwordFields = Fields.Where(f => FieldIsPassword(f)).ToList();
|
||||
if (!_passwordFields.Any())
|
||||
{
|
||||
_passwordFields = Fields.Where(f => FieldHasPasswordTerms(f)).ToList();
|
||||
}
|
||||
}
|
||||
return _passwordFields;
|
||||
}
|
||||
}
|
||||
@@ -87,9 +86,12 @@ namespace Bit.Droid.Autofill
|
||||
{
|
||||
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]);
|
||||
}
|
||||
}
|
||||
else
|
||||
if (_usernameFields.Any())
|
||||
{
|
||||
return _usernameFields;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var passwordField in PasswordFields)
|
||||
{
|
||||
var usernameField = Fields.TakeWhile(f => f.AutofillId != passwordField.AutofillId)
|
||||
@@ -104,7 +106,6 @@ namespace Bit.Droid.Autofill
|
||||
{
|
||||
_usernameFields = Fields.Where(f => FieldIsUsername(f)).ToList();
|
||||
}
|
||||
}
|
||||
return _usernameFields;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ namespace Bit.Droid
|
||||
private IAppIdService _appIdService;
|
||||
private IEventService _eventService;
|
||||
private IPushNotificationListenerService _pushNotificationListenerService;
|
||||
private IVaultTimeoutService _vaultTimeoutService;
|
||||
private ILogger _logger;
|
||||
private PendingIntent _eventUploadPendingIntent;
|
||||
private AppOptions _appOptions;
|
||||
@@ -69,7 +68,6 @@ namespace Bit.Droid
|
||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
_pushNotificationListenerService = ServiceContainer.Resolve<IPushNotificationListenerService>();
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
TabLayoutResource = Resource.Layout.Tabbar;
|
||||
@@ -234,7 +232,6 @@ namespace Bit.Droid
|
||||
|
||||
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
_vaultTimeoutService.ResetTimeoutDelay = true;
|
||||
if (resultCode == Result.Ok &&
|
||||
(requestCode == Core.Constants.SelectFileRequestCode || requestCode == Core.Constants.SaveFileRequestCode))
|
||||
{
|
||||
|
||||
@@ -70,7 +70,8 @@ namespace Bit.Droid
|
||||
var verificationActionsFlowHelper = new VerificationActionsFlowHelper(
|
||||
ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService"),
|
||||
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
|
||||
ServiceContainer.Resolve<ICryptoService>("cryptoService"));
|
||||
ServiceContainer.Resolve<ICryptoService>("cryptoService"),
|
||||
ServiceContainer.Resolve<IUserVerificationService>());
|
||||
ServiceContainer.Register<IVerificationActionsFlowHelper>("verificationActionsFlowHelper", verificationActionsFlowHelper);
|
||||
|
||||
var accountsManager = new AccountsManager(
|
||||
@@ -156,9 +157,9 @@ namespace Bit.Droid
|
||||
messagingService, broadcasterService);
|
||||
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService,
|
||||
platformUtilsService, new LazyResolve<IEventService>());
|
||||
var biometricService = new BiometricService(stateService);
|
||||
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
||||
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
|
||||
var biometricService = new BiometricService(stateService, cryptoService);
|
||||
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService);
|
||||
|
||||
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.5.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.7.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
|
||||
43
src/Android/Renderers/CustomLabelRenderer.cs
Normal file
43
src/Android/Renderers/CustomLabelRenderer.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.ComponentModel;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Droid.Renderers;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
|
||||
[assembly: ExportRenderer(typeof(CustomLabel), typeof(CustomLabelRenderer))]
|
||||
namespace Bit.Droid.Renderers
|
||||
{
|
||||
public class CustomLabelRenderer : LabelRenderer
|
||||
{
|
||||
public CustomLabelRenderer(Context context)
|
||||
: base(context)
|
||||
{ }
|
||||
|
||||
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
|
||||
{
|
||||
base.OnElementChanged(e);
|
||||
|
||||
if (Control != null && e.NewElement is CustomLabel label)
|
||||
{
|
||||
if (label.FontWeight.HasValue && Build.VERSION.SdkInt >= BuildVersionCodes.P)
|
||||
{
|
||||
Control.Typeface = Android.Graphics.Typeface.Create(null, label.FontWeight.Value, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
var label = sender as CustomLabel;
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(CustomLabel.AutomationId):
|
||||
Control.ContentDescription = label.AutomationId;
|
||||
break;
|
||||
}
|
||||
base.OnElementPropertyChanged(sender, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Android/Resources/drawable/empty_uris_placeholder.xml
Normal file
35
src/Android/Resources/drawable/empty_uris_placeholder.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="129"
|
||||
android:viewportHeight="124"
|
||||
android:width="129dp"
|
||||
android:height="124dp">
|
||||
<path
|
||||
android:pathData="M126.8227 61.9441A59.6843 59.6843 0 0 1 7.4541 61.9441A59.6843 59.6843 0 0 1 126.8227 61.9441Z"
|
||||
android:fillColor="#F0F0F0"
|
||||
android:strokeColor="#89929F"
|
||||
android:strokeWidth="3" />
|
||||
<path
|
||||
android:pathData="M21.6167 100.851C52.597 103.31 79.6937 80.3264 82.1391 49.5156C83.6205 30.8497 76.0789 14.8844 62.7275 3.63385"
|
||||
android:strokeColor="#89929F"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M14.5633 34.2845C12.2035 66.7711 38.5225 96.3429 72.6666 98.8232C74.2596 98.9389 78.629 98.9975 80.1951 99C84.6245 98.8232 97.8063 96.593 106.813 91.8485C113.439 88.3581 119.745 84.6984 124.644 79.1121"
|
||||
android:strokeColor="#89929F"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M124.502 48.5051C106.554 24.3817 68.8237 21.6709 41.4178 42.0617C24.8146 54.4149 14.7327 72.4183 13.9255 90.1427"
|
||||
android:strokeColor="#89929F"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M83.4034 28.3934A5 5 0 0 1 73.4034 28.3934A5 5 0 0 1 83.4034 28.3934Z"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M24.7698 66.5518A5 5 0 0 1 14.7698 66.5518A5 5 0 0 1 24.7698 66.5518Z"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M57.344 94.4726A5 5 0 0 1 47.344 94.4726A5 5 0 0 1 57.344 94.4726Z"
|
||||
android:fillColor="#89929F" />
|
||||
</vector>
|
||||
@@ -0,0 +1,35 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="129"
|
||||
android:viewportHeight="124"
|
||||
android:width="129dp"
|
||||
android:height="124dp">
|
||||
<path
|
||||
android:pathData="M126.8227 61.9441A59.6843 59.6843 0 0 1 7.4541 61.9441A59.6843 59.6843 0 0 1 126.8227 61.9441Z"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#A3A3A3"
|
||||
android:strokeWidth="3" />
|
||||
<path
|
||||
android:pathData="M21.6167 100.851C52.597 103.31 79.6937 80.3264 82.1391 49.5156C83.6205 30.8497 76.0789 14.8844 62.7275 3.63385"
|
||||
android:strokeColor="#A3A3A3"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M14.5633 34.2845C12.2035 66.7711 38.5225 96.3429 72.6666 98.8232C74.2596 98.9389 78.629 98.9975 80.1951 99C84.6245 98.8232 97.8063 96.593 106.813 91.8485C113.439 88.3581 119.745 84.6984 124.644 79.1121"
|
||||
android:strokeColor="#A3A3A3"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M124.502 48.5051C106.554 24.3817 68.8237 21.6709 41.4178 42.0617C24.8146 54.4149 14.7327 72.4183 13.9255 90.1427"
|
||||
android:strokeColor="#A3A3A3"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round" />
|
||||
<path
|
||||
android:pathData="M83.4034 28.3934A5 5 0 0 1 73.4034 28.3934A5 5 0 0 1 83.4034 28.3934Z"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M24.7698 66.5518A5 5 0 0 1 14.7698 66.5518A5 5 0 0 1 24.7698 66.5518Z"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M57.344 94.4726A5 5 0 0 1 47.344 94.4726A5 5 0 0 1 57.344 94.4726Z"
|
||||
android:fillColor="#A3A3A3" />
|
||||
</vector>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="30dp"
|
||||
android:paddingRight="30dp">
|
||||
<TextView
|
||||
android:id="@+id/lblHeader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/dialog_header_text_size"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="-3dp"
|
||||
android:labelFor="@+id/txtValue"/>
|
||||
<EditText
|
||||
android:id="@id/txtValue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/dialog_input_text_size"/>
|
||||
<TextView
|
||||
android:id="@+id/lblValueSubinfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/dialog_sub_value_info_text_size"/>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<dimen name="design_bottom_navigation_text_size" tools:override="true">15sp</dimen>
|
||||
<dimen name="design_bottom_navigation_active_text_size" tools:override="true">15sp</dimen>
|
||||
<dimen name="dialog_input_text_size">16sp</dimen>
|
||||
<dimen name="dialog_header_text_size">12sp</dimen>
|
||||
<dimen name="dialog_sub_value_info_text_size">12sp</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using Android.OS;
|
||||
using Android.Security.Keystore;
|
||||
using Bit.App.Services;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Java.Security;
|
||||
@@ -9,10 +10,8 @@ using Javax.Crypto;
|
||||
|
||||
namespace Bit.Droid.Services
|
||||
{
|
||||
public class BiometricService : IBiometricService
|
||||
public class BiometricService : BaseBiometricService
|
||||
{
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private const string KeyName = "com.8bit.bitwarden.biometric_integrity";
|
||||
|
||||
private const string KeyStoreName = "AndroidKeyStore";
|
||||
@@ -24,14 +23,14 @@ namespace Bit.Droid.Services
|
||||
|
||||
private readonly KeyStore _keystore;
|
||||
|
||||
public BiometricService(IStateService stateService)
|
||||
public BiometricService(IStateService stateService, ICryptoService cryptoService)
|
||||
: base(stateService, cryptoService)
|
||||
{
|
||||
_stateService = stateService;
|
||||
_keystore = KeyStore.GetInstance(KeyStoreName);
|
||||
_keystore.Load(null);
|
||||
}
|
||||
|
||||
public async Task<bool> SetupBiometricAsync(string bioIntegritySrcKey = null)
|
||||
public override async Task<bool> SetupBiometricAsync(string bioIntegritySrcKey = null)
|
||||
{
|
||||
if (Build.VERSION.SdkInt >= BuildVersionCodes.M)
|
||||
{
|
||||
@@ -41,7 +40,7 @@ namespace Bit.Droid.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null)
|
||||
public override async Task<bool> IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null)
|
||||
{
|
||||
if (Build.VERSION.SdkInt < BuildVersionCodes.M)
|
||||
{
|
||||
|
||||
@@ -13,12 +13,13 @@ using Android.Views.InputMethods;
|
||||
using Android.Widget;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.App.Utilities.Prompts;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Droid.Utilities;
|
||||
using Plugin.CurrentActivity;
|
||||
using static Bit.App.Pages.SettingsPageViewModel;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
|
||||
namespace Bit.Droid.Services
|
||||
{
|
||||
@@ -209,10 +210,7 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
if (numericKeyboard)
|
||||
{
|
||||
input.InputType = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
input.KeyListener = DigitsKeyListener.GetInstance(false, false);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
SetNumericKeyboardTo(input);
|
||||
}
|
||||
if (password)
|
||||
{
|
||||
@@ -248,6 +246,83 @@ namespace Bit.Droid.Services
|
||||
return result.Task;
|
||||
}
|
||||
|
||||
public Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config)
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
if (activity == null)
|
||||
{
|
||||
return Task.FromResult<ValidatablePromptResponse?>(null);
|
||||
}
|
||||
|
||||
var alertBuilder = new AlertDialog.Builder(activity);
|
||||
alertBuilder.SetTitle(config.Title);
|
||||
var view = activity.LayoutInflater.Inflate(Resource.Layout.validatable_input_dialog_layout, null);
|
||||
alertBuilder.SetView(view);
|
||||
|
||||
var result = new TaskCompletionSource<ValidatablePromptResponse?>();
|
||||
|
||||
alertBuilder.SetOnCancelListener(new BasicDialogWithResultCancelListener(result));
|
||||
alertBuilder.SetPositiveButton(config.OkButtonText ?? AppResources.Ok, listener: null);
|
||||
alertBuilder.SetNegativeButton(config.CancelButtonText ?? AppResources.Cancel, (sender, args) => result.TrySetResult(null));
|
||||
if (!string.IsNullOrEmpty(config.ThirdButtonText))
|
||||
{
|
||||
alertBuilder.SetNeutralButton(config.ThirdButtonText, (sender, args) => result.TrySetResult(new ValidatablePromptResponse(null, true)));
|
||||
}
|
||||
|
||||
var alert = alertBuilder.Create();
|
||||
|
||||
var input = view.FindViewById<EditText>(Resource.Id.txtValue);
|
||||
var lblHeader = view.FindViewById<TextView>(Resource.Id.lblHeader);
|
||||
var lblValueSubinfo = view.FindViewById<TextView>(Resource.Id.lblValueSubinfo);
|
||||
|
||||
lblHeader.Text = config.Subtitle;
|
||||
lblValueSubinfo.Text = config.ValueSubInfo;
|
||||
|
||||
var defaultSubInfoColor = lblValueSubinfo.TextColors;
|
||||
|
||||
input.InputType = InputTypes.ClassText;
|
||||
|
||||
if (config.NumericKeyboard)
|
||||
{
|
||||
SetNumericKeyboardTo(input);
|
||||
}
|
||||
|
||||
input.ImeOptions = input.ImeOptions | (ImeAction)ImeFlags.NoPersonalizedLearning | (ImeAction)ImeFlags.NoExtractUi;
|
||||
input.Text = config.Text ?? string.Empty;
|
||||
input.SetSelection(config.Text?.Length ?? 0);
|
||||
input.AfterTextChanged += (sender, args) =>
|
||||
{
|
||||
if (lblValueSubinfo.Text != config.ValueSubInfo)
|
||||
{
|
||||
lblValueSubinfo.Text = config.ValueSubInfo;
|
||||
lblHeader.SetTextColor(defaultSubInfoColor);
|
||||
lblValueSubinfo.SetTextColor(defaultSubInfoColor);
|
||||
}
|
||||
};
|
||||
|
||||
alert.Window.SetSoftInputMode(SoftInput.StateVisible);
|
||||
alert.Show();
|
||||
|
||||
var positiveButton = alert.GetButton((int)DialogButtonType.Positive);
|
||||
positiveButton.Click += (sender, args) =>
|
||||
{
|
||||
var error = config.ValidateText(input.Text);
|
||||
if (error != null)
|
||||
{
|
||||
lblHeader.SetTextColor(ThemeManager.GetResourceColor("DangerColor").ToAndroid());
|
||||
lblValueSubinfo.SetTextColor(ThemeManager.GetResourceColor("DangerColor").ToAndroid());
|
||||
lblValueSubinfo.Text = error;
|
||||
lblValueSubinfo.SendAccessibilityEvent(Android.Views.Accessibility.EventTypes.ViewFocused);
|
||||
return;
|
||||
}
|
||||
|
||||
result.TrySetResult(new ValidatablePromptResponse(input.Text, false));
|
||||
alert.Dismiss();
|
||||
};
|
||||
|
||||
return result.Task;
|
||||
}
|
||||
|
||||
public void RateApp()
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
@@ -525,5 +600,29 @@ namespace Bit.Droid.Services
|
||||
// only used by iOS
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void SetNumericKeyboardTo(EditText editText)
|
||||
{
|
||||
editText.InputType = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
editText.KeyListener = DigitsKeyListener.GetInstance(false, false);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
|
||||
class BasicDialogWithResultCancelListener : Java.Lang.Object, IDialogInterfaceOnCancelListener
|
||||
{
|
||||
private readonly TaskCompletionSource<ValidatablePromptResponse?> _taskCompletionSource;
|
||||
|
||||
public BasicDialogWithResultCancelListener(TaskCompletionSource<ValidatablePromptResponse?> taskCompletionSource)
|
||||
{
|
||||
_taskCompletionSource = taskCompletionSource;
|
||||
}
|
||||
|
||||
public void OnCancel(IDialogInterface dialog)
|
||||
{
|
||||
_taskCompletionSource?.TrySetResult(null);
|
||||
dialog?.Dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Java.Lang;
|
||||
|
||||
namespace Bit.Droid.Utilities
|
||||
{
|
||||
@@ -13,7 +14,12 @@ namespace Bit.Droid.Utilities
|
||||
// Note: getting the bundle like this will cause to call unparcel() internally
|
||||
var b = intent?.Extras?.GetBundle("trashstringwhichhasnousebuttocheckunparcel");
|
||||
}
|
||||
catch (BadParcelableException)
|
||||
catch (Exception ex) when
|
||||
(
|
||||
ex is BadParcelableException ||
|
||||
ex is ClassNotFoundException ||
|
||||
ex is RuntimeException
|
||||
)
|
||||
{
|
||||
intent.ReplaceExtras((Bundle)null);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities.Prompts;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
|
||||
@@ -18,6 +19,7 @@ namespace Bit.App.Abstractions
|
||||
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
|
||||
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
||||
bool autofocus = true, bool password = false);
|
||||
Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config);
|
||||
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
|
||||
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
|
||||
|
||||
|
||||
@@ -9,7 +9,5 @@ namespace Bit.App.Abstractions
|
||||
Task<bool> ShowPasswordPromptAsync();
|
||||
|
||||
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
|
||||
|
||||
Task<bool> Enabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,8 @@
|
||||
<Folder Include="Controls\DateTime\" />
|
||||
<Folder Include="Controls\IconLabelButton\" />
|
||||
<Folder Include="Controls\PasswordStrengthProgressBar\" />
|
||||
<Folder Include="Utilities\Automation\" />
|
||||
<Folder Include="Utilities\Prompts\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -440,5 +442,7 @@
|
||||
<None Remove="MessagePack" />
|
||||
<None Remove="MessagePack.MSBuild.Tasks" />
|
||||
<None Remove="Controls\PasswordStrengthProgressBar\" />
|
||||
<None Remove="Utilities\Automation\" />
|
||||
<None Remove="Utilities\Prompts\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -297,7 +297,7 @@ namespace Bit.App
|
||||
{
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
// Reset delay on every start
|
||||
_vaultTimeoutService.DelayTimeoutMs = null;
|
||||
_vaultTimeoutService.DelayLockAndLogoutMs = null;
|
||||
}
|
||||
|
||||
await _configService.GetAsync();
|
||||
|
||||
@@ -30,13 +30,15 @@
|
||||
BackgroundColor="{DynamicResource BackgroundColor}"
|
||||
VerticalOptions="Start"
|
||||
RowHeight="{Binding AccountListRowHeight, Source={x:Reference _mainOverlay}}"
|
||||
effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never">
|
||||
effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never"
|
||||
AutomationId="AccountListView">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="view:AccountView">
|
||||
<controls:AccountViewCell
|
||||
Account="{Binding .}"
|
||||
SelectAccountCommand="{Binding SelectAccountCommand, Source={x:Reference _mainOverlay}}"
|
||||
LongPressAccountCommand="{Binding LongPressAccountCommand, Source={x:Reference _mainOverlay}}"
|
||||
AutomationId="AccountViewCell"
|
||||
/>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
|
||||
@@ -60,20 +60,23 @@
|
||||
Text="{Binding AccountView.Email}"
|
||||
IsVisible="{Binding IsActive}"
|
||||
StyleClass="accountlist-title, accountlist-title-platform"
|
||||
LineBreakMode="TailTruncation" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountEmailLabel" />
|
||||
<Label
|
||||
Grid.Row="0"
|
||||
Text="{Binding AccountView.Email}"
|
||||
IsVisible="{Binding IsActive, Converter={StaticResource inverseBool}}"
|
||||
StyleClass="accountlist-title, accountlist-title-platform"
|
||||
TextColor="{DynamicResource MutedColor}"
|
||||
LineBreakMode="TailTruncation" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountEmailLabel" />
|
||||
<Label
|
||||
Grid.Row="1"
|
||||
IsVisible="{Binding ShowHostname}"
|
||||
Text="{Binding AccountView.Hostname}"
|
||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||
LineBreakMode="TailTruncation" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountHostUrlLabel" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Text="{u:I18n AccountUnlocked}"
|
||||
@@ -81,7 +84,8 @@
|
||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||
FontAttributes="Italic"
|
||||
TextTransform="Lowercase"
|
||||
LineBreakMode="TailTruncation" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountStatusLabel" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Text="{u:I18n AccountLocked}"
|
||||
@@ -89,7 +93,8 @@
|
||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||
FontAttributes="Italic"
|
||||
TextTransform="Lowercase"
|
||||
LineBreakMode="TailTruncation" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountStatusLabel" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Text="{u:I18n AccountLoggedOut}"
|
||||
@@ -97,7 +102,8 @@
|
||||
StyleClass="accountlist-sub, accountlist-sub-platform"
|
||||
FontAttributes="Italic"
|
||||
TextTransform="Lowercase"
|
||||
LineBreakMode="TailTruncation" />
|
||||
LineBreakMode="TailTruncation"
|
||||
AutomationId="AccountStatusLabel" />
|
||||
</Grid>
|
||||
|
||||
<controls:IconLabel
|
||||
@@ -107,7 +113,8 @@
|
||||
Margin="12,0"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform" />
|
||||
StyleClass="list-icon, list-icon-platform"
|
||||
AutomationId="InactiveVaultIcon" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="2"
|
||||
Text="{Binding AuthStatusIconActive}"
|
||||
@@ -116,7 +123,8 @@
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform"
|
||||
TextColor="{DynamicResource TextColor}"/>
|
||||
TextColor="{DynamicResource TextColor}"
|
||||
AutomationId="ActiveVaultIcon" />
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
@@ -147,7 +155,8 @@
|
||||
StyleClass="accountlist-title, accountlist-title-platform"
|
||||
LineBreakMode="TailTruncation"
|
||||
VerticalOptions="Center"
|
||||
Grid.Column="1" />
|
||||
Grid.Column="1"
|
||||
AutomationId="AddAccountButton" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ViewCell>
|
||||
@@ -9,7 +9,8 @@
|
||||
StyleClass="list-row, list-row-platform"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0"
|
||||
x:DataType="controls:CipherViewCellViewModel">
|
||||
x:DataType="controls:CipherViewCellViewModel"
|
||||
AutomationId="CipherCell">
|
||||
|
||||
<Grid.Resources>
|
||||
<u:IconGlyphConverter x:Key="iconGlyphConverter"/>
|
||||
@@ -36,7 +37,8 @@
|
||||
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
|
||||
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
|
||||
ShouldUpdateFontSizeDynamicallyForAccesibility="True"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="CipherTypeIcon" />
|
||||
|
||||
<ff:CachedImage
|
||||
x:Name="_iconImage"
|
||||
@@ -52,7 +54,8 @@
|
||||
Aspect="AspectFit"
|
||||
IsVisible="{Binding ShowIconImage}"
|
||||
Source="{Binding IconImageSource, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="CipherWebsiteIcon" />
|
||||
|
||||
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center" Padding="0, 7">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -71,7 +74,8 @@
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Cipher.Name}" />
|
||||
Text="{Binding Cipher.Name}"
|
||||
AutomationId="CipherNameLabel" />
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
@@ -80,7 +84,8 @@
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Cipher.SubTitle}"
|
||||
IsVisible="{Binding Source={RelativeSource Self}, Path=Text,
|
||||
Converter={StaticResource stringHasValueConverter}}"/>
|
||||
Converter={StaticResource stringHasValueConverter}}"
|
||||
AutomationId="CipherSubTitleLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
@@ -91,7 +96,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Collection}}"
|
||||
IsVisible="{Binding Cipher.Shared, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Shared}" />
|
||||
AutomationProperties.Name="{u:I18n Shared}"
|
||||
AutomationId="CipherInCollectionIcon" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="2"
|
||||
Grid.Row="0"
|
||||
@@ -102,7 +108,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Paperclip}}"
|
||||
IsVisible="{Binding Cipher.HasAttachments, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Attachments}" />
|
||||
AutomationProperties.Name="{u:I18n Attachments}"
|
||||
AutomationId="CipherWithAttachmentsIcon" />
|
||||
</Grid>
|
||||
|
||||
<controls:MiButton
|
||||
@@ -114,6 +121,7 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="EndAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
AutomationId="CipherOptionsButton" />
|
||||
|
||||
</controls:ExtendedGrid>
|
||||
@@ -31,7 +31,7 @@ namespace Bit.App.Controls
|
||||
public bool ShowIconImage
|
||||
{
|
||||
get => WebsiteIconsEnabled
|
||||
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
|
||||
&& !string.IsNullOrWhiteSpace(Cipher.LaunchUri)
|
||||
&& IconImageSource != null;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Bit.App.Controls
|
||||
{
|
||||
if (_iconImageSource == string.Empty) // default value since icon source can return null
|
||||
{
|
||||
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
|
||||
_iconImageSource = IconImageHelper.GetIconImage(Cipher);
|
||||
}
|
||||
return _iconImageSource;
|
||||
}
|
||||
|
||||
13
src/App/Controls/CustomLabel.cs
Normal file
13
src/App/Controls/CustomLabel.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class CustomLabel : Label
|
||||
{
|
||||
public CustomLabel()
|
||||
{
|
||||
}
|
||||
|
||||
public int? FontWeight { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Controls.SendViewCell"
|
||||
@@ -54,14 +54,16 @@
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Send.Name}" />
|
||||
Text="{Binding Send.Name}"
|
||||
AutomationId="SendNameLabel" />
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="6"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Send.DisplayDate}" />
|
||||
Text="{Binding Send.DisplayDate}"
|
||||
AutomationId="SendDateLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
@@ -72,7 +74,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ExclamationTriangle}}"
|
||||
IsVisible="{Binding Send.Disabled, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Disabled}" />
|
||||
AutomationProperties.Name="{u:I18n Disabled}"
|
||||
AutomationId="DisabledSendLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="2"
|
||||
Grid.Row="0"
|
||||
@@ -83,7 +86,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Key}}"
|
||||
IsVisible="{Binding Send.HasPassword, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Password}" />
|
||||
AutomationProperties.Name="{u:I18n Password}"
|
||||
AutomationId="PasswordProtectedSendLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="3"
|
||||
Grid.Row="0"
|
||||
@@ -94,7 +98,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Ban}}"
|
||||
IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n MaxAccessCountReached}" />
|
||||
AutomationProperties.Name="{u:I18n MaxAccessCountReached}"
|
||||
AutomationId="SendMaxAccessCountReachedLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="4"
|
||||
Grid.Row="0"
|
||||
@@ -105,7 +110,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clock}}"
|
||||
IsVisible="{Binding Send.Expired, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Expired}" />
|
||||
AutomationProperties.Name="{u:I18n Expired}"
|
||||
AutomationId="ExpiredSendLabel" />
|
||||
<controls:IconLabel
|
||||
Grid.Column="5"
|
||||
Grid.Row="0"
|
||||
@@ -116,7 +122,8 @@
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||
IsVisible="{Binding Send.PendingDelete, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n PendingDelete}" />
|
||||
AutomationProperties.Name="{u:I18n PendingDelete}"
|
||||
AutomationId="SendWithPendingDeletionLabel" />
|
||||
</Grid>
|
||||
|
||||
<controls:MiButton
|
||||
@@ -129,6 +136,7 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="EndAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
AutomationId="SendOptionsButton" />
|
||||
|
||||
</controls:ExtendedGrid>
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="BooleanCustomFieldNameLabel" />
|
||||
<Label
|
||||
Text="{Binding Field.Name, Mode=OneWay}"
|
||||
IsVisible="{Binding IsEditing}"
|
||||
@@ -49,13 +50,15 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="0, 5, 0, 0"
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="BooleanCustomFieldValueLabel" />
|
||||
<Switch
|
||||
IsToggled="{Binding BooleanValue}"
|
||||
IsVisible="{Binding IsEditing}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationId="BooleanCustomFieldValueToggle" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
Text="{Binding Field.Name, Mode=OneWay}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="HiddenCustomFieldNameLabel" />
|
||||
<StackLayout
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
@@ -39,7 +40,8 @@
|
||||
<controls:MonoLabel
|
||||
Text="{Binding ValueText, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
IsVisible="{Binding ShowHiddenValue}" />
|
||||
IsVisible="{Binding ShowHiddenValue}"
|
||||
AutomationId="HiddenCustomFieldValueLabel" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding Field.MaskedValue, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
@@ -54,7 +56,10 @@
|
||||
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding ShowViewHidden}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False">
|
||||
IsTextPredictionEnabled="False"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{Binding Field.Name}"
|
||||
AutomationId="HiddenCustomFieldValueEntry">
|
||||
<Entry.Keyboard>
|
||||
<Keyboard x:FactoryMethod="Create">
|
||||
<x:Arguments>
|
||||
@@ -72,7 +77,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationId="HiddenCustomFieldShowValueButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
|
||||
@@ -29,13 +29,15 @@
|
||||
Text="{Binding Field.Name, Mode=OneWay}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="LinkedCustomFieldNameLabel" />
|
||||
<controls:IconLabel
|
||||
Text="{Binding ValueText, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="LinkedCustomFieldValueLabel" />
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding IsEditing}">
|
||||
@@ -44,7 +46,8 @@
|
||||
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
|
||||
ItemDisplayBinding="{Binding Key}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="LinkedCustomFieldValuePicker" />
|
||||
</StackLayout>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
@@ -55,7 +58,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
AutomationId="LinkedCustomFieldOptionsButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
</StackLayout>
|
||||
|
||||
@@ -29,19 +29,24 @@
|
||||
Text="{Binding Field.Name, Mode=OneWay}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="TextCustomFieldNameLabel" />
|
||||
<Label
|
||||
Text="{Binding ValueText, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="TextCustomFieldValueLabel" />
|
||||
<Entry
|
||||
Text="{Binding Field.Value}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding IsEditing}" />
|
||||
IsVisible="{Binding IsEditing}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{Binding Field.Name}"
|
||||
AutomationId="TextCustomFieldValueEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
@@ -51,7 +56,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Copy}" />
|
||||
AutomationProperties.Name="{u:I18n Copy}"
|
||||
AutomationId="TextCustomFieldCopyValue" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||
@@ -61,7 +67,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
AutomationId="TextCustomFieldOptionsButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||
</StackLayout>
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
Placeholder="ex. https://bitwarden.company.com"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="ServerUrlEntry"/>
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n SelfHostedEnvironmentFooter}"
|
||||
@@ -53,7 +54,8 @@
|
||||
x:Name="_webVaultEntry"
|
||||
Text="{Binding WebVaultUrl}"
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="WebVaultUrlEntry"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
@@ -63,7 +65,8 @@
|
||||
x:Name="_apiEntry"
|
||||
Text="{Binding ApiUrl}"
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ApiUrlEntry"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
@@ -73,7 +76,8 @@
|
||||
x:Name="_identityEntry"
|
||||
Text="{Binding IdentityUrl}"
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="IdentityUrlEntry"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
@@ -85,7 +89,8 @@
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="IconsUrlEntry"/>
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n CustomEnvironmentFooter}"
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
Priority="-1"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Account}" />
|
||||
AutomationProperties.Name="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
@@ -49,7 +50,9 @@
|
||||
Keyboard="Email"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding ContinueCommand}">
|
||||
ReturnCommand="{Binding ContinueCommand}"
|
||||
AutomationId="EmailAddressEntry"
|
||||
>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Disabled">
|
||||
@@ -78,7 +81,8 @@
|
||||
FontSize="13"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
VerticalOptions="Center"
|
||||
VerticalTextAlignment="Center"/>
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="RegionSelectorDropdown"/>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
@@ -92,21 +96,27 @@
|
||||
StyleClass="text-sm"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="Center"
|
||||
VerticalTextAlignment="Center"/>
|
||||
VerticalTextAlignment="Center"
|
||||
/>
|
||||
<Switch
|
||||
Scale="0.8"
|
||||
IsToggled="{Binding RememberEmail}"
|
||||
VerticalOptions="Center"/>
|
||||
VerticalOptions="Center"
|
||||
AutomationId="RememberMeSwitch"
|
||||
/>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n Continue}"
|
||||
StyleClass="btn-primary"
|
||||
IsEnabled="{Binding CanContinue}"
|
||||
Command="{Binding ContinueCommand}" />
|
||||
Command="{Binding ContinueCommand}"
|
||||
AutomationId="ContinueButton"
|
||||
/>
|
||||
|
||||
<Label FormattedText="{Binding CreateAccountText}"
|
||||
Margin="0, 10"
|
||||
StyleClass="box-footer-label">
|
||||
StyleClass="box-footer-label"
|
||||
AutomationId="CreateAccountLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding CreateAccountCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
@@ -132,5 +142,4 @@
|
||||
MainPage="{Binding Source={x:Reference _page}}"
|
||||
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
|
||||
</AbsoluteLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -7,17 +7,17 @@ using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class HomeViewModel : BaseViewModel
|
||||
{
|
||||
private const string LOGGING_IN_ON_US = "bitwarden.com";
|
||||
private const string LOGGING_IN_ON_EU = "bitwarden.eu";
|
||||
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
@@ -30,8 +30,6 @@ namespace Bit.App.Pages
|
||||
private bool _rememberEmail;
|
||||
private string _email;
|
||||
private string _selectedEnvironmentName;
|
||||
private bool _isEmailEnabled;
|
||||
private bool _canLogin;
|
||||
private bool _displayEuEnvironment;
|
||||
|
||||
public HomeViewModel()
|
||||
@@ -86,7 +84,7 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _selectedEnvironmentName, value);
|
||||
}
|
||||
|
||||
public string RegionText => $"{AppResources.Region}:";
|
||||
public string RegionText => $"{AppResources.LoggingInOn}:";
|
||||
public bool CanContinue => !string.IsNullOrEmpty(Email);
|
||||
|
||||
public FormattedString CreateAccountText
|
||||
@@ -167,12 +165,12 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_displayEuEnvironment = await _configService.GetFeatureFlagBoolAsync(Constants.DisplayEuEnvironmentFlag);
|
||||
var options = _displayEuEnvironment
|
||||
? new string[] { AppResources.US, AppResources.EU, AppResources.SelfHosted }
|
||||
: new string[] { AppResources.US, AppResources.SelfHosted };
|
||||
? new string[] { LOGGING_IN_ON_US, LOGGING_IN_ON_EU, AppResources.SelfHosted }
|
||||
: new string[] { LOGGING_IN_ON_US, AppResources.SelfHosted };
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
var result = await Page.DisplayActionSheet(AppResources.DataRegion, AppResources.Cancel, null, options);
|
||||
var result = await Page.DisplayActionSheet(AppResources.LoggingInOn, AppResources.Cancel, null, options);
|
||||
|
||||
if (result is null || result == AppResources.Cancel)
|
||||
{
|
||||
@@ -185,7 +183,7 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
await _environmentService.SetUrlsAsync(result == AppResources.EU ? EnvironmentUrlData.DefaultEU : EnvironmentUrlData.DefaultUS);
|
||||
await _environmentService.SetUrlsAsync(result == LOGGING_IN_ON_EU ? EnvironmentUrlData.DefaultEU : EnvironmentUrlData.DefaultUS);
|
||||
await _configService.GetAsync(true);
|
||||
SelectedEnvironmentName = result;
|
||||
});
|
||||
@@ -198,17 +196,17 @@ namespace Bit.App.Pages
|
||||
{
|
||||
await _environmentService.SetUrlsAsync(EnvironmentUrlData.DefaultUS);
|
||||
environmentsSaved = EnvironmentUrlData.DefaultUS;
|
||||
SelectedEnvironmentName = AppResources.US;
|
||||
SelectedEnvironmentName = LOGGING_IN_ON_US;
|
||||
return;
|
||||
}
|
||||
|
||||
if (environmentsSaved.Base == EnvironmentUrlData.DefaultUS.Base)
|
||||
{
|
||||
SelectedEnvironmentName = AppResources.US;
|
||||
SelectedEnvironmentName = LOGGING_IN_ON_US;
|
||||
}
|
||||
else if (environmentsSaved.Base == EnvironmentUrlData.DefaultEU.Base)
|
||||
{
|
||||
SelectedEnvironmentName = AppResources.EU;
|
||||
SelectedEnvironmentName = LOGGING_IN_ON_EU;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
Priority="-1"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Account}" />
|
||||
AutomationProperties.Name="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
@@ -45,7 +46,7 @@
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding PinLock}"
|
||||
IsVisible="{Binding PinEnabled}"
|
||||
Padding="0, 10, 0, 0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -71,7 +72,8 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="PinEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -81,12 +83,13 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="PinVisibilityToggle" />
|
||||
</Grid>
|
||||
<Grid
|
||||
x:Name="_passwordGrid"
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding PinLock, Converter={StaticResource inverseBool}}"
|
||||
IsVisible="{Binding PinEnabled, Converter={StaticResource inverseBool}}"
|
||||
Padding="0, 10, 0, 0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -111,7 +114,8 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="MasterPasswordEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -121,7 +125,9 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="PasswordVisibilityToggle"
|
||||
/>
|
||||
</Grid>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
@@ -147,7 +153,8 @@
|
||||
x:Name="_unlockButton"
|
||||
Text="{u:I18n Unlock}"
|
||||
StyleClass="btn-primary"
|
||||
Clicked="Unlock_Clicked" />
|
||||
Clicked="Unlock_Clicked"
|
||||
AutomationId="UnlockVaultButton" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -20,13 +20,14 @@ namespace Bit.App.Pages
|
||||
private bool _promptedAfterResume;
|
||||
private bool _appeared;
|
||||
|
||||
public LockPage(AppOptions appOptions = null, bool autoPromptBiometric = true)
|
||||
public LockPage(AppOptions appOptions = null, bool autoPromptBiometric = true, bool checkPendingAuthRequests = true)
|
||||
{
|
||||
_appOptions = appOptions;
|
||||
_autoPromptBiometric = autoPromptBiometric;
|
||||
InitializeComponent();
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
||||
_vm = BindingContext as LockPageViewModel;
|
||||
_vm.CheckPendingAuthRequests = checkPendingAuthRequests;
|
||||
_vm.Page = this;
|
||||
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
||||
|
||||
@@ -44,7 +45,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_vm?.PinLock ?? false)
|
||||
if (_vm?.PinEnabled ?? false)
|
||||
{
|
||||
return _pin;
|
||||
}
|
||||
@@ -54,7 +55,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task PromptBiometricAfterResumeAsync()
|
||||
{
|
||||
if (_vm.BiometricLock)
|
||||
if (_vm.BiometricEnabled)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
if (!_promptedAfterResume)
|
||||
@@ -91,13 +92,13 @@ namespace Bit.App.Pages
|
||||
|
||||
_vm.FocusSecretEntry += PerformFocusSecretEntry;
|
||||
|
||||
if (!_vm.BiometricLock)
|
||||
if (!_vm.BiometricEnabled)
|
||||
{
|
||||
RequestFocus(SecretEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_vm.UsingKeyConnector && !_vm.PinLock)
|
||||
if (!_vm.HasMasterPassword && !_vm.PinEnabled)
|
||||
{
|
||||
_passwordGrid.IsVisible = false;
|
||||
_unlockButton.IsVisible = false;
|
||||
|
||||
@@ -27,27 +27,27 @@ namespace Bit.App.Pages
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IBiometricService _biometricService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IWatchDeviceService _watchDeviceService;
|
||||
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
|
||||
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly ISyncService _syncService;
|
||||
private string _email;
|
||||
private string _masterPassword;
|
||||
private string _pin;
|
||||
private bool _showPassword;
|
||||
private bool _pinLock;
|
||||
private bool _biometricLock;
|
||||
private PinLockType _pinStatus;
|
||||
private bool _pinEnabled;
|
||||
private bool _biometricEnabled;
|
||||
private bool _biometricIntegrityValid = true;
|
||||
private bool _biometricButtonVisible;
|
||||
private bool _usingKeyConnector;
|
||||
private bool _hasMasterPassword;
|
||||
private string _biometricButtonText;
|
||||
private string _loggedInAsText;
|
||||
private string _lockedVerifyText;
|
||||
private bool _isPinProtected;
|
||||
private bool _isPinProtectedWithKey;
|
||||
|
||||
public LockPageViewModel()
|
||||
{
|
||||
@@ -60,11 +60,13 @@ namespace Bit.App.Pages
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>();
|
||||
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||
|
||||
PageTitle = AppResources.VerifyMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@@ -100,21 +102,21 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
|
||||
public bool PinLock
|
||||
public bool PinEnabled
|
||||
{
|
||||
get => _pinLock;
|
||||
set => SetProperty(ref _pinLock, value);
|
||||
get => _pinEnabled;
|
||||
set => SetProperty(ref _pinEnabled, value);
|
||||
}
|
||||
|
||||
public bool UsingKeyConnector
|
||||
public bool HasMasterPassword
|
||||
{
|
||||
get => _usingKeyConnector;
|
||||
get => _hasMasterPassword;
|
||||
}
|
||||
|
||||
public bool BiometricLock
|
||||
public bool BiometricEnabled
|
||||
{
|
||||
get => _biometricLock;
|
||||
set => SetProperty(ref _biometricLock, value);
|
||||
get => _biometricEnabled;
|
||||
set => SetProperty(ref _biometricEnabled, value);
|
||||
}
|
||||
|
||||
public bool BiometricIntegrityValid
|
||||
@@ -147,6 +149,8 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _lockedVerifyText, value);
|
||||
}
|
||||
|
||||
public bool CheckPendingAuthRequests { get; set; }
|
||||
|
||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
@@ -162,18 +166,32 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
(_isPinProtected, _isPinProtectedWithKey) = await _vaultTimeoutService.IsPinLockSetAsync();
|
||||
PinLock = (_isPinProtected && await _stateService.GetPinProtectedKeyAsync() != null) ||
|
||||
_isPinProtectedWithKey;
|
||||
BiometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _cryptoService.HasKeyAsync();
|
||||
|
||||
// Users with key connector and without biometric or pin has no MP to unlock with
|
||||
_usingKeyConnector = await _keyConnectorService.GetUsesKeyConnector();
|
||||
if (_usingKeyConnector && !(BiometricLock || PinLock))
|
||||
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||
if (pendingRequest != null && CheckPendingAuthRequests)
|
||||
{
|
||||
await _vaultTimeoutService.LogOutAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_pinStatus = await _vaultTimeoutService.GetPinLockTypeAsync();
|
||||
|
||||
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
|
||||
?? await _stateService.GetPinProtectedKeyAsync();
|
||||
PinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) ||
|
||||
_pinStatus == PinLockType.Persistent;
|
||||
BiometricEnabled = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _biometricService.CanUseBiometricsUnlockAsync();
|
||||
|
||||
// Users without MP and without biometric or pin has no MP to unlock with
|
||||
_hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync();
|
||||
if (await _stateService.IsAuthenticatedAsync()
|
||||
&& !_hasMasterPassword
|
||||
&& !BiometricEnabled
|
||||
&& !PinEnabled)
|
||||
{
|
||||
await _vaultTimeoutService.LogOutAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_email = await _stateService.GetEmailAsync();
|
||||
if (string.IsNullOrWhiteSpace(_email))
|
||||
{
|
||||
@@ -188,26 +206,18 @@ namespace Bit.App.Pages
|
||||
}
|
||||
var webVaultHostname = CoreHelpers.GetHostname(webVault);
|
||||
LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, webVaultHostname);
|
||||
if (PinLock)
|
||||
if (PinEnabled)
|
||||
{
|
||||
PageTitle = AppResources.VerifyPIN;
|
||||
LockedVerifyText = AppResources.VaultLockedPIN;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_usingKeyConnector)
|
||||
{
|
||||
PageTitle = AppResources.UnlockVault;
|
||||
LockedVerifyText = AppResources.VaultLockedIdentity;
|
||||
}
|
||||
else
|
||||
{
|
||||
PageTitle = AppResources.VerifyMasterPassword;
|
||||
LockedVerifyText = AppResources.VaultLockedMasterPassword;
|
||||
}
|
||||
PageTitle = _hasMasterPassword ? AppResources.VerifyMasterPassword : AppResources.UnlockVault;
|
||||
LockedVerifyText = _hasMasterPassword ? AppResources.VaultLockedMasterPassword : AppResources.VaultLockedIdentity;
|
||||
}
|
||||
|
||||
if (BiometricLock)
|
||||
if (BiometricEnabled)
|
||||
{
|
||||
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
||||
if (!_biometricIntegrityValid)
|
||||
@@ -229,14 +239,14 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
if (PinLock && string.IsNullOrWhiteSpace(Pin))
|
||||
if (PinEnabled && string.IsNullOrWhiteSpace(Pin))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.PIN),
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (!PinLock && string.IsNullOrWhiteSpace(MasterPassword))
|
||||
if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
@@ -247,34 +257,54 @@ namespace Bit.App.Pages
|
||||
ShowPassword = false;
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
|
||||
if (PinLock)
|
||||
if (PinEnabled)
|
||||
{
|
||||
var failed = true;
|
||||
try
|
||||
{
|
||||
if (_isPinProtected)
|
||||
EncString userKeyPin = null;
|
||||
EncString oldPinProtected = null;
|
||||
if (_pinStatus == PinLockType.Persistent)
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email,
|
||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
|
||||
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
|
||||
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
|
||||
}
|
||||
else if (_pinStatus == PinLockType.Transient)
|
||||
{
|
||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
|
||||
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
|
||||
}
|
||||
|
||||
UserKey userKey;
|
||||
if (oldPinProtected != null)
|
||||
{
|
||||
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
|
||||
_pinStatus == PinLockType.Transient,
|
||||
Pin,
|
||||
_email,
|
||||
kdfConfig,
|
||||
await _stateService.GetPinProtectedKeyAsync());
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
||||
oldPinProtected
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
|
||||
Pin,
|
||||
_email,
|
||||
kdfConfig,
|
||||
userKeyPin
|
||||
);
|
||||
}
|
||||
|
||||
var protectedPin = await _stateService.GetProtectedPinAsync();
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
|
||||
failed = decPin != Pin;
|
||||
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
|
||||
failed = decryptedPin != Pin;
|
||||
if (!failed)
|
||||
{
|
||||
Pin = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email, kdfConfig);
|
||||
failed = false;
|
||||
Pin = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -295,19 +325,21 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdfConfig);
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig);
|
||||
var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync();
|
||||
var passwordValid = false;
|
||||
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
|
||||
|
||||
if (storedKeyHash != null)
|
||||
{
|
||||
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, key);
|
||||
// Offline unlock possible
|
||||
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Online unlock required
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
|
||||
var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.ServerAuthorization);
|
||||
var request = new PasswordVerificationRequest();
|
||||
request.MasterPasswordHash = keyHash;
|
||||
|
||||
@@ -316,8 +348,8 @@ namespace Bit.App.Pages
|
||||
var response = await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
|
||||
passwordValid = true;
|
||||
var localKeyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
|
||||
await _cryptoService.SetKeyHashAsync(localKeyHash);
|
||||
var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.LocalAuthorization);
|
||||
await _cryptoService.SetMasterKeyHashAsync(localKeyHash);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -327,15 +359,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (passwordValid)
|
||||
{
|
||||
if (_isPinProtected)
|
||||
{
|
||||
var protectedPin = await _stateService.GetProtectedPinAsync();
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, _email, kdfConfig);
|
||||
await _stateService.SetPinProtectedKeyAsync(await _cryptoService.EncryptAsync(key.Key, pinKey));
|
||||
}
|
||||
|
||||
if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions))
|
||||
{
|
||||
// Save the ForcePasswordResetReason to force a password reset after unlock
|
||||
@@ -345,10 +368,13 @@ namespace Bit.App.Pages
|
||||
|
||||
MasterPassword = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
|
||||
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
||||
await _cryptoService.SetMasterKeyAsync(masterKey);
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
|
||||
// Re-enable biometrics
|
||||
if (BiometricLock & !BiometricIntegrityValid)
|
||||
if (BiometricEnabled & !BiometricIntegrityValid)
|
||||
{
|
||||
await _biometricService.SetupBiometricAsync();
|
||||
}
|
||||
@@ -425,7 +451,7 @@ namespace Bit.App.Pages
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
var secret = PinLock ? Pin : MasterPassword;
|
||||
var secret = PinEnabled ? Pin : MasterPassword;
|
||||
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
|
||||
}
|
||||
|
||||
@@ -433,32 +459,35 @@ namespace Bit.App.Pages
|
||||
{
|
||||
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
||||
BiometricButtonVisible = BiometricIntegrityValid;
|
||||
if (!BiometricLock || !BiometricIntegrityValid)
|
||||
if (!BiometricEnabled || !BiometricIntegrityValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
PinLock ? AppResources.PIN : AppResources.MasterPassword,
|
||||
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
|
||||
await _stateService.SetBiometricLockedAsync(!success);
|
||||
if (success)
|
||||
{
|
||||
await DoContinueAsync();
|
||||
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key)
|
||||
private async Task SetUserKeyAndContinueAsync(UserKey key)
|
||||
{
|
||||
var hasKey = await _cryptoService.HasKeyAsync();
|
||||
var hasKey = await _cryptoService.HasUserKeyAsync();
|
||||
if (!hasKey)
|
||||
{
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
await _cryptoService.SetUserKeyAsync(key);
|
||||
}
|
||||
await _deviceTrustCryptoService.TrustDeviceIfNeededAsync();
|
||||
await DoContinueAsync();
|
||||
}
|
||||
|
||||
private async Task DoContinueAsync()
|
||||
{
|
||||
_syncService.FullSyncAsync(false).FireAndForget();
|
||||
await _stateService.SetBiometricLockedAsync(false);
|
||||
_watchDeviceService.SyncDataToWatchAsync().FireAndForget();
|
||||
_messagingService.Send("unlocked");
|
||||
|
||||
76
src/App/Pages/Accounts/LoginApproveDevicePage.xaml
Normal file
76
src/App/Pages/Accounts/LoginApproveDevicePage.xaml
Normal file
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.LoginApproveDevicePage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginApproveDeviceViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginApproveDeviceViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<StackLayout Padding="10, 10">
|
||||
<StackLayout Padding="5, 10" Orientation="Horizontal">
|
||||
<StackLayout HorizontalOptions="FillAndExpand">
|
||||
<Label
|
||||
StyleClass="text-md"
|
||||
Text="{u:I18n RememberThisDevice}"/>
|
||||
<Label
|
||||
StyleClass="box-sub-label"
|
||||
Text="{u:I18n TurnOffUsingPublicDevice}"/>
|
||||
</StackLayout>
|
||||
<Switch
|
||||
Scale="0.8"
|
||||
IsToggled="{Binding RememberThisDevice}"
|
||||
VerticalOptions="Center"/>
|
||||
</StackLayout>
|
||||
<StackLayout Margin="0, 20, 0, 0">
|
||||
<Button
|
||||
x:Name="_continue"
|
||||
Text="{u:I18n Continue}"
|
||||
StyleClass="btn-primary"
|
||||
Command="{Binding ContinueCommand}"
|
||||
IsVisible="{Binding IsNewUser}"/>
|
||||
<Button
|
||||
x:Name="_approveWithMyOtherDevice"
|
||||
Text="{u:I18n ApproveWithMyOtherDevice}"
|
||||
StyleClass="btn-primary"
|
||||
Command="{Binding ApproveWithMyOtherDeviceCommand}"
|
||||
IsVisible="{Binding ApproveWithMyOtherDeviceEnabled}"/>
|
||||
<Button
|
||||
x:Name="_requestAdminApproval"
|
||||
Text="{u:I18n RequestAdminApproval}"
|
||||
StyleClass="box-button-row"
|
||||
Command="{Binding RequestAdminApprovalCommand}"
|
||||
IsVisible="{Binding RequestAdminApprovalEnabled}"/>
|
||||
<Button
|
||||
x:Name="_approveWithMasterPassword"
|
||||
Text="{u:I18n ApproveWithMasterPassword}"
|
||||
StyleClass="box-button-row"
|
||||
Command="{Binding ApproveWithMasterPasswordCommand}"
|
||||
IsVisible="{Binding ApproveWithMasterPasswordEnabled}"/>
|
||||
<Label
|
||||
Text="{Binding LoggingInAsText}"
|
||||
StyleClass="text-sm"
|
||||
Margin="0,40,0,0"
|
||||
AutomationId="LoggingInAsLabel"
|
||||
/>
|
||||
<Label
|
||||
Text="{u:I18n NotYou}"
|
||||
StyleClass="text-md"
|
||||
HorizontalOptions="Start"
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationId="NotYouLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding LogoutCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
||||
|
||||
|
||||
64
src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs
Normal file
64
src/App/Pages/Accounts/LoginApproveDevicePage.xaml.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginApproveDevicePage : BaseContentPage
|
||||
{
|
||||
|
||||
private readonly LoginApproveDeviceViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
public LoginApproveDevicePage(AppOptions appOptions = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginApproveDeviceViewModel;
|
||||
_vm.LogInWithMasterPasswordAction = () => StartLogInWithMasterPasswordAsync().FireAndForget();
|
||||
_vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget();
|
||||
_vm.RequestAdminApprovalAction = () => RequestAdminApprovalAsync().FireAndForget();
|
||||
_vm.ContinueToVaultAction = () => ContinueToVaultAsync().FireAndForget();
|
||||
_vm.Page = this;
|
||||
_appOptions = appOptions;
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
_vm.InitAsync();
|
||||
}
|
||||
|
||||
private async Task ContinueToVaultAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
|
||||
private async Task StartLogInWithMasterPasswordAsync()
|
||||
{
|
||||
var page = new LockPage(_appOptions, checkPendingAuthRequests: false);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartLoginWithDeviceAsync()
|
||||
{
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task RequestAdminApprovalAsync()
|
||||
{
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AdminApproval, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
150
src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs
Normal file
150
src/App/Pages/Accounts/LoginApproveDeviceViewModel.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities.AccountManagement;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginApproveDeviceViewModel : BaseViewModel
|
||||
{
|
||||
private bool _rememberThisDevice;
|
||||
private bool _approveWithMyOtherDeviceEnabled;
|
||||
private bool _requestAdminApprovalEnabled;
|
||||
private bool _approveWithMasterPasswordEnabled;
|
||||
private string _email;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IApiService _apiService;
|
||||
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
|
||||
public ICommand ApproveWithMyOtherDeviceCommand { get; }
|
||||
public ICommand RequestAdminApprovalCommand { get; }
|
||||
public ICommand ApproveWithMasterPasswordCommand { get; }
|
||||
public ICommand ContinueCommand { get; }
|
||||
public ICommand LogoutCommand { get; }
|
||||
|
||||
public Action LogInWithMasterPasswordAction { get; set; }
|
||||
public Action LogInWithDeviceAction { get; set; }
|
||||
public Action RequestAdminApprovalAction { get; set; }
|
||||
public Action ContinueToVaultAction { get; set; }
|
||||
|
||||
public LoginApproveDeviceViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_apiService = ServiceContainer.Resolve<IApiService>();
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_authService = ServiceContainer.Resolve<IAuthService>();
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>();
|
||||
|
||||
PageTitle = AppResources.LogInInitiated;
|
||||
RememberThisDevice = true;
|
||||
|
||||
ApproveWithMyOtherDeviceCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithDeviceAction),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
RequestAdminApprovalCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(RequestAdminApprovalAction),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
ApproveWithMasterPasswordCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithMasterPasswordAction),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
ContinueCommand = new AsyncCommand(CreateNewSsoUserAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
LogoutCommand = new Command(() => _messagingService.Send(AccountsManagerMessageCommands.LOGOUT));
|
||||
}
|
||||
|
||||
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
|
||||
|
||||
public bool RememberThisDevice
|
||||
{
|
||||
get => _rememberThisDevice;
|
||||
set => SetProperty(ref _rememberThisDevice, value);
|
||||
}
|
||||
|
||||
public bool ApproveWithMyOtherDeviceEnabled
|
||||
{
|
||||
get => _approveWithMyOtherDeviceEnabled;
|
||||
set => SetProperty(ref _approveWithMyOtherDeviceEnabled, value);
|
||||
}
|
||||
|
||||
public bool RequestAdminApprovalEnabled
|
||||
{
|
||||
get => _requestAdminApprovalEnabled;
|
||||
set => SetProperty(ref _requestAdminApprovalEnabled, value,
|
||||
additionalPropertyNames: new[] { nameof(IsNewUser) });
|
||||
}
|
||||
|
||||
public bool ApproveWithMasterPasswordEnabled
|
||||
{
|
||||
get => _approveWithMasterPasswordEnabled;
|
||||
set => SetProperty(ref _approveWithMasterPasswordEnabled, value,
|
||||
additionalPropertyNames: new[] { nameof(IsNewUser) });
|
||||
}
|
||||
|
||||
public bool IsNewUser => !RequestAdminApprovalEnabled && !ApproveWithMasterPasswordEnabled;
|
||||
|
||||
public string Email
|
||||
{
|
||||
get => _email;
|
||||
set => SetProperty(ref _email, value, additionalPropertyNames:
|
||||
new string[] {
|
||||
nameof(LoggingInAsText)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Email = await _stateService.GetActiveUserEmailAsync();
|
||||
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
RequestAdminApprovalEnabled = decryptOptions?.TrustedDeviceOption?.HasAdminApproval ?? false;
|
||||
ApproveWithMasterPasswordEnabled = decryptOptions?.HasMasterPassword ?? false;
|
||||
ApproveWithMyOtherDeviceEnabled = decryptOptions?.TrustedDeviceOption?.HasLoginApprovingDevice ?? false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateNewSsoUserAsync()
|
||||
{
|
||||
await _authService.CreateNewSsoUserAsync(await _stateService.GetRememberedOrgIdentifierAsync());
|
||||
if (RememberThisDevice)
|
||||
{
|
||||
await _deviceTrustCryptoService.TrustDeviceAsync();
|
||||
}
|
||||
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
await Device.InvokeOnMainThreadAsync(ContinueToVaultAction);
|
||||
}
|
||||
|
||||
private async Task SetDeviceTrustAndInvokeAsync(Action action)
|
||||
{
|
||||
await _deviceTrustCryptoService.SetShouldTrustDeviceAsync(RememberThisDevice);
|
||||
await Device.InvokeOnMainThreadAsync(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,23 +9,23 @@
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
Title="{Binding PageTitle}"
|
||||
AutomationId="PageTitleLabel">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<controls:ExtendedToolbarItem
|
||||
x:Name="_accountAvatar"
|
||||
x:Key="accountAvatar"
|
||||
IconImageSource="{Binding AvatarImageSource}"
|
||||
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
|
||||
Order="Primary"
|
||||
Priority="-1"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Account}" />
|
||||
AutomationProperties.Name="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
@@ -34,7 +34,8 @@
|
||||
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
||||
x:Name="_moreItem" x:Key="moreItem"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
AutomationId="OptionsButton" />
|
||||
<ToolbarItem Text="{u:I18n GetPasswordHint}"
|
||||
x:Key="getPasswordHint"
|
||||
x:Name="_getPasswordHint"
|
||||
@@ -75,7 +76,9 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding LogInCommand}" />
|
||||
ReturnCommand="{Binding LogInCommand}"
|
||||
AutomationId="MasterPasswordEntry"
|
||||
/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -84,6 +87,7 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationId="PasswordVisibilityToggle"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||
<Label
|
||||
@@ -93,7 +97,9 @@
|
||||
Padding="0,5,0,0"
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2">
|
||||
Grid.ColumnSpan="2"
|
||||
AutomationId="GetMasterPasswordHintLabel"
|
||||
>
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="Hint_Clicked" />
|
||||
</Label.GestureRecognizers>
|
||||
@@ -104,19 +110,24 @@
|
||||
<Button x:Name="_loginWithMasterPassword"
|
||||
Text="{u:I18n LogInWithMasterPassword}"
|
||||
StyleClass="btn-primary"
|
||||
Clicked="LogIn_Clicked" />
|
||||
Clicked="LogIn_Clicked"
|
||||
AutomationId="LogInWithMasterPasswordButton"
|
||||
/>
|
||||
<controls:IconLabelButton
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
|
||||
Label="{u:I18n LogInWithAnotherDevice}"
|
||||
ButtonCommand="{Binding LogInWithDeviceCommand}"
|
||||
IsVisible="{Binding IsKnownDevice}"/>
|
||||
IsVisible="{Binding IsKnownDevice}"
|
||||
AutomationId="LogInWithAnotherDeviceButton"
|
||||
/>
|
||||
<controls:IconLabelButton
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Suitcase}}"
|
||||
Label="{u:I18n LogInSso}">
|
||||
Label="{u:I18n LogInSso}"
|
||||
AutomationId="LogInWithSsoButton">
|
||||
<controls:IconLabelButton.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="LogInSSO_Clicked" />
|
||||
</controls:IconLabelButton.GestureRecognizers>
|
||||
@@ -124,12 +135,15 @@
|
||||
<Label
|
||||
Text="{Binding LoggingInAsText}"
|
||||
StyleClass="text-sm"
|
||||
Margin="0,40,0,0"/>
|
||||
Margin="0,40,0,0"
|
||||
AutomationId="LoggingInAsLabel"
|
||||
/>
|
||||
<Label
|
||||
Text="{u:I18n NotYou}"
|
||||
StyleClass="text-md"
|
||||
HorizontalOptions="Start"
|
||||
TextColor="{DynamicResource HyperlinkColor}">
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationId="NotYouLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="Cancel_Clicked" />
|
||||
</Label.GestureRecognizers>
|
||||
|
||||
@@ -4,6 +4,7 @@ using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
@@ -135,7 +136,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task StartLoginWithDeviceAsync()
|
||||
{
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, _appOptions);
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
<Label
|
||||
Text="{Binding LogInAttemptByLabel}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,24"/>
|
||||
Margin="0,0,0,24"
|
||||
AutomationId="LogInAttemptByLabel" />
|
||||
<Label
|
||||
Text="{u:I18n FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
@@ -41,7 +42,8 @@
|
||||
FormattedText="{Binding LoginRequest.FingerprintPhrase}"
|
||||
FontSize="Medium"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"
|
||||
Margin="0,0,0,27"/>
|
||||
Margin="0,0,0,27"
|
||||
AutomationId="FingerprintValueLabel" />
|
||||
<Label
|
||||
Text="{u:I18n DeviceType}"
|
||||
FontSize="Small"
|
||||
@@ -49,7 +51,8 @@
|
||||
<Label
|
||||
Text="{Binding LoginRequest.DeviceType}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,21"/>
|
||||
Margin="0,0,0,21"
|
||||
AutomationId="DeviceTypeValueLabel" />
|
||||
<Label
|
||||
Text="{u:I18n IpAddress}"
|
||||
IsVisible="{Binding ShowIpAddress}"
|
||||
@@ -59,7 +62,8 @@
|
||||
Text="{Binding LoginRequest.IpAddress}"
|
||||
IsVisible="{Binding ShowIpAddress}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,21"/>
|
||||
Margin="0,0,0,21"
|
||||
AutomationId="IpAddressValueLabel" />
|
||||
<Label
|
||||
Text="{u:I18n Time}"
|
||||
FontSize="Small"
|
||||
@@ -67,7 +71,8 @@
|
||||
<Label
|
||||
Text="{Binding TimeOfRequestText}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,57"/>
|
||||
Margin="0,0,0,57"
|
||||
AutomationId="TimeOfRequestValueLabel" />
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -75,11 +80,13 @@
|
||||
Text="{u:I18n ConfirmLogIn}"
|
||||
Command="{Binding AcceptRequestCommand}"
|
||||
Margin="0,0,0,17"
|
||||
StyleClass="btn-primary"/>
|
||||
StyleClass="btn-primary"
|
||||
AutomationId="ConfirmLoginButton" />
|
||||
<Button
|
||||
Text="{u:I18n DenyLogIn}"
|
||||
Command="{Binding RejectRequestCommand}"
|
||||
StyleClass="btn-secundary"/>
|
||||
StyleClass="btn-secundary"
|
||||
AutomationId="DenyLoginButton" />
|
||||
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -21,16 +21,17 @@
|
||||
<StackLayout
|
||||
Padding="7, 0, 7, 20">
|
||||
<Label
|
||||
Text="{u:I18n LogInInitiated}"
|
||||
Text="{Binding Title}"
|
||||
FontSize="Title"
|
||||
FontAttributes="Bold"
|
||||
Margin="0,14,0,21"/>
|
||||
Margin="0,14,0,21"
|
||||
AutomationId="LogInInitiatedLabel" />
|
||||
<Label
|
||||
Text="{u:I18n ANotificationHasBeenSentToYourDevice}"
|
||||
Text="{Binding SubTitle}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,10"/>
|
||||
<Label
|
||||
Text="{u:I18n PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice}"
|
||||
Text="{Binding Description}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,24"/>
|
||||
<Label
|
||||
@@ -39,38 +40,39 @@
|
||||
FontAttributes="Bold"/>
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding FingerprintPhrase}"
|
||||
FontSize="Medium"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"/>
|
||||
FontSize="Small"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"
|
||||
AutomationId="FingerprintPhraseValue" />
|
||||
<Label
|
||||
Text="{u:I18n ResendNotification}"
|
||||
StyleClass="text-md"
|
||||
IsVisible="{Binding ResendNotificationVisible}"
|
||||
StyleClass="text-sm"
|
||||
FontAttributes="Bold"
|
||||
HorizontalOptions="Start"
|
||||
Margin="0,40,0,0"
|
||||
TextColor="{DynamicResource HyperlinkColor}">
|
||||
Margin="0,24,0,0"
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationId="ResendNotificationButton">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding CreatePasswordlessLoginCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Margin="0,30,0,0">
|
||||
<BoxView
|
||||
HeightRequest="1"
|
||||
Margin="0,24,0,24"
|
||||
Color="{DynamicResource DisabledIconColor}" />
|
||||
<Label
|
||||
Text="{u:I18n NeedAnotherOption}"
|
||||
FontSize="Small"
|
||||
VerticalTextAlignment="End"/>
|
||||
Text="{Binding OtherOptions}"
|
||||
FontSize="Small"/>
|
||||
<Label
|
||||
Text="{u:I18n ViewAllLoginOptions}"
|
||||
StyleClass="text-md"
|
||||
VerticalTextAlignment="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Margin="5, 0"
|
||||
TextColor="{DynamicResource HyperlinkColor}">
|
||||
StyleClass="text-sm"
|
||||
FontAttributes="Bold"
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationId="ViewAllLoginOptionsButton">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding CloseCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
</StackLayout>
|
||||
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -12,13 +13,14 @@ namespace Bit.App.Pages
|
||||
private LoginPasswordlessRequestViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
public LoginPasswordlessRequestPage(string email, AppOptions appOptions = null)
|
||||
public LoginPasswordlessRequestPage(string email, AuthRequestType authRequestType, AppOptions appOptions = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
_appOptions = appOptions;
|
||||
_vm = BindingContext as LoginPasswordlessRequestViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.Email = email;
|
||||
_vm.AuthRequestType = authRequestType;
|
||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -12,6 +13,7 @@ using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
@@ -32,6 +34,9 @@ namespace Bit.App.Pages
|
||||
private IPlatformUtilsService _platformUtilsService;
|
||||
private IEnvironmentService _environmentService;
|
||||
private ILogger _logger;
|
||||
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
|
||||
protected override II18nService i18nService => _i18nService;
|
||||
protected override IEnvironmentService environmentService => _environmentService;
|
||||
@@ -44,6 +49,7 @@ namespace Bit.App.Pages
|
||||
private string _email;
|
||||
private string _requestId;
|
||||
private string _requestAccessCode;
|
||||
private AuthRequestType _authRequestType;
|
||||
// Item1 publicKey, Item2 privateKey
|
||||
private Tuple<byte[], byte[]> _requestKeyPair;
|
||||
|
||||
@@ -57,8 +63,9 @@ namespace Bit.App.Pages
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>();
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
|
||||
PageTitle = AppResources.LogInWithAnotherDevice;
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>();
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
||||
|
||||
CreatePasswordlessLoginCommand = new AsyncCommand(CreatePasswordlessLoginAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
@@ -77,6 +84,86 @@ namespace Bit.App.Pages
|
||||
public ICommand CreatePasswordlessLoginCommand { get; }
|
||||
public ICommand CloseCommand { get; }
|
||||
|
||||
public string HeaderTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.LogInWithDevice;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.LogInInitiated;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string Title
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.LogInInitiated;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.AdminApprovalRequested;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string SubTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.ANotificationHasBeenSentToYourDevice;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.YourRequestHasBeenSentToYourAdmin;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.YouWillBeNotifiedOnceApproved;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string OtherOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.LogInWithDeviceMustBeSetUpInTheSettingsOfTheBitwardenAppNeedAnotherOption;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.TroubleLoggingIn;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string FingerprintPhrase
|
||||
{
|
||||
get => _fingerprintPhrase;
|
||||
@@ -89,6 +176,25 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _email, value);
|
||||
}
|
||||
|
||||
public AuthRequestType AuthRequestType
|
||||
{
|
||||
get => _authRequestType;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _authRequestType, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(Title),
|
||||
nameof(SubTitle),
|
||||
nameof(Description),
|
||||
nameof(OtherOptions),
|
||||
nameof(ResendNotificationVisible)
|
||||
});
|
||||
PageTitle = HeaderTitle;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ResendNotificationVisible => AuthRequestType == AuthRequestType.AuthenticateAndUnlock;
|
||||
|
||||
public void StartCheckLoginRequestStatus()
|
||||
{
|
||||
try
|
||||
@@ -119,14 +225,22 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task CheckLoginRequestStatus()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_requestAccessCode))
|
||||
if (string.IsNullOrEmpty(_requestId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _authService.GetPasswordlessLoginResponseAsync(_requestId, _requestAccessCode);
|
||||
PasswordlessLoginResponse response = null;
|
||||
if (await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode);
|
||||
}
|
||||
|
||||
if (response.RequestApproved == null || !response.RequestApproved.Value)
|
||||
{
|
||||
@@ -138,6 +252,12 @@ namespace Bit.App.Pages
|
||||
var authResult = await _authService.LogInPasswordlessAsync(Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
|
||||
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
await HandleLoginCompleteAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus))
|
||||
{
|
||||
return;
|
||||
@@ -153,8 +273,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
LogInSuccessAction?.Invoke();
|
||||
await HandleLoginCompleteAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -164,22 +283,67 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleLoginCompleteAsync()
|
||||
{
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
LogInSuccessAction?.Invoke();
|
||||
}
|
||||
|
||||
private async Task CreatePasswordlessLoginAsync()
|
||||
{
|
||||
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
|
||||
|
||||
var response = await _authService.PasswordlessCreateLoginRequestAsync(_email);
|
||||
if (response != null)
|
||||
PasswordlessLoginResponse response = null;
|
||||
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||
if (pendingRequest != null && _authRequestType == AuthRequestType.AdminApproval)
|
||||
{
|
||||
response = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
|
||||
if (response == null || (response.IsAnswered && !response.RequestApproved.Value))
|
||||
{
|
||||
// handle pending auth request not valid remove it from state
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
pendingRequest = null;
|
||||
response = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Derive pubKey from privKey in state to avoid MITM attacks
|
||||
// Also generate FingerprintPhrase locally for the same reason
|
||||
var derivedPublicKey = await _cryptoFunctionService.RsaExtractPublicKeyAsync(pendingRequest.PrivateKey);
|
||||
response.FingerprintPhrase = string.Join("-", await _cryptoService.GetFingerprintAsync(Email, derivedPublicKey));
|
||||
response.RequestKeyPair = new Tuple<byte[], byte[]>(derivedPublicKey, pendingRequest.PrivateKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
response = await _authService.PasswordlessCreateLoginRequestAsync(_email, AuthRequestType);
|
||||
}
|
||||
|
||||
await HandlePasswordlessLoginAsync(response, pendingRequest == null && _authRequestType == AuthRequestType.AdminApproval);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
|
||||
private async Task HandlePasswordlessLoginAsync(PasswordlessLoginResponse response, bool createPendingAdminRequest)
|
||||
{
|
||||
if (response == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(response));
|
||||
}
|
||||
|
||||
if (createPendingAdminRequest)
|
||||
{
|
||||
var pendingAuthRequest = new PendingAdminAuthRequest { Id = response.Id, PrivateKey = response.RequestKeyPair.Item2 };
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(pendingAuthRequest);
|
||||
}
|
||||
|
||||
FingerprintPhrase = response.FingerprintPhrase;
|
||||
_requestId = response.Id;
|
||||
_requestAccessCode = response.RequestAccessCode;
|
||||
_requestKeyPair = response.RequestKeyPair;
|
||||
}
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
|
||||
private void HandleException(Exception ex)
|
||||
{
|
||||
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace Bit.App.Pages
|
||||
_vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync());
|
||||
_vm.UpdateTempPasswordAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
_vm.StartDeviceApprovalOptionsAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
@@ -106,10 +108,17 @@ namespace Bit.App.Pages
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartDeviceApprovalOptionsAsync()
|
||||
{
|
||||
var page = new LoginApproveDevicePage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task SsoAuthSuccessAsync()
|
||||
{
|
||||
RestoreAppOptionsFromCopy();
|
||||
await AppHelpers.ClearPreviousPage();
|
||||
|
||||
if (await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
||||
|
||||
@@ -9,6 +9,7 @@ using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
@@ -29,6 +30,8 @@ namespace Bit.App.Pages
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
|
||||
private string _orgIdentifier;
|
||||
|
||||
@@ -45,7 +48,8 @@ namespace Bit.App.Pages
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_organizationService = ServiceContainer.Resolve<IOrganizationService>();
|
||||
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
||||
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
|
||||
@@ -61,6 +65,7 @@ namespace Bit.App.Pages
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action SsoAuthSuccessAction { get; set; }
|
||||
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
|
||||
@@ -144,7 +149,6 @@ namespace Bit.App.Pages
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
|
||||
new Uri(REDIRECT_URI));
|
||||
|
||||
|
||||
var code = GetResultCode(authResult, state);
|
||||
if (!string.IsNullOrEmpty(code))
|
||||
{
|
||||
@@ -197,14 +201,22 @@ namespace Bit.App.Pages
|
||||
try
|
||||
{
|
||||
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
|
||||
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (response.TwoFactor)
|
||||
{
|
||||
StartTwoFactorAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
else if (response.ResetMasterPassword)
|
||||
|
||||
if (decryptOptions?.TrustedDeviceOption != null)
|
||||
{
|
||||
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||
if (!decryptOptions.HasMasterPassword &&
|
||||
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
}
|
||||
@@ -212,13 +224,66 @@ namespace Bit.App.Pages
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
}
|
||||
else if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
|
||||
{
|
||||
if (decryptOptions.TrustedDeviceOption.EncryptedPrivateKey == null && decryptOptions.TrustedDeviceOption.EncryptedUserKey == null)
|
||||
{
|
||||
await _deviceTrustCryptoService.RemoveTrustedDeviceAsync();
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
else if (pendingRequest != null)
|
||||
{
|
||||
var authRequest = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
|
||||
if (authRequest != null && authRequest.RequestApproved != null && authRequest.RequestApproved.Value)
|
||||
{
|
||||
var authResult = await _authService.LogInPasswordlessAsync(await _stateService.GetActiveUserEmailAsync(), authRequest.RequestAccessCode, pendingRequest.Id, pendingRequest.PrivateKey, authRequest.Key, authRequest.MasterPasswordHash);
|
||||
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
await Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(
|
||||
() => _platformUtilsService.ShowToast("info", null, AppResources.LoginApproved));
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In the standard, non TDE case, a user must set password if they don't
|
||||
// have one and they aren't using key connector.
|
||||
// Note: TDE & Key connector are mutually exclusive org config options.
|
||||
if (response.ResetMasterPassword || (decryptOptions?.RequireSetPassword ?? false))
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
x:Name="_email"
|
||||
Text="{Binding Email}"
|
||||
Keyboard="Email"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="EmailAddressEntry"/>
|
||||
</StackLayout>
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -59,7 +60,8 @@
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="MasterPasswordEntry"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -69,7 +71,8 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="PasswordVisibilityToggle"/>
|
||||
</Grid>
|
||||
<Label
|
||||
StyleClass="box-sub-label"
|
||||
@@ -109,7 +112,8 @@
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="ConfirmMasterPasswordEntry"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -118,6 +122,7 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationId="ConfirmPasswordVisibilityToggle"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</Grid>
|
||||
@@ -130,7 +135,8 @@
|
||||
Text="{Binding Hint}"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="MasterPasswordHintLabel" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHintDescription}"
|
||||
@@ -142,7 +148,8 @@
|
||||
IsToggled="{Binding CheckExposedMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="Start"
|
||||
Margin="0, 0, 10, 0"/>
|
||||
Margin="0, 0, 10, 0"
|
||||
AutomationId="CheckExposedMasterPasswordToggle"/>
|
||||
<Label
|
||||
Text="{u:I18n CheckKnownDataBreachesForThisPassword}"
|
||||
StyleClass="box-footer-label"
|
||||
@@ -154,7 +161,8 @@
|
||||
IsToggled="{Binding AcceptPolicies}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="Start"
|
||||
Margin="0, 0, 10, 0"/>
|
||||
Margin="0, 0, 10, 0"
|
||||
AutomationId="AcceptPoliciesToggle"/>
|
||||
<Label StyleClass="box-footer-label"
|
||||
HorizontalOptions="Fill">
|
||||
<Label.FormattedText>
|
||||
|
||||
@@ -177,25 +177,25 @@ namespace Bit.App.Pages
|
||||
Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
|
||||
Email = Email.Trim().ToLower();
|
||||
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, Email, kdfConfig);
|
||||
var encKey = await _cryptoService.MakeEncKeyAsync(key);
|
||||
var hashedPassword = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1);
|
||||
var newMasterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, Email, kdfConfig);
|
||||
var (newUserKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(newMasterKey);
|
||||
var hashedPassword = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey);
|
||||
var (newPublicKey, newProtectedPrivateKey) = await _cryptoService.MakeKeyPairAsync(newUserKey);
|
||||
var request = new RegisterRequest
|
||||
{
|
||||
Email = Email,
|
||||
Name = Name,
|
||||
MasterPasswordHash = hashedPassword,
|
||||
MasterPasswordHint = Hint,
|
||||
Key = encKey.Item2.EncryptedString,
|
||||
Key = newProtectedUserKey.EncryptedString,
|
||||
Kdf = kdfConfig.Type,
|
||||
KdfIterations = kdfConfig.Iterations,
|
||||
KdfMemory = kdfConfig.Memory,
|
||||
KdfParallelism = kdfConfig.Parallelism,
|
||||
Keys = new KeysRequest
|
||||
{
|
||||
PublicKey = keys.Item1,
|
||||
EncryptedPrivateKey = keys.Item2.EncryptedString
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newProtectedPrivateKey.EncryptedString
|
||||
},
|
||||
CaptchaResponse = _captchaToken,
|
||||
};
|
||||
|
||||
@@ -30,14 +30,14 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task Init()
|
||||
{
|
||||
Organization = await _keyConnectorService.GetManagingOrganization();
|
||||
Organization = await _keyConnectorService.GetManagingOrganizationAsync();
|
||||
}
|
||||
|
||||
public async Task MigrateAccount()
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
|
||||
await _keyConnectorService.MigrateUser();
|
||||
await _keyConnectorService.MigrateUserAsync();
|
||||
await _syncService.FullSyncAsync(true);
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
@@ -165,26 +165,17 @@ namespace Bit.App.Pages
|
||||
|
||||
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig);
|
||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
|
||||
var localMasterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
|
||||
var newMasterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, email, kdfConfig);
|
||||
var masterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey, HashPurpose.ServerAuthorization);
|
||||
var localMasterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey, HashPurpose.LocalAuthorization);
|
||||
|
||||
Tuple<SymmetricCryptoKey, EncString> encKey;
|
||||
var existingEncKey = await _cryptoService.GetEncKeyAsync();
|
||||
if (existingEncKey == null)
|
||||
{
|
||||
encKey = await _cryptoService.MakeEncKeyAsync(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
encKey = await _cryptoService.RemakeEncKeyAsync(key);
|
||||
}
|
||||
var (newUserKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(newMasterKey);
|
||||
|
||||
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1);
|
||||
var (newPublicKey, newProtectedPrivateKey) = await _cryptoService.MakeKeyPairAsync(newUserKey);
|
||||
var request = new SetPasswordRequest
|
||||
{
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
Key = encKey.Item2.EncryptedString,
|
||||
Key = newProtectedUserKey.EncryptedString,
|
||||
MasterPasswordHint = Hint,
|
||||
Kdf = kdfConfig.Type.GetValueOrDefault(KdfType.PBKDF2_SHA256),
|
||||
KdfIterations = kdfConfig.Iterations.GetValueOrDefault(Constants.Pbkdf2Iterations),
|
||||
@@ -193,8 +184,8 @@ namespace Bit.App.Pages
|
||||
OrgIdentifier = OrgIdentifier,
|
||||
Keys = new KeysRequest
|
||||
{
|
||||
PublicKey = keys.Item1,
|
||||
EncryptedPrivateKey = keys.Item2.EncryptedString
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newProtectedPrivateKey.EncryptedString
|
||||
}
|
||||
};
|
||||
|
||||
@@ -204,19 +195,20 @@ namespace Bit.App.Pages
|
||||
// Set Password and relevant information
|
||||
await _apiService.SetPasswordAsync(request);
|
||||
await _stateService.SetKdfConfigurationAsync(kdfConfig);
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
await _cryptoService.SetKeyHashAsync(localMasterPasswordHash);
|
||||
await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString);
|
||||
await _cryptoService.SetEncPrivateKeyAsync(keys.Item2.EncryptedString);
|
||||
await _cryptoService.SetUserKeyAsync(newUserKey);
|
||||
await _cryptoService.SetMasterKeyAsync(newMasterKey);
|
||||
await _cryptoService.SetMasterKeyHashAsync(localMasterPasswordHash);
|
||||
await _cryptoService.SetMasterKeyEncryptedUserKeyAsync(newProtectedUserKey.EncryptedString);
|
||||
await _cryptoService.SetUserPrivateKeyAsync(newProtectedPrivateKey.EncryptedString);
|
||||
|
||||
if (ResetPasswordAutoEnroll)
|
||||
{
|
||||
// Grab Organization Keys
|
||||
var response = await _apiService.GetOrganizationKeysAsync(OrgId);
|
||||
var publicKey = CoreHelpers.Base64UrlDecode(response.PublicKey);
|
||||
// Grab user's Encryption Key and encrypt with Org Public Key
|
||||
var userEncKey = await _cryptoService.GetEncKeyAsync();
|
||||
var encryptedKey = await _cryptoService.RsaEncryptAsync(userEncKey.Key, publicKey);
|
||||
// Grab User Key and encrypt with Org Public Key
|
||||
var userKey = await _cryptoService.GetUserKeyAsync();
|
||||
var encryptedKey = await _cryptoService.RsaEncryptAsync(userKey.Key, publicKey);
|
||||
// Request
|
||||
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using Bit.App.Controls;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -24,19 +25,23 @@ namespace Bit.App.Pages
|
||||
{
|
||||
InitializeComponent();
|
||||
SetActivityIndicator();
|
||||
_authingWithSso = authingWithSso ?? false;
|
||||
_appOptions = appOptions;
|
||||
_orgIdentifier = orgIdentifier;
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_vm = BindingContext as TwoFactorPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.AuthingWithSso = authingWithSso ?? false;
|
||||
_vm.StartSetPasswordAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
_vm.TwoFactorAuthSuccessAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync());
|
||||
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessToMainAsync());
|
||||
_vm.LockAction = () =>
|
||||
Device.BeginInvokeOnMainThread(TwoFactorAuthSuccessWithSSOLocked);
|
||||
_vm.UpdateTempPasswordAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
_vm.StartDeviceApprovalOptionsAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
|
||||
_vm.CloseAction = async () => await Navigation.PopModalAsync();
|
||||
DuoWebView = _duoWebView;
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
@@ -180,13 +185,18 @@ namespace Bit.App.Pages
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task TwoFactorAuthSuccessAsync()
|
||||
private async Task StartDeviceApprovalOptionsAsync()
|
||||
{
|
||||
if (_authingWithSso)
|
||||
var page = new LoginApproveDevicePage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private void TwoFactorAuthSuccessWithSSOLocked()
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
||||
}
|
||||
else
|
||||
|
||||
private async Task TwoFactorAuthSuccessToMainAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
@@ -195,7 +205,6 @@ namespace Bit.App.Pages
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
|
||||
private void Token_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Newtonsoft.Json;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
@@ -32,8 +33,9 @@ namespace Bit.App.Pages
|
||||
private readonly IStateService _stateService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IAppIdService _appIdService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private TwoFactorProviderType? _selectedProviderType;
|
||||
private string _totpInstruction;
|
||||
private string _webVaultUrl = "https://vault.bitwarden.com";
|
||||
@@ -54,7 +56,9 @@ namespace Bit.App.Pages
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
|
||||
PageTitle = AppResources.TwoStepLogin;
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
@@ -69,6 +73,8 @@ namespace Bit.App.Pages
|
||||
|
||||
public bool Remember { get; set; }
|
||||
|
||||
public bool AuthingWithSso { get; set; }
|
||||
|
||||
public string Token { get; set; }
|
||||
|
||||
public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo ||
|
||||
@@ -118,6 +124,8 @@ namespace Bit.App.Pages
|
||||
public Command SubmitCommand { get; }
|
||||
public ICommand MoreCommand { get; }
|
||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||
public Action LockAction { get; set; }
|
||||
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
@@ -315,6 +323,7 @@ namespace Bit.App.Pages
|
||||
|
||||
var task = Task.Run(() => _syncService.FullSyncAsync(true));
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
|
||||
|
||||
@@ -326,9 +335,38 @@ namespace Bit.App.Pages
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
}
|
||||
else if (decryptOptions?.TrustedDeviceOption != null)
|
||||
{
|
||||
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||
if (!decryptOptions.HasMasterPassword &&
|
||||
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
}
|
||||
else if (result.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
}
|
||||
else if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
|
||||
{
|
||||
if (decryptOptions.TrustedDeviceOption.EncryptedPrivateKey == null && decryptOptions.TrustedDeviceOption.EncryptedUserKey == null)
|
||||
{
|
||||
await _deviceTrustCryptoService.RemoveTrustedDeviceAsync();
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
TwoFactorAuthSuccessAction?.Invoke();
|
||||
await TwoFactorAuthSuccessAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await TwoFactorAuthSuccessAsync();
|
||||
}
|
||||
}
|
||||
catch (ApiException e)
|
||||
@@ -422,5 +460,17 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TwoFactorAuthSuccessAsync()
|
||||
{
|
||||
if (AuthingWithSso && await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
LockAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
TwoFactorAuthSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,12 +93,12 @@ namespace Bit.App.Pages
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
|
||||
// Create new key and hash new password
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdfConfig);
|
||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
// Create new master key and hash new password
|
||||
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, email, kdfConfig);
|
||||
var masterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey);
|
||||
|
||||
// Create new encKey for the User
|
||||
var newEncKey = await _cryptoService.RemakeEncKeyAsync(key);
|
||||
// Encrypt user key with new master key
|
||||
var (userKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(masterKey);
|
||||
|
||||
// Initiate API action
|
||||
try
|
||||
@@ -108,10 +108,10 @@ namespace Bit.App.Pages
|
||||
switch (_reason)
|
||||
{
|
||||
case ForcePasswordResetReason.AdminForcePasswordReset:
|
||||
await UpdateTempPasswordAsync(masterPasswordHash, newEncKey.Item2.EncryptedString);
|
||||
await UpdateTempPasswordAsync(masterPasswordHash, newProtectedUserKey.EncryptedString);
|
||||
break;
|
||||
case ForcePasswordResetReason.WeakMasterPasswordOnLogin:
|
||||
await UpdatePasswordAsync(masterPasswordHash, newEncKey.Item2.EncryptedString);
|
||||
await UpdatePasswordAsync(masterPasswordHash, newProtectedUserKey.EncryptedString);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
@@ -155,7 +155,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task UpdatePasswordAsync(string newMasterPasswordHash, string newEncKey)
|
||||
{
|
||||
var currentPasswordHash = await _cryptoService.HashPasswordAsync(CurrentMasterPassword, null);
|
||||
var currentPasswordHash = await _cryptoService.HashMasterKeyAsync(CurrentMasterPassword, null);
|
||||
|
||||
var request = new PasswordRequest
|
||||
{
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
Clicked="Clear_Clicked"
|
||||
Order="Secondary"
|
||||
x:Name="_clearItem"
|
||||
x:Key="clearItem" />
|
||||
x:Key="clearItem"
|
||||
AutomationId="ClearPasswordList" />
|
||||
<ToolbarItem Icon="more_vert.png"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
@@ -43,7 +44,8 @@
|
||||
Margin="20, 0"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center"></Label>
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoPasswordsDisplayedLabel"></Label>
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding History}"
|
||||
@@ -56,7 +58,8 @@
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="10">
|
||||
ColumnSpacing="10"
|
||||
AutomationId="GeneratedPasswordRow">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -71,12 +74,14 @@
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform, text-html"
|
||||
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}" />
|
||||
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}"
|
||||
AutomationId="GeneratedPasswordValue" />
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}" />
|
||||
Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}"
|
||||
AutomationId="GeneratedPasswordDateLabel" />
|
||||
<controls:IconButton
|
||||
StyleClass="list-row-button, list-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Paste}}"
|
||||
@@ -86,7 +91,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}"
|
||||
AutomationId="CopyPasswordValueButton" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
<Label
|
||||
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="PasswordGeneratorPolicyInEffectLabel" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}"
|
||||
@@ -82,21 +83,24 @@
|
||||
x:Name="lblPassword"
|
||||
StyleClass="text-lg, text-html"
|
||||
Text="{Binding ColoredPassword, Mode=OneWay}"
|
||||
Margin="0, 20" />
|
||||
Margin="0, 20"
|
||||
AutomationId="GeneratedPasswordLabel" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}"
|
||||
AutomationId="CopyValueButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
||||
Command="{Binding RegenerateCommand}"
|
||||
Grid.Column="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n GeneratePassword}" />
|
||||
AutomationProperties.Name="{u:I18n GeneratePassword}"
|
||||
AutomationId="RegenerateValueButton" />
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsUsername}"
|
||||
StyleClass="box-row"
|
||||
@@ -107,21 +111,24 @@
|
||||
StyleClass="text-lg, text-html"
|
||||
Text="{Binding ColoredUsername, Mode=OneWay}"
|
||||
Margin="0, 20"
|
||||
HorizontalOptions="Start" />
|
||||
HorizontalOptions="Start"
|
||||
AutomationId="GeneratedPasswordLabel" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyUsername}" />
|
||||
AutomationProperties.Name="{u:I18n CopyUsername}"
|
||||
AutomationId="CopyValueButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
||||
Command="{Binding RegenerateUsernameCommand}"
|
||||
Grid.Column="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n GenerateUsername}" />
|
||||
AutomationProperties.Name="{u:I18n GenerateUsername}"
|
||||
AutomationId="RegenerateValueButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"/>
|
||||
<StackLayout StyleClass="box"
|
||||
@@ -135,7 +142,8 @@
|
||||
ItemsSource="{Binding GeneratorTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding GeneratorTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="GeneratorTypePicker" />
|
||||
</StackLayout>
|
||||
<Label Text="{u:I18n Options, Header=True}"
|
||||
StyleClass="box-header, box-header-platform"
|
||||
@@ -161,7 +169,8 @@
|
||||
ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding UsernameTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="UsernameTypePicker" />
|
||||
<Label
|
||||
StyleClass="box-footer-label"
|
||||
Text="{Binding UsernameTypeDescriptionLabel}" />
|
||||
@@ -172,7 +181,8 @@
|
||||
StyleClass="box-label" />
|
||||
<Entry x:Name="_plusAddressedEmailEntry"
|
||||
Text="{Binding PlusAddressedEmail}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="PlusAddressedEmailEntry" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n EmailType}"
|
||||
StyleClass="box-label"
|
||||
@@ -203,7 +213,8 @@
|
||||
<Entry
|
||||
x:Name="_catchAllEmailDomainNameEntry"
|
||||
Text="{Binding CatchAllEmailDomain}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="CatchAllEmailDomainEntry" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n EmailType}"
|
||||
StyleClass="box-label"
|
||||
@@ -236,26 +247,27 @@
|
||||
ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding ForwardedEmailServiceSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
<!--ANONADDY OPTIONS-->
|
||||
<Grid IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="ServiceTypePicker" />
|
||||
<Grid
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Margin="0,10,0,0"
|
||||
Text="{u:I18n APIAccessToken}"
|
||||
Text="{Binding ForwardedEmailApiSecretLabel}"
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_anonAddyApiAccessTokenEntry"
|
||||
Text="{Binding AnonAddyApiAccessToken}"
|
||||
IsPassword="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"/>
|
||||
Text="{Binding ForwardedEmailApiSecret}"
|
||||
IsPassword="{Binding ShowForwardedEmailApiSecret, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
AutomationId="ForwardedEmailApiSecretEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
||||
Text="{Binding ShowForwardedEmailApiSecret, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"/>
|
||||
Grid.Column="1"
|
||||
AutomationId="ShowForwardedEmailApiSecretButton" />
|
||||
</Grid>
|
||||
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||
Text="{u:I18n DomainNameRequiredParenthesis}"
|
||||
@@ -264,91 +276,8 @@
|
||||
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||
x:Name="_anonAddyDomainNameEntry"
|
||||
Text="{Binding AnonAddyDomainName}"
|
||||
StyleClass="box-value"/>
|
||||
<!--FIREFOX RELAY OPTIONS-->
|
||||
<Grid StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.FirefoxRelay}}"
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Text="{u:I18n APIAccessToken}"
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_firefoxRelayApiAccessTokenEntry"
|
||||
Text="{Binding FirefoxRelayApiAccessToken}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
IsPassword="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool}}"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"/>
|
||||
</Grid>
|
||||
<!--SIMPLELOGIN OPTIONS-->
|
||||
<Grid StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.SimpleLogin}}"
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Text="{u:I18n APIKeyRequiredParenthesis}"
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_simpleLoginApiKeyEntry"
|
||||
Text="{Binding SimpleLoginApiKey}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
IsPassword="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool}}"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"/>
|
||||
</Grid>
|
||||
<!--DUCKDUCKGO OPTIONS-->
|
||||
<Grid StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.DuckDuckGo}}"
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Text="{u:I18n APIKeyRequiredParenthesis}"
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_duckDuckGoApiAccessTokenEntry"
|
||||
Text="{Binding DuckDuckGoApiKey}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
IsPassword="{Binding ShowDuckDuckGoApiKey, Converter={StaticResource inverseBool}}"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowDuckDuckGoApiKey, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"/>
|
||||
</Grid>
|
||||
<!--FASTMAIL OPTIONS-->
|
||||
<Grid StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.Fastmail}}"
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Text="{u:I18n APIKeyRequiredParenthesis}"
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_fastmailApiAccessTokenEntry"
|
||||
Text="{Binding FastmailApiKey}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
IsPassword="{Binding ShowFastmailApiKey, Converter={StaticResource inverseBool}}"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowFastmailApiKey, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"/>
|
||||
</Grid>
|
||||
AutomationId="AnonAddyDomainNameEntry" />
|
||||
</StackLayout>
|
||||
<!--RANDOM WORD OPTIONS-->
|
||||
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
|
||||
@@ -359,7 +288,8 @@
|
||||
<Switch
|
||||
IsToggled="{Binding CapitalizeRandomWordUsername}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="CapitalizeRandomWordUsernameToggle" />
|
||||
</Grid>
|
||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
||||
StyleClass="box-row-separator" />
|
||||
@@ -371,7 +301,8 @@
|
||||
<Switch
|
||||
IsToggled="{Binding IncludeNumberRandomWordUsername}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="IncludeNumberRandomWordUsernameToggle" />
|
||||
</Grid>
|
||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
||||
StyleClass="box-row-separator" />
|
||||
@@ -386,7 +317,8 @@
|
||||
x:Name="_passwordTypePicker"
|
||||
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding PasswordTypeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="PasswordTypePicker" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0"
|
||||
Padding="0"
|
||||
@@ -403,12 +335,14 @@
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="NumberOfWordsLabel" />
|
||||
<controls:ExtendedStepper
|
||||
Value="{Binding NumWords}"
|
||||
Maximum="20"
|
||||
Minimum="3"
|
||||
Increment="1" />
|
||||
Increment="1"
|
||||
AutomationId="NumberOfWordsStepper" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
@@ -419,7 +353,8 @@
|
||||
Text="{Binding WordSeparator}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
StyleClass="box-value">
|
||||
StyleClass="box-value"
|
||||
AutomationId="WordSeparatorEntry">
|
||||
<Entry.Effects>
|
||||
<effects:NoEmojiKeyboardEffect />
|
||||
</Entry.Effects>
|
||||
@@ -435,7 +370,8 @@
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.Capitalize,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="CapitalizePassphraseToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -448,7 +384,8 @@
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.IncludeNumber,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="IncludeNumbersToggle" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding IsPassword}">
|
||||
@@ -462,7 +399,8 @@
|
||||
StyleClass="box-sub-label"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
WidthRequest="50" />
|
||||
WidthRequest="50"
|
||||
AutomationId="PasswordLengthLabel" />
|
||||
<controls:ExtendedSlider
|
||||
DragCompleted="LengthSlider_DragCompleted"
|
||||
Value="{Binding Length}"
|
||||
@@ -471,7 +409,8 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
Maximum="128"
|
||||
Minimum="5" />
|
||||
Minimum="5"
|
||||
AutomationId="PasswordLengthSlider" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -488,7 +427,8 @@
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n UppercaseAtoZ}"/>
|
||||
AutomationProperties.Name="{u:I18n UppercaseAtoZ}"
|
||||
AutomationId="UppercaseAtoZToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -505,7 +445,8 @@
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n LowercaseAtoZ}"/>
|
||||
AutomationProperties.Name="{u:I18n LowercaseAtoZ}"
|
||||
AutomationId="LowercaseAtoZToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -522,7 +463,8 @@
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n NumbersZeroToNine}"/>
|
||||
AutomationProperties.Name="{u:I18n NumbersZeroToNine}"
|
||||
AutomationId="NumbersZeroToNineToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -539,7 +481,8 @@
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n SpecialCharacters}"/>
|
||||
AutomationProperties.Name="{u:I18n SpecialCharacters}"
|
||||
AutomationId="SpecialCharactersToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-stepper">
|
||||
@@ -554,12 +497,14 @@
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="MinNumberValueLabel" />
|
||||
<controls:ExtendedStepper
|
||||
Value="{Binding MinNumber}"
|
||||
Maximum="5"
|
||||
Minimum="0"
|
||||
Increment="1" />
|
||||
Increment="1"
|
||||
AutomationId="MinNumberStepper" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-stepper">
|
||||
@@ -574,12 +519,14 @@
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="MinSpecialValueLabel" />
|
||||
<controls:ExtendedStepper
|
||||
Value="{Binding MinSpecial}"
|
||||
Maximum="5"
|
||||
Minimum="0"
|
||||
Increment="1" />
|
||||
Increment="1"
|
||||
AutomationId="MinSpecialStepper" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -590,7 +537,8 @@
|
||||
<Switch
|
||||
IsToggled="{Binding AvoidAmbiguousChars}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="AvoidAmbiguousCharsToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
@@ -23,7 +24,7 @@ namespace Bit.App.Pages
|
||||
private readonly IUsernameGenerationService _usernameGenerationService;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
|
||||
private PasswordGenerationOptions _options;
|
||||
private UsernameGenerationOptions _usernameOptions;
|
||||
@@ -49,11 +50,7 @@ namespace Bit.App.Pages
|
||||
private bool _doneIniting;
|
||||
private bool _showTypePicker;
|
||||
private string _emailWebsite;
|
||||
private bool _showFirefoxRelayApiAccessToken;
|
||||
private bool _showAnonAddyApiAccessToken;
|
||||
private bool _showSimpleLoginApiKey;
|
||||
private bool _showDuckDuckGoApiKey;
|
||||
private bool _showFastmailApiKey;
|
||||
private bool _showForwardedEmailApiSecret;
|
||||
private bool _editMode;
|
||||
|
||||
public GeneratorPageViewModel()
|
||||
@@ -96,7 +93,7 @@ namespace Bit.App.Pages
|
||||
UsernameTypePromptHelpCommand = new Command(UsernameTypePromptHelp);
|
||||
RegenerateCommand = new AsyncCommand(RegenerateAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
|
||||
RegenerateUsernameCommand = new AsyncCommand(RegenerateUsernameAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
|
||||
ToggleForwardedEmailHiddenValueCommand = new AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
|
||||
ToggleForwardedEmailHiddenValueCommand = new Command(() => ShowForwardedEmailApiSecret = !ShowForwardedEmailApiSecret);
|
||||
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
|
||||
CloseCommand = new AsyncCommand(CloseAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
|
||||
}
|
||||
@@ -415,7 +412,6 @@ namespace Bit.App.Pages
|
||||
|
||||
public string UsernameTypeDescriptionLabel => GetUsernameTypeLabelDescription(UsernameTypeSelected);
|
||||
|
||||
|
||||
public ForwardedEmailServiceType ForwardedEmailServiceSelected
|
||||
{
|
||||
get => _usernameOptions.ServiceType;
|
||||
@@ -425,7 +421,11 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_usernameOptions.ServiceType = value;
|
||||
Username = Constants.DefaultUsernameGenerated;
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected), new string[]
|
||||
{
|
||||
nameof(ForwardedEmailApiSecret),
|
||||
nameof(ForwardedEmailApiSecretLabel)
|
||||
});
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
@@ -445,27 +445,104 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public string AnonAddyApiAccessToken
|
||||
public string ForwardedEmailApiSecret
|
||||
{
|
||||
get => _usernameOptions.AnonAddyApiAccessToken;
|
||||
get
|
||||
{
|
||||
switch (ForwardedEmailServiceSelected)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
return _usernameOptions.AnonAddyApiAccessToken;
|
||||
case ForwardedEmailServiceType.DuckDuckGo:
|
||||
return _usernameOptions.DuckDuckGoApiKey;
|
||||
case ForwardedEmailServiceType.Fastmail:
|
||||
return _usernameOptions.FastMailApiKey;
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
return _usernameOptions.FirefoxRelayApiAccessToken;
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
return _usernameOptions.SimpleLoginApiKey;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
bool changed = false;
|
||||
switch (ForwardedEmailServiceSelected)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
if (_usernameOptions.AnonAddyApiAccessToken != value)
|
||||
{
|
||||
_usernameOptions.AnonAddyApiAccessToken = value;
|
||||
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case ForwardedEmailServiceType.DuckDuckGo:
|
||||
if (_usernameOptions.DuckDuckGoApiKey != value)
|
||||
{
|
||||
_usernameOptions.DuckDuckGoApiKey = value;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case ForwardedEmailServiceType.Fastmail:
|
||||
if (_usernameOptions.FastMailApiKey != value)
|
||||
{
|
||||
_usernameOptions.FastMailApiKey = value;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
if (_usernameOptions.FirefoxRelayApiAccessToken != value)
|
||||
{
|
||||
_usernameOptions.FirefoxRelayApiAccessToken = value;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
if (_usernameOptions.SimpleLoginApiKey != value)
|
||||
{
|
||||
_usernameOptions.SimpleLoginApiKey = value;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailApiSecret));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowAnonAddyApiAccessToken
|
||||
public string ForwardedEmailApiSecretLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showAnonAddyApiAccessToken;
|
||||
switch (ForwardedEmailServiceSelected)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
return AppResources.APIAccessToken;
|
||||
case ForwardedEmailServiceType.DuckDuckGo:
|
||||
case ForwardedEmailServiceType.Fastmail:
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
return AppResources.APIKeyRequiredParenthesis;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
set => SetProperty(ref _showAnonAddyApiAccessToken, value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowForwardedEmailApiSecret
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showForwardedEmailApiSecret;
|
||||
}
|
||||
set => SetProperty(ref _showForwardedEmailApiSecret, value);
|
||||
}
|
||||
|
||||
public string AnonAddyDomainName
|
||||
@@ -482,99 +559,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public string FirefoxRelayApiAccessToken
|
||||
{
|
||||
get => _usernameOptions.FirefoxRelayApiAccessToken;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.FirefoxRelayApiAccessToken != value)
|
||||
{
|
||||
_usernameOptions.FirefoxRelayApiAccessToken = value;
|
||||
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowFirefoxRelayApiAccessToken
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showFirefoxRelayApiAccessToken;
|
||||
}
|
||||
set => SetProperty(ref _showFirefoxRelayApiAccessToken, value);
|
||||
}
|
||||
|
||||
public string SimpleLoginApiKey
|
||||
{
|
||||
get => _usernameOptions.SimpleLoginApiKey;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.SimpleLoginApiKey != value)
|
||||
{
|
||||
_usernameOptions.SimpleLoginApiKey = value;
|
||||
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSimpleLoginApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showSimpleLoginApiKey;
|
||||
}
|
||||
set => SetProperty(ref _showSimpleLoginApiKey, value);
|
||||
}
|
||||
|
||||
public string DuckDuckGoApiKey
|
||||
{
|
||||
get => _usernameOptions.DuckDuckGoApiKey;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.DuckDuckGoApiKey != value)
|
||||
{
|
||||
_usernameOptions.DuckDuckGoApiKey = value;
|
||||
TriggerPropertyChanged(nameof(DuckDuckGoApiKey));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowDuckDuckGoApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showDuckDuckGoApiKey;
|
||||
}
|
||||
set => SetProperty(ref _showDuckDuckGoApiKey, value);
|
||||
}
|
||||
|
||||
|
||||
public string FastmailApiKey
|
||||
{
|
||||
get => _usernameOptions.FastMailApiKey;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.FastMailApiKey != value)
|
||||
{
|
||||
_usernameOptions.FastMailApiKey = value;
|
||||
TriggerPropertyChanged(nameof(FastmailApiKey));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowFastmailApiKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showFastmailApiKey;
|
||||
}
|
||||
set => SetProperty(ref _showFastmailApiKey, value);
|
||||
}
|
||||
|
||||
public bool CapitalizeRandomWordUsername
|
||||
{
|
||||
get => _usernameOptions.CapitalizeRandomWordUsername;
|
||||
@@ -807,12 +791,9 @@ namespace Bit.App.Pages
|
||||
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
|
||||
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
|
||||
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
|
||||
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
|
||||
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailApiSecret));
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailApiSecretLabel));
|
||||
TriggerPropertyChanged(nameof(AnonAddyDomainName));
|
||||
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
|
||||
TriggerPropertyChanged(nameof(DuckDuckGoApiKey));
|
||||
TriggerPropertyChanged(nameof(FastmailApiKey));
|
||||
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
|
||||
TriggerPropertyChanged(nameof(UsernameTypeSelected));
|
||||
@@ -845,17 +826,25 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
|
||||
string message = AppResources.GenericErrorMessage;
|
||||
|
||||
if (IsUsername && UsernameTypeSelected == UsernameType.ForwardedEmailAlias)
|
||||
{
|
||||
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(
|
||||
AppResources.AnErrorHasOccurred, string.Format(AppResources.UnknownXErrorMessage, ForwardedEmailServiceSelected), AppResources.Ok));
|
||||
if (ex is ForwardedEmailInvalidSecretException)
|
||||
{
|
||||
message = ForwardedEmailServiceSelected == ForwardedEmailServiceType.AnonAddy || ForwardedEmailServiceSelected == ForwardedEmailServiceType.FirefoxRelay
|
||||
? AppResources.InvalidAPIToken
|
||||
: AppResources.InvalidAPIKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok));
|
||||
message = string.Format(AppResources.UnknownXErrorMessage, ForwardedEmailServiceSelected);
|
||||
}
|
||||
}
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, message, AppResources.Ok));
|
||||
}
|
||||
|
||||
private string GetUsernameTypeLabelDescription(UsernameType value)
|
||||
{
|
||||
switch (value)
|
||||
@@ -870,27 +859,5 @@ namespace Bit.App.Pages
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleForwardedEmailHiddenValueAsync()
|
||||
{
|
||||
switch (ForwardedEmailServiceSelected)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
ShowAnonAddyApiAccessToken = !ShowAnonAddyApiAccessToken;
|
||||
break;
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
ShowFirefoxRelayApiAccessToken = !ShowFirefoxRelayApiAccessToken;
|
||||
break;
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
ShowSimpleLoginApiKey = !ShowSimpleLoginApiKey;
|
||||
break;
|
||||
case ForwardedEmailServiceType.DuckDuckGo:
|
||||
ShowDuckDuckGoApiKey = !ShowDuckDuckGoApiKey;
|
||||
break;
|
||||
case ForwardedEmailServiceType.Fastmail:
|
||||
ShowFastmailApiKey = !ShowFastmailApiKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
<Label
|
||||
Text="{u:I18n SendDisabledWarning}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="SendDisabledWarningMessageLabel" />
|
||||
</Frame>
|
||||
<Frame
|
||||
IsVisible="{Binding SendOptionsPolicyInEffect}"
|
||||
@@ -83,7 +84,8 @@
|
||||
<Label
|
||||
Text="{u:I18n SendOptionsPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="SendOptionsPolicyInEffectLabel" />
|
||||
</Frame>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
@@ -93,7 +95,8 @@
|
||||
x:Name="_nameEntry"
|
||||
Text="{Binding Send.Name}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="SendNameEntry" />
|
||||
<Label
|
||||
Text="{u:I18n NameInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
@@ -123,6 +126,7 @@
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n File}"
|
||||
AutomationProperties.HelpText="{Binding FileTypeAccessibilityLabel}"
|
||||
AutomationId="SendFileButton"
|
||||
Grid.Column="0">
|
||||
</Button>
|
||||
<Button
|
||||
@@ -135,6 +139,7 @@
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Text}"
|
||||
AutomationProperties.HelpText="{Binding TextTypeAccessibilityLabel}"
|
||||
AutomationId="SendTextButton"
|
||||
Grid.Column="1">
|
||||
</Button>
|
||||
</Grid>
|
||||
@@ -152,12 +157,14 @@
|
||||
Text="{Binding Send.File.FileName, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationId="SendFileNameLabel" />
|
||||
<Label
|
||||
Text="{Binding Send.File.SizeName, Mode=OneWay}"
|
||||
StyleClass="box-sub-label"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="SendFileSizeLabel" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
|
||||
@@ -168,20 +175,23 @@
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="SendNoFileChosenLabel" />
|
||||
<Label
|
||||
IsVisible="{Binding FileName, Converter={StaticResource notNull}}"
|
||||
Text="{Binding FileName}"
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="SendCurrentFileNameLabel" />
|
||||
<Button
|
||||
Text="{u:I18n ChooseFile}"
|
||||
IsVisible="{Binding IsAddFromShare, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-button-row"
|
||||
Clicked="ChooseFile_Clicked" />
|
||||
Clicked="ChooseFile_Clicked"
|
||||
AutomationId="SendChooseFileButton" />
|
||||
<Label
|
||||
Margin="0, 5, 0, 0"
|
||||
Text="{u:I18n MaxFileSize}"
|
||||
@@ -208,6 +218,7 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Margin="{Binding EditorMargins}"
|
||||
AutomationId="SendTextContentEntry"
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
|
||||
<Editor.Behaviors>
|
||||
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
|
||||
@@ -235,7 +246,8 @@
|
||||
IsToggled="{Binding Send.Text.Hidden}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendHideTextByDefaultToggle" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
@@ -249,7 +261,8 @@
|
||||
IsToggled="{Binding ShareOnSave}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendShareSendAfterSaveToggle" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
@@ -263,21 +276,24 @@
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
Margin="0"
|
||||
AutomationProperties.IsInAccessibleTree="False"/>
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="SendShowHideOptionsButton" />
|
||||
<controls:IconButton
|
||||
x:Name="_btnOptionsUp"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ChevronUp}}"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
IsVisible="{Binding ShowOptions}"
|
||||
AutomationProperties.IsInAccessibleTree="False"/>
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="SendOptionsDisplayed" />
|
||||
<controls:IconButton
|
||||
x:Name="_btnOptionsDown"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.AngleDown}}"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
IsVisible="{Binding ShowOptions, Converter={StaticResource inverseBool}}"
|
||||
AutomationProperties.IsInAccessibleTree="False"/>
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="SendOptionsHidden" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding ShowOptions}">
|
||||
<StackLayout
|
||||
@@ -294,7 +310,8 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n DeletionTime}" />
|
||||
AutomationProperties.Name="{u:I18n DeletionTime}"
|
||||
AutomationId="SendDeletionOptionsPicker" />
|
||||
<Grid
|
||||
IsVisible="{Binding ShowDeletionCustomPickers}"
|
||||
Margin="0,5,0,0">
|
||||
@@ -308,14 +325,16 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n DeletionDate}"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="SendCustomDeletionDatePicker" />
|
||||
<controls:ExtendedTimePicker
|
||||
NullableTime="{Binding DeletionDateTimeViewModel.Time, Mode=TwoWay}"
|
||||
Format="t"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n DeletionTime}"
|
||||
Grid.Column="1" />
|
||||
Grid.Column="1"
|
||||
AutomationId="SendCustomDeletionTimePicker" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n DeletionDateInfo}"
|
||||
@@ -334,7 +353,8 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ExpirationTime}" />
|
||||
AutomationProperties.Name="{u:I18n ExpirationTime}"
|
||||
AutomationId="SendExpirationOptionsPicker" />
|
||||
<Grid
|
||||
IsVisible="{Binding ShowExpirationCustomPickers}"
|
||||
Margin="0,5,0,0">
|
||||
@@ -349,7 +369,8 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ExpirationDate}"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="SendCustomExpirationDatePicker" />
|
||||
<controls:ExtendedTimePicker
|
||||
NullableTime="{Binding ExpirationDateTimeViewModel.Time, Mode=TwoWay}"
|
||||
PlaceHolder="--:-- --"
|
||||
@@ -357,7 +378,8 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ExpirationTime}"
|
||||
Grid.Column="1" />
|
||||
Grid.Column="1"
|
||||
AutomationId="SendCustomExpirationTimePicker" />
|
||||
</Grid>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
@@ -374,7 +396,8 @@
|
||||
FontSize="{Binding SegmentedButtonFontSize}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-row-button"
|
||||
Clicked="ClearExpirationDate_Clicked" />
|
||||
Clicked="ClearExpirationDate_Clicked"
|
||||
AutomationId="SendClearExpirationDateButton" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
@@ -393,13 +416,15 @@
|
||||
Keyboard="Numeric"
|
||||
MaxLength="9"
|
||||
TextChanged="OnMaxAccessCountTextChanged"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="SendMaxAccessCountEntry" />
|
||||
<controls:ExtendedStepper
|
||||
x:Name="_maxAccessCountStepper"
|
||||
Value="{Binding MaxAccessCount}"
|
||||
Maximum="999999999"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
Margin="10,0,0,0" />
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendMaxAccessCountStepper" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MaximumAccessCountInfo}"
|
||||
@@ -419,7 +444,8 @@
|
||||
<Label
|
||||
Text="{Binding Send.AccessCount, Mode=OneWay}"
|
||||
StyleClass="box-label"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="SendCurrentAccessCountLabel" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
@@ -436,7 +462,8 @@
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="SendNewPasswordEntry" />
|
||||
<controls:IconButton
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
@@ -445,7 +472,8 @@
|
||||
Margin="10,0,0,0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="SendShowHidePasswordButton" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n PasswordInfo}"
|
||||
@@ -464,7 +492,8 @@
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Margin="{Binding EditorMargins}"
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false"
|
||||
AutomationId="SendNotesEntry">
|
||||
<Editor.Behaviors>
|
||||
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
|
||||
</Editor.Behaviors>
|
||||
@@ -492,7 +521,8 @@
|
||||
IsToggled="{Binding Send.HideEmail}"
|
||||
IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendHideEmailSwitch" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
@@ -506,7 +536,8 @@
|
||||
IsToggled="{Binding Send.Disabled}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendDeactivateSwitch" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
Priority="-2"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Account}" />
|
||||
AutomationProperties.Name="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/>
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
@@ -44,13 +44,15 @@
|
||||
<controls:SendViewCell
|
||||
Send="{Binding Send}"
|
||||
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}"
|
||||
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}" />
|
||||
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}"
|
||||
AutomationId="SendCell" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="sendGroupTemplate"
|
||||
x:DataType="pages:SendGroupingsPageListItem">
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform">
|
||||
StyleClass="list-row, list-row-platform"
|
||||
AutomationId="{Binding AutomationId}">
|
||||
<controls:IconLabel Text="{Binding Icon, Mode=OneWay}"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
@@ -64,12 +66,14 @@
|
||||
LineBreakMode="TailTruncation"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title" />
|
||||
StyleClass="list-title"
|
||||
AutomationId="SendFilterNameLabel" />
|
||||
<Label Text="{Binding ItemCount, Mode=OneWay}"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
StyleClass="list-sub" />
|
||||
StyleClass="list-sub"
|
||||
AutomationId="SendFilterCountLabel" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
|
||||
|
||||
@@ -66,5 +66,27 @@ namespace Bit.App.Pages
|
||||
return _icon;
|
||||
}
|
||||
}
|
||||
|
||||
public string AutomationId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_name != null)
|
||||
{
|
||||
return "SendItem";
|
||||
}
|
||||
if (Type != null)
|
||||
{
|
||||
switch (Type.Value)
|
||||
{
|
||||
case SendType.Text:
|
||||
return "SendTextFilter";
|
||||
case SendType.File:
|
||||
return "SendFileFilter";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@
|
||||
Margin="20, 0"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoSendDisplayedLabel" />
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowList}"
|
||||
ItemsSource="{Binding Sends}"
|
||||
@@ -67,13 +68,15 @@
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Sends Page">
|
||||
ExtraDataForLogging="Sends Page"
|
||||
AutomationId="SendCellList">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:SendView">
|
||||
<controls:SendViewCell
|
||||
Send="{Binding .}"
|
||||
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}"
|
||||
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}" />
|
||||
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}"
|
||||
AutomationId="SendCell" />
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
|
||||
86
src/App/Pages/Settings/BlockAutofillUrisPage.xaml
Normal file
86
src/App/Pages/Settings/BlockAutofillUrisPage.xaml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.BlockAutofillUrisPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||
x:DataType="pages:BlockAutofillUrisPageViewModel"
|
||||
NavigationPage.HasBackButton="False"
|
||||
Title="{u:I18n BlockAutoFill}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:BlockAutofillUrisPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
<StackLayout Orientation="Vertical">
|
||||
<Image
|
||||
x:Name="_emptyUrisPlaceholder"
|
||||
HorizontalOptions="Center"
|
||||
WidthRequest="120"
|
||||
HeightRequest="120"
|
||||
Margin="0,100,0,0"
|
||||
IsVisible="{Binding ShowList, Converter={StaticResource inverseBool}}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ThereAreNoBlockedURIs}" />
|
||||
<controls:CustomLabel
|
||||
StyleClass="box-label-regular"
|
||||
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
|
||||
FontWeight="500"
|
||||
HorizontalTextAlignment="Center"
|
||||
Margin="14,10,14,0"/>
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding BlockedUris}"
|
||||
IsVisible="{Binding ShowList}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
Margin="0,5,0,0"
|
||||
SelectionMode="None"
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Blocked Autofill Uris"
|
||||
AutomationId="BlockedUrisCellList">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="pages:BlockAutofillUriItemViewModel">
|
||||
<StackLayout
|
||||
Orientation="Vertical"
|
||||
AutomationId="BlockedUriCell">
|
||||
<StackLayout
|
||||
Orientation="Horizontal">
|
||||
<controls:CustomLabel
|
||||
VerticalOptions="Center"
|
||||
StyleClass="box-label-regular"
|
||||
Text="{Binding Uri}"
|
||||
MaxLines="2"
|
||||
LineBreakMode="TailTruncation"
|
||||
FontWeight="500"
|
||||
Margin="15,0,0,0"
|
||||
HorizontalOptions="StartAndExpand"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button-muted, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.PencilSquare}}"
|
||||
Command="{Binding EditUriCommand}"
|
||||
Margin="5,0,15,0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n EditURI}"
|
||||
AutomationId="EditUriButton" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
<Button
|
||||
Text="{u:I18n NewBlockedURI}"
|
||||
Command="{Binding AddUriCommand}"
|
||||
VerticalOptions="End"
|
||||
HeightRequest="40"
|
||||
Opacity="0.8"
|
||||
Margin="14,5,14,10"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n NewBlockedURI}"
|
||||
AutomationId="NewBlockedUriButton" />
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
||||
44
src/App/Pages/Settings/BlockAutofillUrisPage.xaml.cs
Normal file
44
src/App/Pages/Settings/BlockAutofillUrisPage.xaml.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Styles;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class BlockAutofillUrisPage : BaseContentPage, IThemeDirtablePage
|
||||
{
|
||||
private readonly BlockAutofillUrisPageViewModel _vm;
|
||||
|
||||
public BlockAutofillUrisPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_vm = BindingContext as BlockAutofillUrisPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
_vm.InitAsync().FireAndForget(_ => Navigation.PopAsync());
|
||||
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
public override async Task UpdateOnThemeChanged()
|
||||
{
|
||||
await base.UpdateOnThemeChanged();
|
||||
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
private void UpdatePlaceholder()
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
_emptyUrisPlaceholder.Source = ImageSource.FromFile(ThemeManager.UsingLightTheme ? "empty_uris_placeholder" : "empty_uris_placeholder_dark"));
|
||||
}
|
||||
}
|
||||
}
|
||||
186
src/App/Pages/Settings/BlockAutofillUrisPageViewModel.cs
Normal file
186
src/App/Pages/Settings/BlockAutofillUrisPageViewModel.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class BlockAutofillUrisPageViewModel : BaseViewModel
|
||||
{
|
||||
private const char URI_SEPARARTOR = ',';
|
||||
private const string URI_FORMAT = "https://domain.com";
|
||||
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
|
||||
public BlockAutofillUrisPageViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
|
||||
AddUriCommand = new AsyncCommand(AddUriAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
EditUriCommand = new AsyncCommand<BlockAutofillUriItemViewModel>(EditUriAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public ObservableRangeCollection<BlockAutofillUriItemViewModel> BlockedUris { get; set; } = new ObservableRangeCollection<BlockAutofillUriItemViewModel>();
|
||||
|
||||
public bool ShowList => BlockedUris.Any();
|
||||
|
||||
public ICommand AddUriCommand { get; }
|
||||
|
||||
public ICommand EditUriCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
if (blockedUrisList?.Any() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
BlockedUris.AddRange(blockedUrisList.OrderBy(uri => uri).Select(u => new BlockAutofillUriItemViewModel(u, EditUriCommand)).ToList());
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
}
|
||||
|
||||
private async Task AddUriAsync()
|
||||
{
|
||||
var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
|
||||
{
|
||||
Title = AppResources.NewUri,
|
||||
Subtitle = AppResources.EnterURI,
|
||||
ValueSubInfo = string.Format(AppResources.FormatXSeparateMultipleURIsWithAComma, URI_FORMAT),
|
||||
OkButtonText = AppResources.Save,
|
||||
ValidateText = text => ValidateUris(text, true)
|
||||
});
|
||||
if (response?.Text is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
foreach (var uri in response.Value.Text.Split(URI_SEPARARTOR).Where(s => !string.IsNullOrEmpty(s)))
|
||||
{
|
||||
var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
|
||||
BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
|
||||
}
|
||||
|
||||
BlockedUris = new ObservableRangeCollection<BlockAutofillUriItemViewModel>(BlockedUris.OrderBy(b => b.Uri));
|
||||
TriggerPropertyChanged(nameof(BlockedUris));
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
await UpdateAutofillBlacklistedUrisAsync();
|
||||
_deviceActionService.Toast(AppResources.URISaved);
|
||||
}
|
||||
|
||||
private async Task EditUriAsync(BlockAutofillUriItemViewModel uriItemViewModel)
|
||||
{
|
||||
var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
|
||||
{
|
||||
Title = AppResources.EditURI,
|
||||
Subtitle = AppResources.EnterURI,
|
||||
Text = uriItemViewModel.Uri,
|
||||
ValueSubInfo = string.Format(AppResources.FormatX, URI_FORMAT),
|
||||
OkButtonText = AppResources.Save,
|
||||
ThirdButtonText = AppResources.Remove,
|
||||
ValidateText = text => ValidateUris(text, false)
|
||||
});
|
||||
if (response is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Value.ExecuteThirdAction)
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
BlockedUris.Remove(uriItemViewModel);
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
await UpdateAutofillBlacklistedUrisAsync();
|
||||
_deviceActionService.Toast(AppResources.URIRemoved);
|
||||
return;
|
||||
}
|
||||
|
||||
var cleanedUri = response.Value.Text.Replace(Environment.NewLine, string.Empty).Trim();
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
BlockedUris.Remove(uriItemViewModel);
|
||||
BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
|
||||
BlockedUris = new ObservableRangeCollection<BlockAutofillUriItemViewModel>(BlockedUris.OrderBy(b => b.Uri));
|
||||
TriggerPropertyChanged(nameof(BlockedUris));
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
await UpdateAutofillBlacklistedUrisAsync();
|
||||
_deviceActionService.Toast(AppResources.URISaved);
|
||||
}
|
||||
|
||||
private string ValidateUris(string uris, bool allowMultipleUris)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uris))
|
||||
{
|
||||
return string.Format(AppResources.FormatX, URI_FORMAT);
|
||||
}
|
||||
|
||||
if (!allowMultipleUris && uris.Contains(URI_SEPARARTOR))
|
||||
{
|
||||
return AppResources.CannotEditMultipleURIsAtOnce;
|
||||
}
|
||||
|
||||
foreach (var uri in uris.Split(URI_SEPARARTOR).Where(u => !string.IsNullOrWhiteSpace(u)))
|
||||
{
|
||||
var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
|
||||
if (!cleanedUri.StartsWith("http://") && !cleanedUri.StartsWith("https://") &&
|
||||
!cleanedUri.StartsWith(Constants.AndroidAppProtocol))
|
||||
{
|
||||
return AppResources.InvalidFormatUseHttpsHttpOrAndroidApp;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(cleanedUri, UriKind.Absolute, out var _))
|
||||
{
|
||||
return AppResources.InvalidURI;
|
||||
}
|
||||
|
||||
if (BlockedUris.Any(uriItem => uriItem.Uri == cleanedUri))
|
||||
{
|
||||
return string.Format(AppResources.TheURIXIsAlreadyBlocked, cleanedUri);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task UpdateAutofillBlacklistedUrisAsync()
|
||||
{
|
||||
await _stateService.SetAutofillBlacklistedUrisAsync(BlockedUris.Any() ? BlockedUris.Select(bu => bu.Uri).ToList() : null);
|
||||
}
|
||||
}
|
||||
|
||||
public class BlockAutofillUriItemViewModel : ExtendedViewModel
|
||||
{
|
||||
public BlockAutofillUriItemViewModel(string uri, ICommand editUriCommand)
|
||||
{
|
||||
Uri = uri;
|
||||
EditUriCommand = new Command(() => editUriCommand.Execute(this));
|
||||
}
|
||||
|
||||
public string Uri { get; }
|
||||
|
||||
public ICommand EditUriCommand { get; }
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ namespace Bit.App.Pages
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IExportService _exportService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly ILogger _logger;
|
||||
@@ -45,8 +44,7 @@ namespace Bit.App.Pages
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_exportService = ServiceContainer.Resolve<IExportService>("exportService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>("userVerificationService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
@@ -67,7 +65,7 @@ namespace Bit.App.Pages
|
||||
_initialized = true;
|
||||
FileFormatSelectedIndex = FileFormatOptions.FindIndex(k => k.Key == "json");
|
||||
DisablePrivateVaultPolicyEnabled = await _policyService.PolicyAppliesToUser(PolicyType.DisablePersonalVaultExport);
|
||||
UseOTPVerification = await _keyConnectorService.GetUsesKeyConnector();
|
||||
UseOTPVerification = !await _userVerificationService.HasMasterPasswordAsync();
|
||||
|
||||
if (UseOTPVerification)
|
||||
{
|
||||
@@ -165,9 +163,9 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
var verificationType = await _keyConnectorService.GetUsesKeyConnector()
|
||||
? VerificationType.OTP
|
||||
: VerificationType.MasterPassword;
|
||||
var verificationType = await _userVerificationService.HasMasterPasswordAsync()
|
||||
? VerificationType.MasterPassword
|
||||
: VerificationType.OTP;
|
||||
if (!await _userVerificationService.VerifyUser(Secret, verificationType))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -13,8 +13,13 @@
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}"
|
||||
Clicked="Close_Clicked"
|
||||
Order="Primary"
|
||||
Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Save}"
|
||||
Clicked="Save_Clicked"
|
||||
Order="Primary" />
|
||||
<ToolbarItem Text="{u:I18n Delete}"
|
||||
Clicked="Delete_Clicked"
|
||||
Order="Secondary"
|
||||
@@ -43,7 +48,8 @@
|
||||
StyleClass="box-value"
|
||||
x:Name="_nameEntry"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="FolderNameEntry" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
Margin="20, 0"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center"></Label>
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoFoldersLabel"></Label>
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding Folders}"
|
||||
@@ -44,10 +45,12 @@
|
||||
<DataTemplate x:DataType="views:FolderView">
|
||||
<controls:ExtendedStackLayout
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10">
|
||||
Padding="10"
|
||||
AutomationId="FolderCell">
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Name, Mode=OneWay}" />
|
||||
Text="{Binding Name, Mode=OneWay}"
|
||||
AutomationId="FolderName" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
x:Name="_themePicker"
|
||||
ItemsSource="{Binding ThemeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding ThemeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ThemeSelectorPicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
StyleClass="box-footer-label"
|
||||
@@ -44,7 +45,8 @@
|
||||
x:Name="_autoDarkThemePicker"
|
||||
ItemsSource="{Binding AutoDarkThemeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding AutoDarkThemeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="DefaultDarkThemePicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
StyleClass="box-footer-label"
|
||||
@@ -59,7 +61,8 @@
|
||||
x:Name="_uriMatchPicker"
|
||||
ItemsSource="{Binding UriMatchOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding UriMatchSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="DefaultUriMatchDetectionPicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n DefaultUriMatchDetectionDescription}"
|
||||
@@ -74,7 +77,8 @@
|
||||
x:Name="_clearClipboardPicker"
|
||||
ItemsSource="{Binding ClearClipboardOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding ClearClipboardSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ClearClipboardPicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n ClearClipboardDescription}"
|
||||
@@ -90,7 +94,8 @@
|
||||
ItemsSource="{Binding LocalesOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding SelectedLocale}"
|
||||
ItemDisplayBinding="{Binding Value}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="LanguagePicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n LanguageChangeRequiresAppRestart}"
|
||||
@@ -105,7 +110,8 @@
|
||||
<Switch
|
||||
IsToggled="{Binding AutoTotpCopy}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="CopyTotpAutomaticallyToggle" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n CopyTotpAutomaticallyDescription}"
|
||||
@@ -120,7 +126,8 @@
|
||||
<Switch
|
||||
IsToggled="{Binding Favicon}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="ShowWebsiteIconsToggle" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n ShowWebsiteIconsDescription}"
|
||||
@@ -146,22 +153,14 @@
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToBlockAutofillUrisCommand}" />
|
||||
</StackLayout.GestureRecognizers>
|
||||
<Label
|
||||
Text="{u:I18n AutofillBlockedUris}"
|
||||
StyleClass="box-label" />
|
||||
<Editor
|
||||
x:Name="_autofillBlockedUrisEditor"
|
||||
Text="{Binding AutofillBlockedUris}"
|
||||
StyleClass="box-value"
|
||||
AutoSize="TextChanges"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
Keyboard="Url"
|
||||
Unfocused="AutofillBlockedUrisEditor_Unfocused" />
|
||||
</StackLayout>
|
||||
Text="{u:I18n BlockAutoFill}"
|
||||
StyleClass="box-label-regular" />
|
||||
<Label
|
||||
Text="{u:I18n AutofillBlockedUrisDescription}"
|
||||
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
@@ -44,17 +42,6 @@ namespace Bit.App.Pages
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
|
||||
protected async override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
await _vm.UpdateAutofillBlockedUris();
|
||||
}
|
||||
|
||||
private async void AutofillBlockedUrisEditor_Unfocused(object sender, FocusEventArgs e)
|
||||
{
|
||||
await _vm.UpdateAutofillBlockedUris();
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -19,7 +20,6 @@ namespace Bit.App.Pages
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
|
||||
private bool _autofillSavePrompt;
|
||||
private string _autofillBlockedUris;
|
||||
private bool _favicon;
|
||||
private bool _autoTotpCopy;
|
||||
private int _clearClipboardSelectedIndex;
|
||||
@@ -84,6 +84,10 @@ namespace Bit.App.Pages
|
||||
new KeyValuePair<string, string>(null, AppResources.DefaultSystem)
|
||||
};
|
||||
LocalesOptions.AddRange(_i18nService.LocaleNames.ToList());
|
||||
|
||||
GoToBlockAutofillUrisCommand = new AsyncCommand(() => Page.Navigation.PushAsync(new BlockAutofillUrisPage()),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public List<KeyValuePair<int?, string>> ClearClipboardOptions { get; set; }
|
||||
@@ -192,25 +196,18 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public string AutofillBlockedUris
|
||||
{
|
||||
get => _autofillBlockedUris;
|
||||
set => SetProperty(ref _autofillBlockedUris, value);
|
||||
}
|
||||
|
||||
public bool ShowAndroidAutofillSettings
|
||||
{
|
||||
get => _showAndroidAutofillSettings;
|
||||
set => SetProperty(ref _showAndroidAutofillSettings, value);
|
||||
}
|
||||
|
||||
public ICommand GoToBlockAutofillUrisCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
|
||||
|
||||
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
AutofillBlockedUris = blockedUrisList != null ? string.Join(", ", blockedUrisList) : null;
|
||||
|
||||
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
|
||||
|
||||
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
||||
@@ -288,41 +285,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAutofillBlockedUris()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(AutofillBlockedUris))
|
||||
{
|
||||
await _stateService.SetAutofillBlacklistedUrisAsync(null);
|
||||
AutofillBlockedUris = null;
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
var csv = AutofillBlockedUris;
|
||||
var urisList = new List<string>();
|
||||
foreach (var uri in csv.Split(','))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uri))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var cleanedUri = uri.Replace(System.Environment.NewLine, string.Empty).Trim();
|
||||
if (!cleanedUri.StartsWith("http://") && !cleanedUri.StartsWith("https://") &&
|
||||
!cleanedUri.StartsWith(Constants.AndroidAppProtocol))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
urisList.Add(cleanedUri);
|
||||
}
|
||||
await _stateService.SetAutofillBlacklistedUrisAsync(urisList);
|
||||
AutofillBlockedUris = string.Join(", ", urisList);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateCurrentLocaleAsync()
|
||||
{
|
||||
if (!_inited)
|
||||
|
||||
@@ -32,19 +32,21 @@
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<Label IsVisible="{Binding UseFrame, Converter={StaticResource inverseBool}}"
|
||||
<controls:CustomLabel IsVisible="{Binding UseFrame, Converter={StaticResource inverseBool}}"
|
||||
Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="{Binding LineBreakMode}"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title"/>
|
||||
<Label Text="{Binding SubLabel, Mode=OneWay}"
|
||||
StyleClass="list-title"
|
||||
AutomationId="{Binding AutomationIdSettingName}" />
|
||||
<controls:CustomLabel Text="{Binding SubLabel, Mode=OneWay}"
|
||||
IsVisible="{Binding ShowSubLabel}"
|
||||
HorizontalOptions="End"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
TextColor="{Binding SubLabelColor}"
|
||||
StyleClass="list-sub" />
|
||||
StyleClass="list-sub"
|
||||
AutomationId="{Binding AutomationIdSettingStatus}" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
<DataTemplate
|
||||
@@ -57,7 +59,8 @@
|
||||
Padding="10"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
BorderColor="{DynamicResource PrimaryColor}"
|
||||
AutomationId="SettingActivePolicyTextLabel">
|
||||
<Label
|
||||
Text="{Binding Name, Mode=OneWay}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
@@ -75,7 +78,8 @@
|
||||
VerticalOptions="Center"
|
||||
FontSize="Small"
|
||||
TextColor="{Binding SubLabelColor}"
|
||||
StyleClass="list-sub" Margin="-5"/>
|
||||
StyleClass="list-sub" Margin="-5"
|
||||
AutomationId="SettingCustomVaultTimeoutPicker" />
|
||||
<controls:ExtendedStackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="ActivateTimePicker"/>
|
||||
</controls:ExtendedStackLayout.GestureRecognizers>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.App.Utilities.Automation;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -22,5 +24,29 @@ namespace Bit.App.Pages
|
||||
public Color SubLabelColor => SubLabelTextEnabled ?
|
||||
ThemeManager.GetResourceColor("SuccessColor") :
|
||||
ThemeManager.GetResourceColor("MutedColor");
|
||||
|
||||
public string AutomationIdSettingName
|
||||
{
|
||||
get
|
||||
{
|
||||
return AutomationIdsHelper.AddSuffixFor(
|
||||
UseFrame ? "EnabledPolicy"
|
||||
: AutomationIdsHelper.ToEnglishTitleCase(Name)
|
||||
, SuffixType.Cell);
|
||||
}
|
||||
}
|
||||
|
||||
public string AutomationIdSettingStatus
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UseFrame)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return AutomationIdsHelper.AddSuffixFor(AutomationIdsHelper.ToEnglishTitleCase(Name), SuffixType.SettingValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Bit.App.Pages
|
||||
private readonly IBiometricService _biometricService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ILocalizeService _localizeService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ILogger _loggerService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
@@ -48,6 +48,7 @@ namespace Bit.App.Pages
|
||||
private bool _reportLoggingEnabled;
|
||||
private bool _approvePasswordlessLoginRequests;
|
||||
private bool _shouldConnectToWatch;
|
||||
private bool _hasMasterPassword;
|
||||
private readonly static List<KeyValuePair<string, int?>> VaultTimeoutOptions =
|
||||
new List<KeyValuePair<string, int?>>
|
||||
{
|
||||
@@ -88,7 +89,7 @@ namespace Bit.App.Pages
|
||||
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||
@@ -100,12 +101,17 @@ namespace Bit.App.Pages
|
||||
ExecuteSettingItemCommand = new AsyncCommand<SettingsPageListItem>(item => item.ExecuteAsync(), onException: _loggerService.Exception, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
private bool IsVaultTimeoutActionLockAllowed => _hasMasterPassword || _biometric || _pin;
|
||||
|
||||
public ObservableRangeCollection<ISettingsPageListItem> GroupedItems { get; set; }
|
||||
|
||||
public IAsyncCommand<SettingsPageListItem> ExecuteSettingItemCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var decryptionOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
// set has true for backwards compatibility
|
||||
_hasMasterPassword = decryptionOptions?.HasMasterPassword ?? true;
|
||||
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
|
||||
var lastSync = await _syncService.GetLastSyncAsync();
|
||||
if (lastSync != null)
|
||||
@@ -124,8 +130,17 @@ namespace Bit.App.Pages
|
||||
_vaultTimeoutDisplayValue = _vaultTimeoutOptions.FirstOrDefault(o => o.Value == _vaultTimeout).Key;
|
||||
_vaultTimeoutDisplayValue ??= _vaultTimeoutOptions.Where(o => o.Value == CustomVaultTimeoutValue).First().Key;
|
||||
|
||||
var action = await _vaultTimeoutService.GetVaultTimeoutAction() ?? VaultTimeoutAction.Lock;
|
||||
_vaultTimeoutActionDisplayValue = _vaultTimeoutActionOptions.FirstOrDefault(o => o.Value == action).Key;
|
||||
|
||||
var pinSet = await _vaultTimeoutService.GetPinLockTypeAsync();
|
||||
_pin = pinSet != PinLockType.Disabled;
|
||||
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
|
||||
var timeoutAction = await _vaultTimeoutService.GetVaultTimeoutAction() ?? VaultTimeoutAction.Lock;
|
||||
if (!IsVaultTimeoutActionLockAllowed && timeoutAction == VaultTimeoutAction.Lock)
|
||||
{
|
||||
timeoutAction = VaultTimeoutAction.Logout;
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(_vaultTimeout, VaultTimeoutAction.Logout);
|
||||
}
|
||||
_vaultTimeoutActionDisplayValue = _vaultTimeoutActionOptions.FirstOrDefault(o => o.Value == timeoutAction).Key;
|
||||
|
||||
if (await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout))
|
||||
{
|
||||
@@ -137,10 +152,6 @@ namespace Bit.App.Pages
|
||||
(t.Value > 0 || t.Value == CustomVaultTimeoutValue) &&
|
||||
t.Value != null).ToList();
|
||||
}
|
||||
|
||||
var pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
|
||||
_pin = pinSet.Item1 || pinSet.Item2;
|
||||
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
|
||||
_screenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
|
||||
|
||||
if (_vaultTimeoutDisplayValue == null)
|
||||
@@ -148,8 +159,7 @@ namespace Bit.App.Pages
|
||||
_vaultTimeoutDisplayValue = AppResources.Custom;
|
||||
}
|
||||
|
||||
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
|
||||
!await _keyConnectorService.GetUsesKeyConnector();
|
||||
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() && await _userVerificationService.HasMasterPasswordAsync();
|
||||
_reportLoggingEnabled = await _loggerService.IsEnabled();
|
||||
_approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
|
||||
_shouldConnectToWatch = await _stateService.GetShouldConnectToWatchAsync();
|
||||
@@ -323,6 +333,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (oldTimeout != newTimeout)
|
||||
{
|
||||
await _cryptoService.RefreshKeysAsync();
|
||||
await Device.InvokeOnMainThreadAsync(BuildList);
|
||||
}
|
||||
}
|
||||
@@ -387,8 +398,11 @@ namespace Bit.App.Pages
|
||||
// do nothing if we have a policy set
|
||||
return;
|
||||
}
|
||||
var options = _vaultTimeoutActionOptions.Select(o =>
|
||||
o.Key == _vaultTimeoutActionDisplayValue ? $"✓ {o.Key}" : o.Key).ToArray();
|
||||
|
||||
var options = IsVaultTimeoutActionLockAllowed
|
||||
? _vaultTimeoutActionOptions.Select(o => CreateSelectableOption(o.Key, _vaultTimeoutActionDisplayValue == o.Key)).ToArray()
|
||||
: _vaultTimeoutActionOptions.Where(o => o.Value == VaultTimeoutAction.Logout).Select(v => ToSelectedOption(v.Key)).ToArray();
|
||||
|
||||
var selection = await Page.DisplayActionSheet(AppResources.VaultTimeoutAction,
|
||||
AppResources.Cancel, null, options);
|
||||
if (selection == null || selection == AppResources.Cancel)
|
||||
@@ -428,7 +442,7 @@ namespace Bit.App.Pages
|
||||
if (!string.IsNullOrWhiteSpace(pin))
|
||||
{
|
||||
var masterPassOnRestart = false;
|
||||
if (!await _keyConnectorService.GetUsesKeyConnector())
|
||||
if (await _userVerificationService.HasMasterPasswordAsync())
|
||||
{
|
||||
masterPassOnRestart = await _platformUtilsService.ShowDialogAsync(
|
||||
AppResources.PINRequireMasterPasswordRestart, AppResources.UnlockWithPIN,
|
||||
@@ -437,19 +451,20 @@ namespace Bit.App.Pages
|
||||
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(pin, email, kdfConfig);
|
||||
var key = await _cryptoService.GetKeyAsync();
|
||||
var pinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
|
||||
var pinKey = await _cryptoService.MakePinKeyAsync(pin, email, kdfConfig);
|
||||
var userKey = await _cryptoService.GetUserKeyAsync();
|
||||
var protectedPinKey = await _cryptoService.EncryptAsync(userKey.Key, pinKey);
|
||||
|
||||
var encPin = await _cryptoService.EncryptAsync(pin);
|
||||
await _stateService.SetProtectedPinAsync(encPin.EncryptedString);
|
||||
|
||||
if (masterPassOnRestart)
|
||||
{
|
||||
var encPin = await _cryptoService.EncryptAsync(pin);
|
||||
await _stateService.SetProtectedPinAsync(encPin.EncryptedString);
|
||||
await _stateService.SetPinProtectedKeyAsync(pinProtectedKey);
|
||||
await _stateService.SetPinKeyEncryptedUserKeyEphemeralAsync(protectedPinKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stateService.SetPinProtectedAsync(pinProtectedKey.EncryptedString);
|
||||
await _stateService.SetPinKeyEncryptedUserKeyAsync(protectedPinKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -459,8 +474,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (!_pin)
|
||||
{
|
||||
await _cryptoService.ClearPinProtectedKeyAsync();
|
||||
await _vaultTimeoutService.ClearAsync();
|
||||
await UpdateVaultTimeoutActionIfNeededAsync();
|
||||
}
|
||||
BuildList();
|
||||
}
|
||||
@@ -489,9 +504,10 @@ namespace Bit.App.Pages
|
||||
else
|
||||
{
|
||||
await _stateService.SetBiometricUnlockAsync(null);
|
||||
await UpdateVaultTimeoutActionIfNeededAsync();
|
||||
}
|
||||
await _stateService.SetBiometricLockedAsync(false);
|
||||
await _cryptoService.ToggleKeyAsync();
|
||||
await _cryptoService.RefreshKeysAsync();
|
||||
BuildList();
|
||||
}
|
||||
|
||||
@@ -835,9 +851,11 @@ namespace Bit.App.Pages
|
||||
return _vaultTimeoutOptions.FirstOrDefault(o => o.Key == key).Value;
|
||||
}
|
||||
|
||||
private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option;
|
||||
private string CreateSelectableOption(string option, bool selected) => selected ? ToSelectedOption(option) : option;
|
||||
|
||||
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == $"✓ {compareTo}";
|
||||
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == ToSelectedOption(compareTo);
|
||||
|
||||
private string ToSelectedOption(string option) => $"✓ {option}";
|
||||
|
||||
public async Task SetScreenCaptureAllowedAsync()
|
||||
{
|
||||
@@ -869,5 +887,17 @@ namespace Bit.App.Pages
|
||||
await _watchDeviceService.SetShouldConnectToWatchAsync(_shouldConnectToWatch);
|
||||
BuildList();
|
||||
}
|
||||
|
||||
private async Task UpdateVaultTimeoutActionIfNeededAsync()
|
||||
{
|
||||
if (IsVaultTimeoutActionLockAllowed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_vaultTimeoutActionDisplayValue = _vaultTimeoutActionOptions.First(o => o.Value == VaultTimeoutAction.Logout).Key;
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(_vaultTimeout, VaultTimeoutAction.Logout);
|
||||
_deviceActionService.Toast(AppResources.VaultTimeoutActionChangedToLogOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
});
|
||||
await UpdateVaultButtonTitleAsync();
|
||||
if (await _keyConnectorService.UserNeedsMigration())
|
||||
if (await _keyConnectorService.UserNeedsMigrationAsync())
|
||||
{
|
||||
_messagingService.Send("convertAccountToKeyConnector");
|
||||
}
|
||||
|
||||
@@ -33,23 +33,25 @@
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row" Padding="10, 20"
|
||||
IsVisible="{Binding HasAttachments, Converter={StaticResource inverseBool}}">
|
||||
<Label Text="{u:I18n NoAttachments}" HorizontalTextAlignment="Center" />
|
||||
<Label Text="{u:I18n NoAttachments}" HorizontalTextAlignment="Center" AutomationId="NoAttachmentsLabel" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Attachments}" IsVisible="{Binding HasAttachments}">
|
||||
<controls:RepeaterView ItemsSource="{Binding Attachments}" IsVisible="{Binding HasAttachments}" AutomationId="AttachmentsList">
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:AttachmentView">
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10">
|
||||
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10" AutomationId="AttachmentRow">
|
||||
<Label
|
||||
Text="{Binding FileName, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationId="AttachmentFileNameLabel" />
|
||||
<Label
|
||||
Text="{Binding SizeName, Mode=OneWay}"
|
||||
StyleClass="box-sub-label"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="AttachmentFileSizeLabel" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||
@@ -57,7 +59,8 @@
|
||||
CommandParameter="{Binding .}"
|
||||
VerticalOptions="Center"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Delete}" />
|
||||
AutomationProperties.Name="{u:I18n Delete}"
|
||||
AutomationId="AttachmentDeleteButton" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@@ -77,17 +80,20 @@
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoFileChosenLabel" />
|
||||
<Label
|
||||
IsVisible="{Binding FileName, Converter={StaticResource notNull}}"
|
||||
Text="{Binding FileName}"
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NewAttachmentNameLabel" />
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n ChooseFile}" StyleClass="box-button-row"
|
||||
Clicked="ChooseFile_Clicked"></Button>
|
||||
Clicked="ChooseFile_Clicked"
|
||||
AutomationId="ChooseFileButton"></Button>
|
||||
<Label
|
||||
Margin="0, 10, 0, 0"
|
||||
Text="{u:I18n MaxFileSize}"
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace Bit.App.Pages
|
||||
_cipherDomain = await _cipherService.GetAsync(CipherId);
|
||||
Cipher = await _cipherDomain.DecryptAsync();
|
||||
LoadAttachments();
|
||||
_hasUpdatedKey = await _cryptoService.HasEncKeyAsync();
|
||||
_hasUpdatedKey = await _cryptoService.HasUserKeyAsync();
|
||||
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
|
||||
_canAccessAttachments = canAccessPremium || Cipher.OrganizationId != null;
|
||||
if (!_canAccessAttachments)
|
||||
@@ -156,7 +156,7 @@ namespace Bit.App.Pages
|
||||
// Prevent Android from locking if vault timeout set to "immediate"
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
_vaultTimeoutService.DelayTimeoutMs = 60000;
|
||||
_vaultTimeoutService.DelayLockAndLogoutMs = 60000;
|
||||
}
|
||||
await _fileService.SelectFileAsync();
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _cipher, value, additionalPropertyNames: AdditionalPropertiesToRaiseOnCipherChanged);
|
||||
}
|
||||
|
||||
public string CreationDate => string.Format(AppResources.CreatedX, Cipher.CreationDate.ToShortDateString());
|
||||
|
||||
public AsyncCommand CheckPasswordCommand { get; }
|
||||
|
||||
protected async Task CheckPasswordAsync()
|
||||
|
||||
@@ -57,16 +57,16 @@
|
||||
x:Key="deleteItem" />
|
||||
|
||||
<DataTemplate x:Key="TextCustomFieldDataTemplate">
|
||||
<il:TextCustomFieldItemLayout />
|
||||
<il:TextCustomFieldItemLayout AutomationId="TextCustomFieldItem" />
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="BooleanCustomFieldDataTemplate">
|
||||
<il:BooleanCustomFieldItemLayout />
|
||||
<il:BooleanCustomFieldItemLayout AutomationId="BooleanCustomFieldItem" />
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="HiddenCustomFieldDataTemplate">
|
||||
<il:HiddenCustomFieldItemLayout />
|
||||
<il:HiddenCustomFieldItemLayout AutomationId="HiddenCustomFieldItem" />
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="LinkedCustomFieldDataTemplate">
|
||||
<il:LinkedCustomFieldItemLayout />
|
||||
<il:LinkedCustomFieldItemLayout AutomationId="LinkedCustomFieldItem" />
|
||||
</DataTemplate>
|
||||
|
||||
<dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector"
|
||||
@@ -100,7 +100,8 @@
|
||||
<Label
|
||||
Text="{u:I18n PersonalOwnershipPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="PersonalOwnershipPolicyLabel"/>
|
||||
</Frame>
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
@@ -116,7 +117,8 @@
|
||||
x:Name="_typePicker"
|
||||
ItemsSource="{Binding TypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding TypeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemTypePicker" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -125,7 +127,10 @@
|
||||
<Entry
|
||||
x:Name="_nameEntry"
|
||||
Text="{Binding Cipher.Name}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Name}"
|
||||
AutomationId="ItemNameEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0">
|
||||
<Grid StyleClass="box-row, box-row-input"
|
||||
@@ -138,7 +143,10 @@
|
||||
x:Name="_loginUsernameEntry"
|
||||
Text="{Binding Cipher.Login.Username}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"/>
|
||||
Grid.Row="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Username}"
|
||||
AutomationId="LoginUsernameEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
||||
@@ -146,7 +154,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n GenerateUsername}" />
|
||||
AutomationProperties.Name="{u:I18n GenerateUsername}"
|
||||
AutomationId="GenerateUsernameButton" />
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -174,7 +183,10 @@
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsEnabled="{Binding Cipher.ViewPassword}"/>
|
||||
IsEnabled="{Binding Cipher.ViewPassword}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Password}"
|
||||
AutomationId="LoginPasswordEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
|
||||
@@ -184,7 +196,8 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CheckPassword}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
IsVisible="{Binding Cipher.ViewPassword}"
|
||||
AutomationId="CheckPasswordButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -195,7 +208,8 @@
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
IsVisible="{Binding Cipher.ViewPassword}"
|
||||
AutomationId="ViewPasswordButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
||||
@@ -205,9 +219,21 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n GeneratePassword}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
IsVisible="{Binding Cipher.ViewPassword}"
|
||||
AutomationId="RegeneratePasswordButton" />
|
||||
</Grid>
|
||||
|
||||
<Label
|
||||
Text="{u:I18n Passkey}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"
|
||||
IsVisible="{Binding ShowPasskeyInfo}"/>
|
||||
<Entry
|
||||
Text="{u:I18n AvailableForTwoStepLogin}"
|
||||
IsEnabled="False"
|
||||
StyleClass="box-value,text-muted"
|
||||
IsVisible="{Binding ShowPasskeyInfo}" />
|
||||
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -241,7 +267,8 @@
|
||||
Padding="0,15"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="SetupTotpButton" />
|
||||
</Frame>
|
||||
<controls:MonoEntry
|
||||
x:Name="_loginTotpEntry"
|
||||
@@ -254,7 +281,10 @@
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="{Binding TotpColumnSpan}" />
|
||||
Grid.ColumnSpan="{Binding TotpColumnSpan}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n AuthenticatorKey}"
|
||||
AutomationId="LoginTotpEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
@@ -262,7 +292,10 @@
|
||||
IsVisible="{Binding HasTotpValue}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyTotp}"
|
||||
AutomationId="CopyTotpValueButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Camera}}"
|
||||
@@ -272,7 +305,8 @@
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding HasTotpValue}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ScanQrTitle}" />
|
||||
AutomationProperties.Name="{u:I18n ScanQrTitle}"
|
||||
/>
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding IsCard}" Spacing="0" Padding="0">
|
||||
@@ -283,7 +317,8 @@
|
||||
<Entry
|
||||
x:Name="_cardholderNameEntry"
|
||||
Text="{Binding Cipher.Card.CardholderName}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="CardholderNameEntry" />
|
||||
</StackLayout>
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -307,7 +342,10 @@
|
||||
Grid.Column="0"
|
||||
IsPassword="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False" />
|
||||
IsTextPredictionEnabled="False"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Number}"
|
||||
AutomationId="CardNumberEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowCardNumberIcon}"
|
||||
@@ -316,7 +354,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationId="ShowCardNumberButton" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -326,7 +365,8 @@
|
||||
x:Name="_cardBrandPicker"
|
||||
ItemsSource="{Binding CardBrandOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding CardBrandSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="CardBrandPicker" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -336,7 +376,8 @@
|
||||
x:Name="_cardExpMonthPicker"
|
||||
ItemsSource="{Binding CardExpMonthOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding CardExpMonthSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="CardExpirationMonthPicker" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -346,7 +387,10 @@
|
||||
x:Name="_cardExpYearEntry"
|
||||
Text="{Binding Cipher.Card.ExpYear}"
|
||||
StyleClass="box-value"
|
||||
Keyboard="Numeric" />
|
||||
Keyboard="Numeric"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ExpirationYear}"
|
||||
AutomationId="CardExpirationYearEntry" />
|
||||
</StackLayout>
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -371,7 +415,10 @@
|
||||
Keyboard="Numeric"
|
||||
IsPassword="{Binding ShowCardCode, Converter={StaticResource inverseBool}}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False" />
|
||||
IsTextPredictionEnabled="False"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n SecurityCode}"
|
||||
AutomationId="CardSecurityCodeEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowCardCodeIcon}"
|
||||
@@ -380,7 +427,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationId="CardShowSecurityCodeButton" />
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding IsIdentity}" Spacing="0" Padding="0">
|
||||
@@ -392,7 +440,8 @@
|
||||
x:Name="_identityTitlePicker"
|
||||
ItemsSource="{Binding IdentityTitleOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding IdentityTitleSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="IdentityTitlePicker" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -401,7 +450,10 @@
|
||||
<Entry
|
||||
x:Name="_identityFirstNameEntry"
|
||||
Text="{Binding Cipher.Identity.FirstName}"
|
||||
StyleClass="box-value,capitalize-word-input"/>
|
||||
StyleClass="box-value,capitalize-word-input"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n FirstName}"
|
||||
AutomationId="IdentityFirstNameEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -410,7 +462,10 @@
|
||||
<Entry
|
||||
x:Name="_identityMiddleNameEntry"
|
||||
Text="{Binding Cipher.Identity.MiddleName}"
|
||||
StyleClass="box-value,capitalize-word-input" />
|
||||
StyleClass="box-value,capitalize-word-input"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n MiddleName}"
|
||||
AutomationId="IdentityMiddleNameEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -419,7 +474,10 @@
|
||||
<Entry
|
||||
x:Name="_identityLastNameEntry"
|
||||
Text="{Binding Cipher.Identity.LastName}"
|
||||
StyleClass="box-value,capitalize-word-input" />
|
||||
StyleClass="box-value,capitalize-word-input"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n LastName}"
|
||||
AutomationId="IdentityLastNameEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -428,7 +486,10 @@
|
||||
<Entry
|
||||
x:Name="_identityUsernameEntry"
|
||||
Text="{Binding Cipher.Identity.Username}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Username}"
|
||||
AutomationId="IdentityUsernameEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -437,7 +498,10 @@
|
||||
<Entry
|
||||
x:Name="_identityCompanyEntry"
|
||||
Text="{Binding Cipher.Identity.Company}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Company}"
|
||||
AutomationId="IdentityCompanyEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -446,7 +510,10 @@
|
||||
<Entry
|
||||
x:Name="_identitySsnEntry"
|
||||
Text="{Binding Cipher.Identity.SSN}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n SSN}"
|
||||
AutomationId="IdentitySsnEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -455,7 +522,10 @@
|
||||
<Entry
|
||||
x:Name="_identityPassportNumberEntry"
|
||||
Text="{Binding Cipher.Identity.PassportNumber}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n PassportNumber}"
|
||||
AutomationId="IdentityPassportNumberEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -464,7 +534,10 @@
|
||||
<Entry
|
||||
x:Name="_identityLicenseNumberEntry"
|
||||
Text="{Binding Cipher.Identity.LicenseNumber}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n LicenseNumber}"
|
||||
AutomationId="IdentityLicenseNumberEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -474,7 +547,10 @@
|
||||
x:Name="_identityEmailEntry"
|
||||
Keyboard="Email"
|
||||
Text="{Binding Cipher.Identity.Email}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Email}"
|
||||
AutomationId="IdentityEmailEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -484,7 +560,10 @@
|
||||
x:Name="_identityPhoneEntry"
|
||||
Text="{Binding Cipher.Identity.Phone}"
|
||||
Keyboard="Telephone"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Phone}"
|
||||
AutomationId="IdentityPhoneEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -493,7 +572,10 @@
|
||||
<Entry
|
||||
x:Name="_identityAddress1Entry"
|
||||
Text="{Binding Cipher.Identity.Address1}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Address1}"
|
||||
AutomationId="IdentityAddressOneEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -502,7 +584,10 @@
|
||||
<Entry
|
||||
x:Name="_identityAddress2Entry"
|
||||
Text="{Binding Cipher.Identity.Address2}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Address2}"
|
||||
AutomationId="IdentityAddressTwoEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -511,7 +596,10 @@
|
||||
<Entry
|
||||
x:Name="_identityAddress3Entry"
|
||||
Text="{Binding Cipher.Identity.Address3}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Address3}"
|
||||
AutomationId="IdentityAddressThreeEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -520,7 +608,10 @@
|
||||
<Entry
|
||||
x:Name="_identityCityEntry"
|
||||
Text="{Binding Cipher.Identity.City}"
|
||||
StyleClass="box-value,capitalize-sentence-input" />
|
||||
StyleClass="box-value,capitalize-sentence-input"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CityTown}"
|
||||
AutomationId="IdentityCityEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -529,7 +620,10 @@
|
||||
<Entry
|
||||
x:Name="_identityStateEntry"
|
||||
Text="{Binding Cipher.Identity.State}"
|
||||
StyleClass="box-value,capitalize-sentence-input" />
|
||||
StyleClass="box-value,capitalize-sentence-input"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n StateProvince}"
|
||||
AutomationId="IdentityStateEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -538,7 +632,10 @@
|
||||
<Entry
|
||||
x:Name="_identityPostalCodeEntry"
|
||||
Text="{Binding Cipher.Identity.PostalCode}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ZipPostalCode}"
|
||||
AutomationId="IdentityPostalCodeEntry" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
@@ -547,9 +644,44 @@
|
||||
<Entry
|
||||
x:Name="_identityCountryEntry"
|
||||
Text="{Binding Cipher.Identity.Country}"
|
||||
StyleClass="box-value,capitalize-sentence-input" />
|
||||
StyleClass="box-value,capitalize-sentence-input"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Country}"
|
||||
AutomationId="IdentityCountryEntry" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
<StackLayout IsVisible="{Binding IsFido2Key}" Spacing="0" Padding="0">
|
||||
<Label
|
||||
Text="{u:I18n Username}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"/>
|
||||
<Entry
|
||||
x:Name="_fido2KeyUsernameEntry"
|
||||
Text="{Binding Cipher.Fido2Key.UserName}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"/>
|
||||
<Label
|
||||
Text="{u:I18n Passkey}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"/>
|
||||
<Entry
|
||||
Text="{Binding CreationDate}"
|
||||
IsEnabled="False"
|
||||
StyleClass="box-value,text-muted" />
|
||||
<Label
|
||||
Text="{u:I18n Application}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"/>
|
||||
<Entry
|
||||
Text="{Binding Cipher.Fido2Key.LaunchUri}"
|
||||
IsEnabled="False"
|
||||
StyleClass="box-value,text-muted" />
|
||||
<Label
|
||||
Text="{u:I18n YouCannotEditPasskeyApplicationBecauseItWouldInvalidateThePasskey}"
|
||||
StyleClass="box-sub-label" />
|
||||
</StackLayout>
|
||||
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding IsLogin}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
@@ -559,7 +691,7 @@
|
||||
<controls:RepeaterView ItemsSource="{Binding Uris}">
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:LoginUriView">
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
<Grid StyleClass="box-row, box-row-input" AutomationId="UriListGrid" >
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -578,7 +710,10 @@
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n URI}"
|
||||
AutomationId="LoginUriEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||
@@ -588,13 +723,15 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
AutomationProperties.Name="{u:I18n Options}"
|
||||
AutomationId="LoginUriOptionsButton" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</controls:RepeaterView.ItemTemplate>
|
||||
</controls:RepeaterView>
|
||||
<Button Text="{u:I18n NewUri}" StyleClass="box-button-row"
|
||||
Clicked="NewUri_Clicked"></Button>
|
||||
Clicked="NewUri_Clicked"
|
||||
AutomationId="LoginAddNewUriButton"></Button>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
@@ -609,7 +746,8 @@
|
||||
x:Name="_folderPicker"
|
||||
ItemsSource="{Binding FolderOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding FolderSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="FolderPicker" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
@@ -619,7 +757,8 @@
|
||||
<Switch
|
||||
IsToggled="{Binding Cipher.Favorite}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="ItemFavoriteToggle" />
|
||||
</StackLayout>
|
||||
<StackLayout x:Name="_passwordPrompt" StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
@@ -631,13 +770,14 @@
|
||||
Command="{Binding PasswordPromptHelpCommand}"
|
||||
TextColor="{DynamicResource MutedColor}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.Name="{u:I18n MasterPasswordRePromptHelp}"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding PasswordPrompt}"
|
||||
Toggled="PasswordPrompt_Toggled"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="MasterPasswordRepromptToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@@ -652,7 +792,10 @@
|
||||
AutoSize="TextChanges"
|
||||
StyleClass="box-value"
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false"
|
||||
Text="{Binding Cipher.Notes}">
|
||||
Text="{Binding Cipher.Notes}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Notes}"
|
||||
AutomationId="ItemNotesEntry">
|
||||
<Editor.Behaviors>
|
||||
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
|
||||
</Editor.Behaviors>
|
||||
@@ -671,9 +814,11 @@
|
||||
<StackLayout
|
||||
Spacing="0"
|
||||
BindableLayout.ItemsSource="{Binding Fields}"
|
||||
BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}" />
|
||||
BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}"
|
||||
AutomationId="CustomFieldsList" />
|
||||
<Button Text="{u:I18n NewCustomField}" StyleClass="box-button-row"
|
||||
Clicked="NewField_Clicked"></Button>
|
||||
Clicked="NewField_Clicked"
|
||||
AutomationId="NewCustomFieldButton"></Button>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowOwnershipOptions}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
@@ -688,7 +833,8 @@
|
||||
x:Name="_ownershipPicker"
|
||||
ItemsSource="{Binding OwnershipOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding OwnershipSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemOwnershipPicker" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowCollections}">
|
||||
@@ -699,7 +845,8 @@
|
||||
<StackLayout Spacing="0" Padding="0"
|
||||
IsVisible="{Binding HasCollections, Converter={StaticResource inverseBool}}">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label Text="{u:I18n NoCollectionsToList}" />
|
||||
<Label Text="{u:I18n NoCollectionsToList}"
|
||||
AutomationId="NoCollectionsToListLabel" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@@ -709,15 +856,17 @@
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
<DataTemplate x:DataType="pages:CollectionViewModel">
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<StackLayout StyleClass="box-row, box-row-switch" AutomationId="CollectionItemCell">
|
||||
<Label
|
||||
Text="{Binding Collection.Name}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationId="CollectionItemNameLabel" />
|
||||
<Switch
|
||||
IsToggled="{Binding Checked}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
HorizontalOptions="End"
|
||||
AutomationId="CollectionItemSwitch" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@@ -727,5 +876,4 @@
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Bit.App.Pages
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAutofillHandler _autofillHandler;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
|
||||
private CipherAddEditPageViewModel _vm;
|
||||
private bool _fromAutofill;
|
||||
@@ -43,7 +43,7 @@ namespace Bit.App.Pages
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
|
||||
_appOptions = appOptions;
|
||||
_fromAutofill = fromAutofill;
|
||||
@@ -175,8 +175,8 @@ namespace Bit.App.Pages
|
||||
RequestFocus(_nameEntry);
|
||||
}
|
||||
});
|
||||
// Hide password reprompt option if using key connector
|
||||
_passwordPrompt.IsVisible = !await _keyConnectorService.GetUsesKeyConnector();
|
||||
|
||||
_passwordPrompt.IsVisible = await _userVerificationService.HasMasterPasswordAsync();
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
|
||||
@@ -88,7 +88,6 @@ namespace Bit.App.Pages
|
||||
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>();
|
||||
|
||||
|
||||
GeneratePasswordCommand = new Command(GeneratePassword);
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
||||
@@ -297,6 +296,7 @@ namespace Bit.App.Pages
|
||||
public bool IsIdentity => Cipher?.Type == CipherType.Identity;
|
||||
public bool IsCard => Cipher?.Type == CipherType.Card;
|
||||
public bool IsSecureNote => Cipher?.Type == CipherType.SecureNote;
|
||||
public bool IsFido2Key => Cipher?.Type == CipherType.Fido2Key;
|
||||
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
|
||||
public bool ShowAttachments => Cipher.HasAttachments;
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
@@ -309,6 +309,7 @@ namespace Bit.App.Pages
|
||||
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 bool ShowPasskeyInfo => Cipher?.Login?.Fido2Key != null && !CloneMode;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
@@ -367,6 +368,11 @@ namespace Bit.App.Pages
|
||||
{
|
||||
Cipher.OrganizationId = OrganizationId;
|
||||
}
|
||||
if (Cipher.Type == CipherType.Login)
|
||||
{
|
||||
// passkeys can't be cloned
|
||||
Cipher.Login.Fido2Key = null;
|
||||
}
|
||||
}
|
||||
if (appOptions?.OtpData != null && Cipher.Type == CipherType.Login)
|
||||
{
|
||||
|
||||
@@ -45,20 +45,20 @@
|
||||
x:Name="_attachmentsItem" x:Key="attachmentsItem" />
|
||||
<ToolbarItem Text="{u:I18n Delete}" Clicked="Delete_Clicked" Order="Secondary" IsDestructive="True"
|
||||
x:Name="_deleteItem" x:Key="deleteItem" />
|
||||
<ToolbarItem Text="{u:I18n Clone}" Clicked="Clone_Clicked" Order="Secondary"
|
||||
<ToolbarItem Text="{u:I18n Clone}" Command="{Binding CloneCommand}" Order="Secondary"
|
||||
x:Name="_cloneItem" x:Key="cloneItem" />
|
||||
|
||||
<DataTemplate x:Key="TextCustomFieldDataTemplate">
|
||||
<il:TextCustomFieldItemLayout />
|
||||
<il:TextCustomFieldItemLayout AutomationId="TextCustomFieldItem" />
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="BooleanCustomFieldDataTemplate">
|
||||
<il:BooleanCustomFieldItemLayout />
|
||||
<il:BooleanCustomFieldItemLayout AutomationId="BooleanCustomFieldItem" />
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="HiddenCustomFieldDataTemplate">
|
||||
<il:HiddenCustomFieldItemLayout />
|
||||
<il:HiddenCustomFieldItemLayout AutomationId="HiddenCustomFieldItem" />
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="LinkedCustomFieldDataTemplate">
|
||||
<il:LinkedCustomFieldItemLayout />
|
||||
<il:LinkedCustomFieldItemLayout AutomationId="LinkedCustomFieldItem" />
|
||||
</DataTemplate>
|
||||
|
||||
<dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector"
|
||||
@@ -69,23 +69,26 @@
|
||||
|
||||
<ScrollView x:Key="scrollView" x:Name="_scrollView">
|
||||
<StackLayout Spacing="20" x:Name="_mainLayout">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box" AutomationId="ItemInformationSection">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n ItemInformation, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<StackLayout StyleClass="box-row" AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n Name}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Name, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0">
|
||||
<Grid StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -98,12 +101,14 @@
|
||||
Text="{u:I18n Username}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Login.Username, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="ItemValue" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
@@ -113,12 +118,14 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyUsername}" />
|
||||
AutomationProperties.Name="{u:I18n CopyUsername}"
|
||||
AutomationId="CopyValueButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}" />
|
||||
<Grid StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -133,20 +140,23 @@
|
||||
Text="{u:I18n Password}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="ItemName" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding Cipher.Login.MaskedPassword, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding ShowPassword, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="ItemValue" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding ColoredPassword, Mode=OneWay}"
|
||||
StyleClass="box-value, text-html"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
LineBreakMode="CharacterWrap"
|
||||
IsVisible="{Binding ShowPassword}" />
|
||||
IsVisible="{Binding ShowPassword}"
|
||||
AutomationId="ItemValue" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
|
||||
@@ -156,7 +166,8 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CheckPassword}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
IsVisible="{Binding Cipher.ViewPassword}"
|
||||
AutomationId="CheckPasswordButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -167,7 +178,8 @@
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
IsVisible="{Binding Cipher.ViewPassword}"
|
||||
AutomationId="ShowValueButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
@@ -178,11 +190,24 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
IsVisible="{Binding Cipher.ViewPassword}"
|
||||
AutomationId="CopyValueButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}" />
|
||||
<Grid StyleClass="box-row" IsVisible="{Binding ShowTotp}">
|
||||
<Label
|
||||
Text="{u:I18n Passkey}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"
|
||||
IsVisible="{Binding Cipher.Login.Fido2Key, Converter={StaticResource notNull}}"/>
|
||||
<Entry
|
||||
Text="{u:I18n AvailableForTwoStepLogin}"
|
||||
IsEnabled="False"
|
||||
StyleClass="box-value,text-muted"
|
||||
IsVisible="{Binding Cipher.Login.Fido2Key, Converter={StaticResource notNull}}" />
|
||||
<Grid StyleClass="box-row"
|
||||
IsVisible="{Binding ShowTotp}"
|
||||
AutomationId="ItemRow">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -197,7 +222,8 @@
|
||||
Text="{u:I18n VerificationCodeTotp}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="ItemName" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding TotpCodeFormatted, Mode=OneWay}"
|
||||
IsVisible="{Binding ShowUpgradePremiumTotpText, Converter={StaticResource inverseBool}}"
|
||||
@@ -205,14 +231,16 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
VerticalTextAlignment="Start"
|
||||
VerticalOptions="Start" />
|
||||
VerticalOptions="Start"
|
||||
AutomationId="ItemValue" />
|
||||
<controls:CircularProgressbarView
|
||||
Progress="{Binding TotpProgress}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand" />
|
||||
VerticalOptions="FillAndExpand"
|
||||
AutomationId="LoginTotpProgressBar" />
|
||||
<Label
|
||||
Text="{Binding TotpSec, Mode=OneWay}"
|
||||
Style="{DynamicResource textTotp}"
|
||||
@@ -234,7 +262,8 @@
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyTotp}" />
|
||||
AutomationProperties.Name="{u:I18n CopyTotp}"
|
||||
AutomationId="CopyValueButton" />
|
||||
<Label
|
||||
Text="{u:I18n PremiumSubscriptionRequired}"
|
||||
StyleClass="box-footer-label"
|
||||
@@ -242,24 +271,29 @@
|
||||
Margin="0,5,0,2"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="ShowUpgradePremiumTotpLabel" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowTotp}" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding IsCard}" Spacing="0" Padding="0">
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n CardholderName}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Card.CardholderName, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Card.CardholderName, Converter={StaticResource stringHasValue}}" />
|
||||
<Grid StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -273,19 +307,22 @@
|
||||
Text="{u:I18n Number}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="ItemName" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding Cipher.Card.MaskedNumber, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="ItemValue" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding Cipher.Card.Number, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding ShowCardNumber}" />
|
||||
IsVisible="{Binding ShowCardNumber}"
|
||||
AutomationId="ItemValue" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowCardNumberIcon}"
|
||||
@@ -294,7 +331,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationId="ShowValueButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
@@ -304,34 +342,42 @@
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyNumber}" />
|
||||
AutomationProperties.Name="{u:I18n CopyNumber}"
|
||||
AutomationId="CopyValueButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}" />
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n Brand}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Card.Brand, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Card.Brand, Converter={StaticResource stringHasValue}}" />
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n Expiration}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Card.Expiration, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Card.Expiration, Converter={StaticResource stringHasValue}}" />
|
||||
<Grid StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -345,19 +391,22 @@
|
||||
Text="{u:I18n SecurityCode}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="ItemName" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding Cipher.Card.MaskedCode, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding ShowCardCode, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="{Binding ShowCardCode, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="ItemValue" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding Cipher.Card.Code, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding ShowCardCode}" />
|
||||
IsVisible="{Binding ShowCardCode}"
|
||||
AutomationId="ItemValue" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowCardCodeIcon}"
|
||||
@@ -366,7 +415,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationId="ShowValueButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
@@ -376,138 +426,228 @@
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopySecurityCode}" />
|
||||
AutomationProperties.Name="{u:I18n CopySecurityCode}"
|
||||
AutomationId="CopyValueButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding IsIdentity}" Spacing="0" Padding="0">
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n IdentityName}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.FullName, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Identity.FullName, Converter={StaticResource stringHasValue}}" />
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n Username}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.Username, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Identity.Username, Converter={StaticResource stringHasValue}}" />
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n Company}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.Company, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Identity.Company, Converter={StaticResource stringHasValue}}" />
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n SSN}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.SSN, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Identity.SSN, Converter={StaticResource stringHasValue}}" />
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n PassportNumber}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.PassportNumber, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Identity.PassportNumber, Converter={StaticResource stringHasValue}}" />
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n LicenseNumber}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.LicenseNumber, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Identity.LicenseNumber, Converter={StaticResource stringHasValue}}" />
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n Email}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.Email, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Identity.Email, Converter={StaticResource stringHasValue}}" />
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}">
|
||||
IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}"
|
||||
AutomationId="ItemRow" >
|
||||
<Label
|
||||
Text="{u:I18n Phone}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.Phone, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="ItemValue" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Identity.Phone, Converter={StaticResource stringHasValue}}" />
|
||||
<StackLayout StyleClass="box-row" IsVisible="{Binding ShowIdentityAddress}">
|
||||
<StackLayout StyleClass="box-row" IsVisible="{Binding ShowIdentityAddress}"
|
||||
AutomationId="ItemRow">
|
||||
<Label
|
||||
Text="{u:I18n Address}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"
|
||||
AutomationId="ItemName" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.Address1, Mode=OneWay}"
|
||||
IsVisible="{Binding Cipher.Identity.Address1, Converter={StaticResource stringHasValue}}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="IdentityAddressOneLabel" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.Address2, Mode=OneWay}"
|
||||
IsVisible="{Binding Cipher.Identity.Address2, Converter={StaticResource stringHasValue}}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="IdentityAddressTwoLabel" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.Address3, Mode=OneWay}"
|
||||
IsVisible="{Binding Cipher.Identity.Address3, Converter={StaticResource stringHasValue}}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="IdentityAddressThreeLabel" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.FullAddressPart2, Mode=OneWay}"
|
||||
IsVisible="{Binding Cipher.Identity.FullAddressPart2, Converter={StaticResource stringHasValue}}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="IdentityFullAddressPartTwoLabel" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Identity.Country, Mode=OneWay}"
|
||||
IsVisible="{Binding Cipher.Identity.Country, Converter={StaticResource stringHasValue}}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="IdentityCountryLabel" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowIdentityAddress}" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
IsVisible="{Binding IsFido2Key}"
|
||||
Spacing="0"
|
||||
Padding="0"
|
||||
Margin="0,10,0,0">
|
||||
<Label
|
||||
Text="{u:I18n Username}"
|
||||
StyleClass="box-label" />
|
||||
<Label
|
||||
Text="{Binding Cipher.Fido2Key.UserName, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
<BoxView StyleClass="box-row-separator" Margin="0,10,0,0" />
|
||||
<Label
|
||||
Text="{u:I18n Passkey}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0" />
|
||||
<Label
|
||||
Text="{Binding CreationDate, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
<BoxView StyleClass="box-row-separator" Margin="0,10,0,0" />
|
||||
<Grid
|
||||
StyleClass="box-row"
|
||||
RowDefinitions="Auto,*,Auto"
|
||||
ColumnDefinitions="*,Auto,Auto">
|
||||
<Label
|
||||
Text="{u:I18n Application}"
|
||||
StyleClass="box-label" />
|
||||
<Label
|
||||
Grid.Row="1"
|
||||
Text="{Binding Cipher.Fido2Key.LaunchUri, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ShareSquare}}"
|
||||
Command="{Binding LaunchUriCommand}"
|
||||
CommandParameter="{Binding Cipher.Fido2Key}"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
VerticalOptions="End"
|
||||
IsVisible="{Binding Cipher.Fido2Key.CanLaunch, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Launch}" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
CommandParameter="Fido2KeyApplication"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyApplication}" />
|
||||
<BoxView
|
||||
StyleClass="box-row-separator"
|
||||
Margin="0,3,0,0"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="3" />
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowUris}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n URIs, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Cipher.Login.Uris}">
|
||||
<controls:RepeaterView ItemsSource="{Binding Cipher.Login.Uris}" AutomationId="CipherUriContainer">
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:LoginUriView">
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid StyleClass="box-row" AutomationId="UriRow">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -533,7 +673,8 @@
|
||||
Text="{Binding HostOrUri, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
AutomationId="UriValue" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ShareSquare}}"
|
||||
@@ -544,7 +685,8 @@
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding CanLaunch, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Launch}" />
|
||||
AutomationProperties.Name="{u:I18n Launch}"
|
||||
AutomationId="LaunchUriButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
@@ -554,7 +696,8 @@
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Copy}" />
|
||||
AutomationProperties.Name="{u:I18n Copy}"
|
||||
AutomationId="CopyUriButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@@ -568,14 +711,15 @@
|
||||
<Label Text="{u:I18n Notes, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<StackLayout StyleClass="box-row" AutomationId="NotesRow">
|
||||
<controls:SelectableLabel
|
||||
Text="{Binding Cipher.Notes, Mode=OneWay}"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value"
|
||||
AutomationId="CipherNotesLabel" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding Cipher.HasFields}">
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding Cipher.HasFields}" AutomationId="CustomFieldsContainer">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n CustomFields, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
@@ -590,21 +734,23 @@
|
||||
<Label Text="{u:I18n Attachments, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Cipher.Attachments}">
|
||||
<controls:RepeaterView ItemsSource="{Binding Cipher.Attachments}" AutomationId="CipherAttachmentsContainer">
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:AttachmentView">
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10">
|
||||
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10" AutomationId="CipherAttachment">
|
||||
<Label
|
||||
Text="{Binding FileName, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationId="CipherAttachmentFileNameLabel" />
|
||||
<Label
|
||||
Text="{Binding SizeName, Mode=OneWay}"
|
||||
StyleClass="box-sub-label"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalTextAlignment="Center" />
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="CipherAttachmentFileSizeLabel" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Download}}"
|
||||
@@ -612,7 +758,8 @@
|
||||
CommandParameter="{Binding .}"
|
||||
VerticalOptions="Center"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Download}" />
|
||||
AutomationProperties.Name="{u:I18n Download}"
|
||||
AutomationId="CipherAttachmentDownloadButton" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@@ -622,17 +769,20 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-bottom">
|
||||
<Label FormattedText="{Binding UpdatedText}"
|
||||
StyleClass="box-footer-label" />
|
||||
StyleClass="box-footer-label"
|
||||
AutomationId="CipherUpdatedDateLabel" />
|
||||
<Label FormattedText="{Binding PasswordUpdatedText}"
|
||||
StyleClass="box-footer-label"
|
||||
IsVisible="{Binding Cipher.PasswordRevisionDisplayDate, Converter={StaticResource notNull}}">
|
||||
IsVisible="{Binding Cipher.PasswordRevisionDisplayDate, Converter={StaticResource notNull}}"
|
||||
AutomationId="CipherUpdatedPasswordDateLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="PasswordHistory_Tapped" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
<Label FormattedText="{Binding PasswordHistoryText}"
|
||||
StyleClass="box-footer-label"
|
||||
IsVisible="{Binding Cipher.HasPasswordHistory}">
|
||||
IsVisible="{Binding Cipher.HasPasswordHistory}"
|
||||
AutomationId="CipherPasswordHistoryLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="PasswordHistory_Tapped" />
|
||||
</Label.GestureRecognizers>
|
||||
@@ -662,6 +812,7 @@
|
||||
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n EditItem}"
|
||||
AutomationId="CipherEditButton"
|
||||
IsVisible="{Binding CanEdit}">
|
||||
<Button.Effects>
|
||||
<effects:FabShadowEffect />
|
||||
|
||||
@@ -204,20 +204,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Clone_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
if (!await _vm.PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, System.EventArgs e)
|
||||
private async void More_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (!DoOnce())
|
||||
{
|
||||
@@ -226,8 +213,12 @@ namespace Bit.App.Pages
|
||||
|
||||
var options = new List<string> { AppResources.Attachments };
|
||||
if (_vm.Cipher.OrganizationId == null)
|
||||
{
|
||||
if (_vm.CanClone)
|
||||
{
|
||||
options.Add(AppResources.Clone);
|
||||
}
|
||||
|
||||
options.Add(AppResources.MoveToOrganization);
|
||||
}
|
||||
else
|
||||
@@ -267,8 +258,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if (selection == AppResources.Clone)
|
||||
{
|
||||
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
_vm.CloneCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,13 +292,13 @@ namespace Bit.App.Pages
|
||||
{
|
||||
ToolbarItems.Remove(_collectionsItem);
|
||||
}
|
||||
if (!ToolbarItems.Contains(_cloneItem))
|
||||
if (_vm.CanClone && !ToolbarItems.Contains(_cloneItem))
|
||||
{
|
||||
ToolbarItems.Insert(1, _cloneItem);
|
||||
}
|
||||
if (!ToolbarItems.Contains(_shareItem))
|
||||
{
|
||||
ToolbarItems.Insert(2, _shareItem);
|
||||
ToolbarItems.Insert(_vm.CanClone ? 2 : 1, _shareItem);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -68,7 +68,8 @@ namespace Bit.App.Pages
|
||||
CopyCommand = new AsyncCommand<string>((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
CopyUriCommand = new AsyncCommand<LoginUriView>(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
CopyFieldCommand = new AsyncCommand<FieldView>(field => CopyAsync(field.Type == FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
LaunchUriCommand = new Command<LoginUriView>(LaunchUri);
|
||||
LaunchUriCommand = new Command<ILaunchableView>(LaunchUri);
|
||||
CloneCommand = new AsyncCommand(CloneAsync, onException: ex => HandleException(ex), allowsMultipleExecutions: false);
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
||||
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
||||
@@ -81,6 +82,7 @@ namespace Bit.App.Pages
|
||||
public ICommand CopyUriCommand { get; set; }
|
||||
public ICommand CopyFieldCommand { get; set; }
|
||||
public Command LaunchUriCommand { get; set; }
|
||||
public ICommand CloneCommand { get; set; }
|
||||
public Command TogglePasswordCommand { get; set; }
|
||||
public Command ToggleCardNumberCommand { get; set; }
|
||||
public Command ToggleCardCodeCommand { get; set; }
|
||||
@@ -146,6 +148,7 @@ namespace Bit.App.Pages
|
||||
public bool IsIdentity => Cipher?.Type == Core.Enums.CipherType.Identity;
|
||||
public bool IsCard => Cipher?.Type == Core.Enums.CipherType.Card;
|
||||
public bool IsSecureNote => Cipher?.Type == Core.Enums.CipherType.SecureNote;
|
||||
public bool IsFido2Key => Cipher?.Type == Core.Enums.CipherType.Fido2Key;
|
||||
public FormattedString ColoredPassword => GeneratedValueFormatter.Format(Cipher.Login.Password);
|
||||
public FormattedString UpdatedText
|
||||
{
|
||||
@@ -246,6 +249,7 @@ namespace Bit.App.Pages
|
||||
public double TotpProgress => string.IsNullOrEmpty(TotpSec) ? 0 : double.Parse(TotpSec) * 100 / _totpInterval;
|
||||
public bool IsDeleted => Cipher.IsDeleted;
|
||||
public bool CanEdit => !Cipher.IsDeleted;
|
||||
public bool CanClone => Cipher.IsClonable;
|
||||
|
||||
public async Task<bool> LoadAsync(Action finishedLoadingAction = null)
|
||||
{
|
||||
@@ -645,6 +649,11 @@ namespace Bit.App.Pages
|
||||
text = Cipher.Card.Code;
|
||||
name = AppResources.SecurityCode;
|
||||
}
|
||||
else if (id == "Fido2KeyApplication")
|
||||
{
|
||||
text = Cipher.Fido2Key?.LaunchUri;
|
||||
name = AppResources.Application;
|
||||
}
|
||||
|
||||
if (text != null)
|
||||
{
|
||||
@@ -668,14 +677,25 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchUri(LoginUriView uri)
|
||||
private void LaunchUri(ILaunchableView launchableView)
|
||||
{
|
||||
if (uri.CanLaunch && (Page as BaseContentPage).DoOnce())
|
||||
if (launchableView.CanLaunch && (Page as BaseContentPage).DoOnce())
|
||||
{
|
||||
_platformUtilsService.LaunchUri(uri.LaunchUri);
|
||||
_platformUtilsService.LaunchUri(launchableView.LaunchUri);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CloneAsync()
|
||||
{
|
||||
if (!await CanCloneAsync() || !await PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var page = new CipherAddEditPage(CipherId, cloneMode: true, cipherDetailsPage: Page as CipherDetailsPage);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
public async Task<bool> PromptPasswordAsync()
|
||||
{
|
||||
if (Cipher.Reprompt == CipherRepromptType.None || _passwordReprompted)
|
||||
@@ -685,5 +705,21 @@ namespace Bit.App.Pages
|
||||
|
||||
return _passwordReprompted = await _passwordRepromptService.ShowPasswordPromptAsync();
|
||||
}
|
||||
|
||||
private async Task<bool> CanCloneAsync()
|
||||
{
|
||||
if (Cipher.Type == CipherType.Fido2Key)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PasskeyWillNotBeCopied);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Cipher.Type == CipherType.Login && Cipher.Login?.Fido2Key != null)
|
||||
{
|
||||
return await _platformUtilsService.ShowDialogAsync(AppResources.ThePasskeyWillNotBeCopiedToTheClonedItemDoYouWantToContinueCloningThisItem, AppResources.PasskeyWillNotBeCopied, AppResources.Yes, AppResources.No);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
Priority="-1"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Account}" />
|
||||
AutomationProperties.Name="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
<ToolbarItem IconImageSource="search.png" Clicked="Search_Clicked"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Search}" />
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
HorizontalOptions="FillAndExpand"
|
||||
TextChanged="SearchBar_TextChanged"
|
||||
SearchButtonPressed="SearchBar_SearchButtonPressed"
|
||||
Placeholder="{Binding PageTitle}" />
|
||||
Placeholder="{Binding PageTitle}"
|
||||
AutomationId="SearchBar" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform"
|
||||
x:Name="_separator" x:Key="separator" />
|
||||
@@ -91,7 +92,8 @@
|
||||
Source="empty_items_state" />
|
||||
<Label
|
||||
Text="{u:I18n ThereAreNoItemsThatMatchTheSearch}"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoSearchResultsLabel" />
|
||||
<Button
|
||||
Text="{u:I18n AddAnItem}"
|
||||
Command="{Binding AddCipherCommand}"
|
||||
@@ -104,7 +106,8 @@
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Ciphers Page">
|
||||
ExtraDataForLogging="Ciphers Page"
|
||||
AutomationId="CipherList">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:CipherView">
|
||||
<controls:CipherViewCell
|
||||
|
||||
@@ -208,6 +208,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if (selection == AppResources.Autofill || selection == AppResources.AutofillAndSave)
|
||||
{
|
||||
if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection == AppResources.AutofillAndSave)
|
||||
{
|
||||
var uris = cipher.Login?.Uris?.ToList();
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
Priority="-1"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Account}" />
|
||||
AutomationProperties.Name="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
<ToolbarItem Icon="search.png" Clicked="Search_Clicked"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Search}" />
|
||||
@@ -65,7 +66,8 @@
|
||||
<DataTemplate x:Key="groupTemplate"
|
||||
x:DataType="pages:GroupingsPageListItem">
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform">
|
||||
StyleClass="list-row, list-row-platform"
|
||||
AutomationId="{Binding AutomationId}">
|
||||
<controls:IconLabel Text="{Binding Icon, Mode=OneWay}"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
@@ -79,12 +81,14 @@
|
||||
LineBreakMode="TailTruncation"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title"/>
|
||||
StyleClass="list-title"
|
||||
AutomationId="ItemNameLabel" />
|
||||
<Label Text="{Binding ItemCount, Mode=OneWay}"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
StyleClass="list-sub"/>
|
||||
StyleClass="list-sub"
|
||||
AutomationId="ItemCountLabel" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -95,7 +99,8 @@
|
||||
Spacing="0"
|
||||
Padding="0"
|
||||
VerticalOptions="FillAndExpand"
|
||||
StyleClass="list-row-header-container, list-row-header-container-platform">
|
||||
StyleClass="list-row-header-container, list-row-header-container-platform"
|
||||
AutomationId="{Binding AutomationId}">
|
||||
<BoxView
|
||||
StyleClass="list-section-separator-top, list-section-separator-top-platform" />
|
||||
<StackLayout StyleClass="list-row-header, list-row-header-platform">
|
||||
@@ -104,7 +109,8 @@
|
||||
StyleClass="list-header, list-header-platform" />
|
||||
<Label
|
||||
Text="{Binding ItemCount}"
|
||||
StyleClass="list-header-sub" />
|
||||
StyleClass="list-header-sub"
|
||||
AutomationId="SectionItemCount" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" />
|
||||
</StackLayout>
|
||||
@@ -147,7 +153,8 @@
|
||||
IsVisible="{Binding ShowNoData}">
|
||||
<Label
|
||||
Text="{Binding NoDataText}"
|
||||
HorizontalTextAlignment="Center"></Label>
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoDataDisplayed"></Label>
|
||||
<Button
|
||||
Text="{u:I18n AddAnItem}"
|
||||
Clicked="AddButton_Clicked"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Bit.App.Pages
|
||||
using Bit.App.Utilities.Automation;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class GroupingsPageHeaderListItem : IGroupingsPageListItem
|
||||
{
|
||||
@@ -10,5 +12,12 @@
|
||||
|
||||
public string Title { get; }
|
||||
public string ItemCount { get; set; }
|
||||
public string AutomationId
|
||||
{
|
||||
get
|
||||
{
|
||||
return AutomationIdsHelper.AddSuffixFor(AutomationIdsHelper.ToEnglishTitleCase(Title), SuffixType.Header);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Bit.App.Utilities.Automation;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -32,5 +33,6 @@ namespace Bit.App.Pages
|
||||
public string Name { get; set; }
|
||||
public string NameShort => string.IsNullOrWhiteSpace(Name) || Name.Length == 0 ? "-" : Name[0].ToString();
|
||||
public string ItemCount { get; set; }
|
||||
public string AutomationId => AutomationIdsHelper.AddSuffixFor(NameShort, SuffixType.ListGroup);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities.Automation;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.View;
|
||||
@@ -59,6 +60,9 @@ namespace Bit.App.Pages
|
||||
case CipherType.Identity:
|
||||
_name = AppResources.TypeIdentity;
|
||||
break;
|
||||
case CipherType.Fido2Key:
|
||||
_name = AppResources.Passkey;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -107,6 +111,9 @@ namespace Bit.App.Pages
|
||||
case CipherType.Identity:
|
||||
_icon = BitwardenIcons.IdCard;
|
||||
break;
|
||||
case CipherType.Fido2Key:
|
||||
_icon = BitwardenIcons.Passkey;
|
||||
break;
|
||||
default:
|
||||
_icon = BitwardenIcons.Globe;
|
||||
break;
|
||||
@@ -115,5 +122,38 @@ namespace Bit.App.Pages
|
||||
return _icon;
|
||||
}
|
||||
}
|
||||
|
||||
public string AutomationId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Type != null)
|
||||
{
|
||||
return AutomationIdsHelper.AddSuffixFor(System.Enum.GetName(typeof(CipherType), Type.Value), SuffixType.Filter);
|
||||
}
|
||||
|
||||
if (IsTrash)
|
||||
{
|
||||
return AutomationIdsHelper.AddSuffixFor("Trash", SuffixType.Filter);
|
||||
}
|
||||
|
||||
if (Folder != null)
|
||||
{
|
||||
return AutomationIdsHelper.AddSuffixFor("Folder", SuffixType.Filter);
|
||||
}
|
||||
|
||||
if (Collection != null)
|
||||
{
|
||||
return AutomationIdsHelper.AddSuffixFor("Collection", SuffixType.Filter);
|
||||
}
|
||||
|
||||
if (IsTotpCode)
|
||||
{
|
||||
return AutomationIdsHelper.AddSuffixFor("TOTP", SuffixType.ListItem);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,35 +235,18 @@ namespace Bit.App.Pages
|
||||
{
|
||||
AddTotpGroupItem(groupedItems, uppercaseGroupNames);
|
||||
|
||||
groupedItems.Add(new GroupingsPageListGroup(
|
||||
AppResources.Types, 4, uppercaseGroupNames, !hasFavorites)
|
||||
var types = new CipherType[] { CipherType.Login, CipherType.Card, CipherType.Identity, CipherType.SecureNote };
|
||||
var typesGroup = new GroupingsPageListGroup(AppResources.Types, types.Length, uppercaseGroupNames, !hasFavorites);
|
||||
foreach (CipherType t in types)
|
||||
{
|
||||
new GroupingsPageListItem
|
||||
typesGroup.Add(new GroupingsPageListItem
|
||||
{
|
||||
Type = CipherType.Login,
|
||||
ItemCount = (_typeCounts.ContainsKey(CipherType.Login) ?
|
||||
_typeCounts[CipherType.Login] : 0).ToString("N0")
|
||||
},
|
||||
new GroupingsPageListItem
|
||||
{
|
||||
Type = CipherType.Card,
|
||||
ItemCount = (_typeCounts.ContainsKey(CipherType.Card) ?
|
||||
_typeCounts[CipherType.Card] : 0).ToString("N0")
|
||||
},
|
||||
new GroupingsPageListItem
|
||||
{
|
||||
Type = CipherType.Identity,
|
||||
ItemCount = (_typeCounts.ContainsKey(CipherType.Identity) ?
|
||||
_typeCounts[CipherType.Identity] : 0).ToString("N0")
|
||||
},
|
||||
new GroupingsPageListItem
|
||||
{
|
||||
Type = CipherType.SecureNote,
|
||||
ItemCount = (_typeCounts.ContainsKey(CipherType.SecureNote) ?
|
||||
_typeCounts[CipherType.SecureNote] : 0).ToString("N0")
|
||||
},
|
||||
Type = t,
|
||||
ItemCount = _typeCounts.GetValueOrDefault(t).ToString("N0")
|
||||
});
|
||||
}
|
||||
groupedItems.Add(typesGroup);
|
||||
}
|
||||
if (NestedFolders?.Any() ?? false)
|
||||
{
|
||||
var folderListItems = NestedFolders.Select(f =>
|
||||
@@ -330,6 +313,8 @@ namespace Bit.App.Pages
|
||||
items.AddRange(itemGroup);
|
||||
}
|
||||
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
// HACK: [PS-536] Fix to avoid blank list after back navigation on unlocking with previous page info
|
||||
@@ -337,6 +322,7 @@ namespace Bit.App.Pages
|
||||
GroupedItems.Clear();
|
||||
}
|
||||
GroupedItems.ReplaceRange(items);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -356,6 +342,8 @@ namespace Bit.App.Pages
|
||||
items.AddRange(itemGroup);
|
||||
}
|
||||
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
if (groupedItems.Any())
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
@@ -371,6 +359,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
GroupedItems.Clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -378,9 +367,12 @@ namespace Bit.App.Pages
|
||||
_doingLoad = false;
|
||||
Loaded = true;
|
||||
Loading = false;
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
ShowNoData = (MainPage && !HasCiphers) || !groupedItems.Any();
|
||||
ShowList = !ShowNoData;
|
||||
DisableRefreshing();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,7 +567,9 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if (Type != null)
|
||||
{
|
||||
Filter = c => c.Type == Type.Value && !c.IsDeleted;
|
||||
Filter = c => !c.IsDeleted
|
||||
&&
|
||||
Type.Value.IsEqualToOrCanSignIn(c.Type);
|
||||
}
|
||||
else if (FolderId != null)
|
||||
{
|
||||
@@ -642,14 +636,11 @@ namespace Bit.App.Pages
|
||||
NoFolderCiphers.Add(c);
|
||||
}
|
||||
|
||||
if (_typeCounts.ContainsKey(c.Type))
|
||||
{
|
||||
_typeCounts[c.Type] = _typeCounts[c.Type] + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
_typeCounts.Add(c.Type, 1);
|
||||
}
|
||||
// Fido2Key ciphers should be counted as Login ciphers
|
||||
var countType = c.Type == CipherType.Fido2Key ? CipherType.Login : c.Type;
|
||||
_typeCounts[countType] = _typeCounts.TryGetValue(countType, out var currentTypeCount)
|
||||
? currentTypeCount + 1
|
||||
: 1;
|
||||
}
|
||||
|
||||
if (c.IsDeleted)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Move}" Clicked="Save_Clicked" />
|
||||
<ToolbarItem Text="{u:I18n Move}" Command="{Binding MoveCommand}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
|
||||
@@ -32,19 +32,6 @@ namespace Bit.App.Pages
|
||||
await LoadOnAppearedAsync(_scrollView, true, () => _vm.LoadAsync());
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
}
|
||||
|
||||
private async void Save_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
@@ -8,6 +9,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -34,6 +36,8 @@ namespace Bit.App.Pages
|
||||
Collections = new ExtendedObservableCollection<CollectionViewModel>();
|
||||
OrganizationOptions = new List<KeyValuePair<string, string>>();
|
||||
PageTitle = AppResources.MoveToOrganization;
|
||||
|
||||
MoveCommand = new AsyncCommand(MoveAsync, onException: ex => HandleException(ex), allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public string CipherId { get; set; }
|
||||
@@ -62,6 +66,8 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _hasOrganizations, value);
|
||||
}
|
||||
|
||||
public ICommand MoveCommand { get; }
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
var allCollections = await _collectionService.GetAllDecryptedAsync();
|
||||
@@ -84,7 +90,7 @@ namespace Bit.App.Pages
|
||||
FilterCollections();
|
||||
}
|
||||
|
||||
public async Task<bool> SubmitAsync()
|
||||
public async Task<bool> MoveAsync()
|
||||
{
|
||||
var selectedCollectionIds = Collections?.Where(c => c.Checked).Select(c => c.Collection.Id);
|
||||
if (!selectedCollectionIds?.Any() ?? true)
|
||||
@@ -107,8 +113,15 @@ namespace Bit.App.Pages
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||
await _cipherService.ShareWithServerAsync(cipherView, OrganizationId, checkedCollectionIds);
|
||||
var error = await _cipherService.ShareWithServerAsync(cipherView, OrganizationId, checkedCollectionIds);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
if (error == ICipherService.ShareWithServerError.DuplicatedPasskeyInOrg)
|
||||
{
|
||||
_platformUtilsService.ShowToast(null, null, AppResources.ThisItemCannotBeSharedWithTheOrganizationBecauseThereIsOneAlreadyWithTheSamePasskey);
|
||||
return false;
|
||||
}
|
||||
|
||||
var movedItemToOrgText = string.Format(AppResources.MovedItemToOrg, cipherView.Name,
|
||||
(await _organizationService.GetAsync(OrganizationId)).Name);
|
||||
_platformUtilsService.ShowToast("success", null, movedItemToOrgText);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user