mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72de17bd1d | ||
|
|
ed3467515e | ||
|
|
21fc56457d | ||
|
|
bc2eb212a6 | ||
|
|
a1912526c2 | ||
|
|
9d0209751c | ||
|
|
f2936c95fa | ||
|
|
bb2f1f0f5f | ||
|
|
5a0c2115a1 | ||
|
|
a67f50b145 | ||
|
|
757e5ea647 | ||
|
|
b23f29511c | ||
|
|
71731bb9b7 | ||
|
|
f2be840a7d | ||
|
|
685e0f407a | ||
|
|
bbef0f8c93 | ||
|
|
3cdf5ccd3b | ||
|
|
e97a37222a | ||
|
|
218a30b510 | ||
|
|
828043ec97 | ||
|
|
b25c8b0842 | ||
|
|
a4a0d31fc6 | ||
|
|
6ef6cf5d84 | ||
|
|
597f629920 | ||
|
|
b8cef16711 | ||
|
|
c4f6ae9077 | ||
|
|
8b9658d2c5 | ||
|
|
43bf0fbdb3 | ||
|
|
11922c6f49 | ||
|
|
a6f05338c2 | ||
|
|
b932824b5a | ||
|
|
efd1671f48 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -6,6 +6,8 @@
|
||||
# Unless a later match takes precedence
|
||||
# @bitwarden/tech-leads
|
||||
|
||||
@bitwarden/dept-development-mobile
|
||||
|
||||
## Auth team files ##
|
||||
|
||||
## Platform team files ##
|
||||
|
||||
@@ -245,6 +245,14 @@
|
||||
<SubType></SubType>
|
||||
<Generator></Generator>
|
||||
</AndroidResource>
|
||||
<AndroidResource Include="Resources\drawable\empty_login_requests.xml">
|
||||
<SubType></SubType>
|
||||
<Generator></Generator>
|
||||
</AndroidResource>
|
||||
<AndroidResource Include="Resources\drawable\empty_login_requests_dark.xml">
|
||||
<SubType></SubType>
|
||||
<Generator></Generator>
|
||||
</AndroidResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
|
||||
|
||||
@@ -20,6 +20,7 @@ using AndroidX.AutoFill.Inline.V1;
|
||||
using Bit.Core.Abstractions;
|
||||
using SaveFlags = Android.Service.Autofill.SaveFlags;
|
||||
using Bit.Droid.Utilities;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Droid.Autofill
|
||||
{
|
||||
@@ -152,8 +153,9 @@ namespace Bit.Droid.Autofill
|
||||
"androidapp://com.oneplus.applocker",
|
||||
};
|
||||
|
||||
public static async Task<List<FilledItem>> GetFillItemsAsync(Parser parser, ICipherService cipherService)
|
||||
public static async Task<List<FilledItem>> GetFillItemsAsync(Parser parser, ICipherService cipherService, IUserVerificationService userVerificationService)
|
||||
{
|
||||
var userHasMasterPassword = await userVerificationService.HasMasterPasswordAsync();
|
||||
if (parser.FieldCollection.FillableForLogin)
|
||||
{
|
||||
var ciphers = await cipherService.GetAllDecryptedByUrlAsync(parser.Uri);
|
||||
@@ -161,14 +163,14 @@ namespace Bit.Droid.Autofill
|
||||
{
|
||||
var allCiphers = ciphers.Item1.ToList();
|
||||
allCiphers.AddRange(ciphers.Item2.ToList());
|
||||
var nonPromptCiphers = allCiphers.Where(cipher => cipher.Reprompt == CipherRepromptType.None);
|
||||
var nonPromptCiphers = allCiphers.Where(cipher => !userHasMasterPassword || cipher.Reprompt == CipherRepromptType.None);
|
||||
return nonPromptCiphers.Select(c => new FilledItem(c)).ToList();
|
||||
}
|
||||
}
|
||||
else if (parser.FieldCollection.FillableForCard)
|
||||
{
|
||||
var ciphers = await cipherService.GetAllDecryptedAsync();
|
||||
return ciphers.Where(c => c.Type == CipherType.Card && c.Reprompt == CipherRepromptType.None).Select(c => new FilledItem(c)).ToList();
|
||||
return ciphers.Where(c => c.Type == CipherType.Card && (!userHasMasterPassword || c.Reprompt == CipherRepromptType.None)).Select(c => new FilledItem(c)).ToList();
|
||||
}
|
||||
return new List<FilledItem>();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using Android.Widget;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Droid.Autofill
|
||||
@@ -26,6 +27,7 @@ namespace Bit.Droid.Autofill
|
||||
private IPolicyService _policyService;
|
||||
private IStateService _stateService;
|
||||
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
private IUserVerificationService _userVerificationService;
|
||||
|
||||
public async override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal,
|
||||
FillCallback callback)
|
||||
@@ -64,11 +66,9 @@ namespace Bit.Droid.Autofill
|
||||
var locked = await _vaultTimeoutService.IsLockedAsync();
|
||||
if (!locked)
|
||||
{
|
||||
if (_cipherService == null)
|
||||
{
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
}
|
||||
items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService);
|
||||
_cipherService ??= ServiceContainer.Resolve<ICipherService>();
|
||||
_userVerificationService ??= ServiceContainer.Resolve<IUserVerificationService>();
|
||||
items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService, _userVerificationService);
|
||||
}
|
||||
|
||||
// build response
|
||||
|
||||
@@ -3,5 +3,11 @@
|
||||
public static class Constants
|
||||
{
|
||||
public const string PACKAGE_NAME = "com.x8bit.bitwarden";
|
||||
public const string TEMP_CAMERA_IMAGE_NAME = "temp_camera_image.jpg";
|
||||
|
||||
/// <summary>
|
||||
/// This directory must also be declared in filepaths.xml
|
||||
/// </summary>
|
||||
public const string TEMP_CAMERA_IMAGE_DIR = "camera_temp";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace Bit.Droid
|
||||
{
|
||||
ListenYubiKey((bool)message.Data);
|
||||
}
|
||||
else if (message.Command == "updatedTheme")
|
||||
else if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
|
||||
{
|
||||
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => AppearanceAdjustments());
|
||||
}
|
||||
@@ -239,18 +239,22 @@ namespace Bit.Droid
|
||||
string fileName = null;
|
||||
if (data != null && data.Data != null)
|
||||
{
|
||||
uri = data.Data;
|
||||
fileName = AndroidHelpers.GetFileName(ApplicationContext, uri);
|
||||
if (data.Data.ToString()?.Contains(Constants.PACKAGE_NAME) != true)
|
||||
{
|
||||
uri = data.Data;
|
||||
fileName = AndroidHelpers.GetFileName(ApplicationContext, uri);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// camera
|
||||
var file = new Java.IO.File(FilesDir, "temp_camera_photo.jpg");
|
||||
var tmpDir = new Java.IO.File(FilesDir, Constants.TEMP_CAMERA_IMAGE_DIR);
|
||||
var file = new Java.IO.File(tmpDir, Constants.TEMP_CAMERA_IMAGE_NAME);
|
||||
uri = FileProvider.GetUriForFile(this, "com.x8bit.bitwarden.fileprovider", file);
|
||||
fileName = $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg";
|
||||
}
|
||||
|
||||
if (uri == null)
|
||||
if (uri == null || fileName == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ namespace Bit.Droid
|
||||
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
||||
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
|
||||
var biometricService = new BiometricService(stateService, cryptoService);
|
||||
var userPinService = new UserPinService(stateService, cryptoService);
|
||||
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
|
||||
|
||||
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
||||
@@ -182,6 +183,7 @@ namespace Bit.Droid
|
||||
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
||||
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
||||
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
||||
ServiceContainer.Register<IUserPinService>(userPinService);
|
||||
|
||||
// Push
|
||||
#if FDROID
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.8.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.9.3" 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" />
|
||||
|
||||
41
src/Android/Resources/drawable/empty_login_requests.xml
Normal file
41
src/Android/Resources/drawable/empty_login_requests.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="143"
|
||||
android:width="200dp"
|
||||
android:height="143dp">
|
||||
<path
|
||||
android:pathData="M34 43H10C6.68629 43 4 45.6863 4 49V109C4 112.314 6.68629 115 10 115H34C37.3137 115 40 112.314 40 109V49C40 45.6863 37.3137 43 34 43ZM10 39C4.47715 39 0 43.4772 0 49V109C0 114.523 4.47715 119 10 119H34C39.5228 119 44 114.523 44 109V49C44 43.4772 39.5228 39 34 39H10Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M20.3701 47.809C20.3701 47.2567 20.8178 46.809 21.3701 46.809H22.6122C23.1645 46.809 23.6122 47.2567 23.6122 47.809C23.6122 48.3612 23.1645 48.809 22.6122 48.809H21.3701C20.8178 48.809 20.3701 48.3612 20.3701 47.809Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M68 120C68 119.448 68.4477 119 69 119H127C127.552 119 128 119.448 128 120C128 120.552 127.552 121 127 121H69C68.4477 121 68 120.552 68 120Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M87.7402 120V102.236H89.7402V120H87.7402Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M107.71 120V102.236H109.71V120H107.71Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M27 25C27 17.268 33.268 11 41 11H157C164.732 11 171 17.268 171 25V31H167V25C167 19.4772 162.523 15 157 15H41C35.4772 15 31 19.4772 31 25V41H27V25ZM42 99H127V103H42V99Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M35 26C35 22.134 38.134 19 42 19H156C159.866 19 163 22.134 163 26V31H161V26C161 23.2386 158.761 21 156 21H42C39.2386 21 37 23.2386 37 26V41H35V26ZM42 93H127V95H42V93Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M125 39C125 33.4771 129.477 29 135 29H188C193.523 29 198 33.4772 198 39V119C198 124.523 193.523 129 188 129H135C129.477 129 125 124.523 125 119V39ZM135 33C131.686 33 129 35.6863 129 39V119C129 122.314 131.686 125 135 125H188C191.314 125 194 122.314 194 119V39C194 35.6863 191.314 33 188 33H135Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#89929F" />
|
||||
<path
|
||||
android:pathData="M164 120C164 121.105 163.105 122 162 122C160.895 122 160 121.105 160 120C160 118.895 160.895 118 162 118C163.105 118 164 118.895 164 120Z"
|
||||
android:fillColor="#89929F" />
|
||||
</vector>
|
||||
41
src/Android/Resources/drawable/empty_login_requests_dark.xml
Normal file
41
src/Android/Resources/drawable/empty_login_requests_dark.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="143"
|
||||
android:width="200dp"
|
||||
android:height="143dp">
|
||||
<path
|
||||
android:pathData="M34 43H10C6.68629 43 4 45.6863 4 49V109C4 112.314 6.68629 115 10 115H34C37.3137 115 40 112.314 40 109V49C40 45.6863 37.3137 43 34 43ZM10 39C4.47715 39 0 43.4772 0 49V109C0 114.523 4.47715 119 10 119H34C39.5228 119 44 114.523 44 109V49C44 43.4772 39.5228 39 34 39H10Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M20.3701 47.809C20.3701 47.2567 20.8178 46.809 21.3701 46.809H22.6122C23.1645 46.809 23.6122 47.2567 23.6122 47.809C23.6122 48.3612 23.1645 48.809 22.6122 48.809H21.3701C20.8178 48.809 20.3701 48.3612 20.3701 47.809Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M68 120C68 119.448 68.4477 119 69 119H127C127.552 119 128 119.448 128 120C128 120.552 127.552 121 127 121H69C68.4477 121 68 120.552 68 120Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M87.7402 120V102.236H89.7402V120H87.7402Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M107.71 120V102.236H109.71V120H107.71Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M27 25C27 17.268 33.268 11 41 11H157C164.732 11 171 17.268 171 25V31H167V25C167 19.4772 162.523 15 157 15H41C35.4772 15 31 19.4772 31 25V41H27V25ZM42 99H127V103H42V99Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M35 26C35 22.134 38.134 19 42 19H156C159.866 19 163 22.134 163 26V31H161V26C161 23.2386 158.761 21 156 21H42C39.2386 21 37 23.2386 37 26V41H35V26ZM42 93H127V95H42V93Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M125 39C125 33.4771 129.477 29 135 29H188C193.523 29 198 33.4772 198 39V119C198 124.523 193.523 129 188 129H135C129.477 129 125 124.523 125 119V39ZM135 33C131.686 33 129 35.6863 129 39V119C129 122.314 131.686 125 135 125H188C191.314 125 194 122.314 194 119V39C194 35.6863 191.314 33 188 33H135Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#A3A3A3" />
|
||||
<path
|
||||
android:pathData="M164 120C164 121.105 163.105 122 162 122C160.895 122 160 121.105 160 120C160 118.895 160.895 118 162 118C163.105 118 164 118.895 164 120Z"
|
||||
android:fillColor="#A3A3A3" />
|
||||
</vector>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path name="cache" path="." />
|
||||
<files-path name="internal" path="." />
|
||||
<files-path name="temp_camera_images" path="camera_temp/" />
|
||||
</paths>
|
||||
|
||||
@@ -547,6 +547,12 @@ namespace Bit.Droid.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
|
||||
|
||||
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;
|
||||
|
||||
public bool SupportsDrawOver() => Build.VERSION.SdkInt >= BuildVersionCodes.M;
|
||||
|
||||
private Intent RateIntentForUrl(string url, Activity activity)
|
||||
{
|
||||
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
|
||||
@@ -601,6 +607,38 @@ namespace Bit.Droid.Services
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public string GetAutofillAccessibilityDescription()
|
||||
{
|
||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.LollipopMr1)
|
||||
{
|
||||
return AppResources.AccessibilityDescription;
|
||||
}
|
||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
|
||||
{
|
||||
return AppResources.AccessibilityDescription2;
|
||||
}
|
||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.NMr1)
|
||||
{
|
||||
return AppResources.AccessibilityDescription3;
|
||||
}
|
||||
|
||||
return AppResources.AccessibilityDescription4;
|
||||
}
|
||||
|
||||
public string GetAutofillDrawOverDescription()
|
||||
{
|
||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
|
||||
{
|
||||
return AppResources.DrawOverDescription;
|
||||
}
|
||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.NMr1)
|
||||
{
|
||||
return AppResources.DrawOverDescription2;
|
||||
}
|
||||
|
||||
return AppResources.DrawOverDescription3;
|
||||
}
|
||||
|
||||
private void SetNumericKeyboardTo(EditText editText)
|
||||
{
|
||||
editText.InputType = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned;
|
||||
|
||||
@@ -190,7 +190,8 @@ namespace Bit.Droid.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
|
||||
var tmpDir = new Java.IO.File(activity.FilesDir, Constants.TEMP_CAMERA_IMAGE_DIR);
|
||||
var file = new Java.IO.File(tmpDir, Constants.TEMP_CAMERA_IMAGE_NAME);
|
||||
if (!file.Exists())
|
||||
{
|
||||
file.ParentFile.Mkdirs();
|
||||
|
||||
@@ -28,6 +28,9 @@ namespace Bit.App.Abstractions
|
||||
bool SupportsNfc();
|
||||
bool SupportsCamera();
|
||||
bool SupportsFido2();
|
||||
bool SupportsAutofillServices();
|
||||
bool SupportsInlineAutofill();
|
||||
bool SupportsDrawOver();
|
||||
|
||||
bool LaunchApp(string appName);
|
||||
void RateApp();
|
||||
@@ -41,5 +44,7 @@ namespace Bit.App.Abstractions
|
||||
Task SetScreenCaptureAllowedAsync();
|
||||
void OpenAppSettings();
|
||||
void CloseExtensionPopUp();
|
||||
string GetAutofillAccessibilityDescription();
|
||||
string GetAutofillDrawOverDescription();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,6 @@
|
||||
<Compile Update="Pages\Settings\ExtensionPage.xaml.cs">
|
||||
<DependentUpon>ExtensionPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\AutofillServicesPage.xaml.cs">
|
||||
<DependentUpon>AutofillServicesPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\FolderAddEditPage.xaml.cs">
|
||||
<DependentUpon>FolderAddEditPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@@ -71,12 +68,6 @@
|
||||
<Compile Update="Pages\Settings\ExportVaultPage.xaml.cs">
|
||||
<DependentUpon>ExportVaultPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\OptionsPage.xaml.cs">
|
||||
<DependentUpon>OptionsPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\SyncPage.xaml.cs">
|
||||
<DependentUpon>SyncPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Vault\AttachmentsPage.xaml.cs">
|
||||
<DependentUpon>AttachmentsPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@@ -147,6 +138,7 @@
|
||||
<Folder Include="Controls\PasswordStrengthProgressBar\" />
|
||||
<Folder Include="Utilities\Automation\" />
|
||||
<Folder Include="Utilities\Prompts\" />
|
||||
<Folder Include="Controls\Settings\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -444,5 +436,6 @@
|
||||
<None Remove="Controls\PasswordStrengthProgressBar\" />
|
||||
<None Remove="Utilities\Automation\" />
|
||||
<None Remove="Utilities\Prompts\" />
|
||||
<None Remove="Controls\Settings\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace Bit.App
|
||||
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
||||
});
|
||||
}
|
||||
else if (message.Command == "resumed")
|
||||
else if (message.Command == AppHelpers.RESUMED_MESSAGE_COMMAND)
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
@@ -365,7 +365,7 @@ namespace Bit.App
|
||||
await Device.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
ThemeManager.SetTheme(Current.Resources);
|
||||
_messagingService.Send("updatedTheme");
|
||||
_messagingService.Send(ThemeManager.UPDATED_THEME_MESSAGE_KEY);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Bit.App.Controls
|
||||
|
||||
public bool ShowHostname
|
||||
{
|
||||
get => !string.IsNullOrWhiteSpace(AccountView.Hostname) && AccountView.Hostname != "vault.bitwarden.com";
|
||||
get => !string.IsNullOrWhiteSpace(AccountView.Hostname);
|
||||
}
|
||||
|
||||
public bool IsActive
|
||||
|
||||
29
src/App/Controls/ExternalLinkItemView.xaml
Normal file
29
src/App/Controls/ExternalLinkItemView.xaml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<ContentView
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
x:Class="Bit.App.Controls.ExternalLinkItemView"
|
||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||
x:Name="_contentView">
|
||||
<ContentView.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToLinkCommand, Mode=OneWay, Source={x:Reference _contentView}}" />
|
||||
</ContentView.GestureRecognizers>
|
||||
<StackLayout
|
||||
Orientation="Horizontal">
|
||||
<controls:CustomLabel
|
||||
Text="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
LineBreakMode="TailTruncation" />
|
||||
|
||||
<controls:IconLabel
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ShareSquare}}"
|
||||
TextColor="{DynamicResource TextColor}"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="Center"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}" />
|
||||
|
||||
</StackLayout>
|
||||
</ContentView>
|
||||
|
||||
31
src/App/Controls/ExternalLinkItemView.xaml.cs
Normal file
31
src/App/Controls/ExternalLinkItemView.xaml.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Windows.Input;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public partial class ExternalLinkItemView : ContentView
|
||||
{
|
||||
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
|
||||
nameof(Title), typeof(string), typeof(ExternalLinkItemView), null, BindingMode.OneWay);
|
||||
|
||||
public static readonly BindableProperty GoToLinkCommandProperty = BindableProperty.Create(
|
||||
nameof(GoToLinkCommand), typeof(ICommand), typeof(ExternalLinkItemView));
|
||||
|
||||
public ExternalLinkItemView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public string Title
|
||||
{
|
||||
get { return (string)GetValue(TitleProperty); }
|
||||
set { SetValue(TitleProperty, value); }
|
||||
}
|
||||
|
||||
public ICommand GoToLinkCommand
|
||||
{
|
||||
get => GetValue(GoToLinkCommandProperty) as ICommand;
|
||||
set => SetValue(GoToLinkCommandProperty, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/App/Controls/Settings/BaseSettingControlView.cs
Normal file
25
src/App/Controls/Settings/BaseSettingControlView.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class BaseSettingItemView : ContentView
|
||||
{
|
||||
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
|
||||
nameof(Title), typeof(string), typeof(SwitchItemView), null, BindingMode.OneWay);
|
||||
|
||||
public static readonly BindableProperty SubtitleProperty = BindableProperty.Create(
|
||||
nameof(Subtitle), typeof(string), typeof(SwitchItemView), null, BindingMode.OneWay);
|
||||
|
||||
public string Title
|
||||
{
|
||||
get { return (string)GetValue(TitleProperty); }
|
||||
set { SetValue(TitleProperty, value); }
|
||||
}
|
||||
|
||||
public string Subtitle
|
||||
{
|
||||
get { return (string)GetValue(SubtitleProperty); }
|
||||
set { SetValue(SubtitleProperty, value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/App/Controls/Settings/SettingChooserItemView.xaml
Normal file
19
src/App/Controls/Settings/SettingChooserItemView.xaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<controls:BaseSettingItemView
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
x:Class="Bit.App.Controls.SettingChooserItemView"
|
||||
x:Name="_contentView"
|
||||
ControlTemplate="{StaticResource SettingControlTemplate}">
|
||||
<controls:BaseSettingItemView.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding ChooseCommand, Mode=OneWay, Source={x:Reference _contentView}}" />
|
||||
</controls:BaseSettingItemView.GestureRecognizers>
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{Binding DisplayValue, Source={x:Reference _contentView}}"
|
||||
HorizontalTextAlignment="End"
|
||||
TextColor="{DynamicResource MutedColor}"
|
||||
StyleClass="list-sub" />
|
||||
|
||||
</controls:BaseSettingItemView>
|
||||
31
src/App/Controls/Settings/SettingChooserItemView.xaml.cs
Normal file
31
src/App/Controls/Settings/SettingChooserItemView.xaml.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Windows.Input;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public partial class SettingChooserItemView : BaseSettingItemView
|
||||
{
|
||||
public static readonly BindableProperty DisplayValueProperty = BindableProperty.Create(
|
||||
nameof(DisplayValue), typeof(string), typeof(SettingChooserItemView), null, BindingMode.OneWay);
|
||||
|
||||
public static readonly BindableProperty ChooseCommandProperty = BindableProperty.Create(
|
||||
nameof(ChooseCommand), typeof(ICommand), typeof(ExternalLinkItemView));
|
||||
|
||||
public string DisplayValue
|
||||
{
|
||||
get { return (string)GetValue(DisplayValueProperty); }
|
||||
set { SetValue(DisplayValueProperty, value); }
|
||||
}
|
||||
|
||||
public SettingChooserItemView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public ICommand ChooseCommand
|
||||
{
|
||||
get => GetValue(ChooseCommandProperty) as ICommand;
|
||||
set => SetValue(ChooseCommandProperty, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/App/Controls/Settings/SwitchItemView.xaml
Normal file
19
src/App/Controls/Settings/SwitchItemView.xaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<controls:BaseSettingItemView
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
x:Class="Bit.App.Controls.SwitchItemView"
|
||||
x:Name="_contentView"
|
||||
ControlTemplate="{StaticResource SettingControlTemplate}">
|
||||
<controls:BaseSettingItemView.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="ContentView_Tapped" />
|
||||
</controls:BaseSettingItemView.GestureRecognizers>
|
||||
|
||||
<Switch
|
||||
x:Name="_switch"
|
||||
HeightRequest="20"
|
||||
Scale="{OnPlatform iOS=0.8, Android=1}"
|
||||
IsToggled="{Binding IsToggled, Mode=TwoWay, Source={x:Reference _contentView}}"
|
||||
AutomationId="{Binding SwitchAutomationId, Mode=OneWay, Source={x:Reference _contentView}}"/>
|
||||
</controls:BaseSettingItemView>
|
||||
45
src/App/Controls/Settings/SwitchItemView.xaml.cs
Normal file
45
src/App/Controls/Settings/SwitchItemView.xaml.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Windows.Input;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public partial class SwitchItemView : BaseSettingItemView
|
||||
{
|
||||
public static readonly BindableProperty IsToggledProperty = BindableProperty.Create(
|
||||
nameof(IsToggled), typeof(bool), typeof(SwitchItemView), null, BindingMode.TwoWay);
|
||||
|
||||
public static readonly BindableProperty SwitchAutomationIdProperty = BindableProperty.Create(
|
||||
nameof(SwitchAutomationId), typeof(string), typeof(SwitchItemView), null, BindingMode.OneWay);
|
||||
|
||||
public static readonly BindableProperty ToggleSwitchCommandProperty = BindableProperty.Create(
|
||||
nameof(ToggleSwitchCommand), typeof(ICommand), typeof(ExternalLinkItemView));
|
||||
|
||||
public SwitchItemView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public bool IsToggled
|
||||
{
|
||||
get { return (bool)GetValue(IsToggledProperty); }
|
||||
set { SetValue(IsToggledProperty, value); }
|
||||
}
|
||||
|
||||
public string SwitchAutomationId
|
||||
{
|
||||
get { return (string)GetValue(SwitchAutomationIdProperty); }
|
||||
set { SetValue(SwitchAutomationIdProperty, value); }
|
||||
}
|
||||
|
||||
public ICommand ToggleSwitchCommand
|
||||
{
|
||||
get => GetValue(ToggleSwitchCommandProperty) as ICommand;
|
||||
set => SetValue(ToggleSwitchCommandProperty, value);
|
||||
}
|
||||
|
||||
void ContentView_Tapped(System.Object sender, System.EventArgs e)
|
||||
{
|
||||
_switch.IsToggled = !_switch.IsToggled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
|
||||
{
|
||||
if (message.Command == "updatedTheme")
|
||||
if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
|
||||
@@ -7,6 +7,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.Models.Request;
|
||||
using Bit.Core.Services;
|
||||
@@ -72,11 +73,12 @@ namespace Bit.App.Pages
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
|
||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||
{
|
||||
AllowAddAccountRow = true,
|
||||
AllowActiveAccountSelection = true
|
||||
};
|
||||
AccountSwitchingOverlayViewModel =
|
||||
new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||
{
|
||||
AllowAddAccountRow = true,
|
||||
AllowActiveAccountSelection = true
|
||||
};
|
||||
}
|
||||
|
||||
public string MasterPassword
|
||||
@@ -155,8 +157,12 @@ namespace Bit.App.Pages
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword
|
||||
? AppResources.PasswordIsVisibleTapToHide
|
||||
: AppResources.PasswordIsNotVisibleTapToShow;
|
||||
|
||||
public Action UnlockedAction { get; set; }
|
||||
public event Action<int?> FocusSecretEntry
|
||||
{
|
||||
@@ -178,8 +184,9 @@ namespace Bit.App.Pages
|
||||
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
|
||||
?? await _stateService.GetPinProtectedKeyAsync();
|
||||
PinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) ||
|
||||
_pinStatus == PinLockType.Persistent;
|
||||
BiometricEnabled = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _biometricService.CanUseBiometricsUnlockAsync();
|
||||
_pinStatus == PinLockType.Persistent;
|
||||
|
||||
BiometricEnabled = await IsBiometricsEnabledAsync();
|
||||
|
||||
// Users without MP and without biometric or pin has no MP to unlock with
|
||||
_hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync();
|
||||
@@ -214,7 +221,9 @@ namespace Bit.App.Pages
|
||||
else
|
||||
{
|
||||
PageTitle = _hasMasterPassword ? AppResources.VerifyMasterPassword : AppResources.UnlockVault;
|
||||
LockedVerifyText = _hasMasterPassword ? AppResources.VaultLockedMasterPassword : AppResources.VaultLockedIdentity;
|
||||
LockedVerifyText = _hasMasterPassword
|
||||
? AppResources.VaultLockedMasterPassword
|
||||
: AppResources.VaultLockedIdentity;
|
||||
}
|
||||
|
||||
if (BiometricEnabled)
|
||||
@@ -233,11 +242,32 @@ namespace Bit.App.Pages
|
||||
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
|
||||
AppResources.UseFingerprintToUnlock;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
ShowPassword = false;
|
||||
try
|
||||
{
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
if (PinEnabled)
|
||||
{
|
||||
await UnlockWithPinAsync(kdfConfig);
|
||||
}
|
||||
else
|
||||
{
|
||||
await UnlockWithMasterPasswordAsync(kdfConfig);
|
||||
}
|
||||
|
||||
}
|
||||
catch (LegacyUserException)
|
||||
{
|
||||
await HandleLegacyUserAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UnlockWithPinAsync(KdfConfig kdfConfig)
|
||||
{
|
||||
if (PinEnabled && string.IsNullOrWhiteSpace(Pin))
|
||||
{
|
||||
@@ -246,6 +276,84 @@ namespace Bit.App.Pages
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
var failed = true;
|
||||
try
|
||||
{
|
||||
EncString userKeyPin;
|
||||
EncString oldPinProtected;
|
||||
switch (_pinStatus)
|
||||
{
|
||||
case PinLockType.Persistent:
|
||||
{
|
||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
|
||||
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
|
||||
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
|
||||
break;
|
||||
}
|
||||
case PinLockType.Transient:
|
||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
|
||||
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
|
||||
break;
|
||||
case PinLockType.Disabled:
|
||||
default:
|
||||
throw new Exception("Pin is disabled");
|
||||
}
|
||||
|
||||
UserKey userKey;
|
||||
if (oldPinProtected != null)
|
||||
{
|
||||
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
|
||||
_pinStatus == PinLockType.Transient,
|
||||
Pin,
|
||||
_email,
|
||||
kdfConfig,
|
||||
oldPinProtected
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
|
||||
Pin,
|
||||
_email,
|
||||
kdfConfig,
|
||||
userKeyPin
|
||||
);
|
||||
}
|
||||
|
||||
var protectedPin = await _stateService.GetProtectedPinAsync();
|
||||
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
|
||||
failed = decryptedPin != Pin;
|
||||
if (!failed)
|
||||
{
|
||||
Pin = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
}
|
||||
}
|
||||
catch (LegacyUserException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
if (failed)
|
||||
{
|
||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||
if (invalidUnlockAttempts >= 5)
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
return;
|
||||
}
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UnlockWithMasterPasswordAsync(KdfConfig kdfConfig)
|
||||
{
|
||||
if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
@@ -254,142 +362,78 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
ShowPassword = false;
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
|
||||
if (PinEnabled)
|
||||
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig);
|
||||
if (await _cryptoService.IsLegacyUserAsync(masterKey))
|
||||
{
|
||||
var failed = true;
|
||||
throw new LegacyUserException();
|
||||
}
|
||||
|
||||
var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync();
|
||||
var passwordValid = false;
|
||||
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
|
||||
|
||||
if (storedKeyHash != null)
|
||||
{
|
||||
// Offline unlock possible
|
||||
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Online unlock required
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey,
|
||||
HashPurpose.ServerAuthorization);
|
||||
var request = new PasswordVerificationRequest();
|
||||
request.MasterPasswordHash = keyHash;
|
||||
|
||||
try
|
||||
{
|
||||
EncString userKeyPin = null;
|
||||
EncString oldPinProtected = null;
|
||||
if (_pinStatus == PinLockType.Persistent)
|
||||
{
|
||||
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,
|
||||
oldPinProtected
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
|
||||
Pin,
|
||||
_email,
|
||||
kdfConfig,
|
||||
userKeyPin
|
||||
);
|
||||
}
|
||||
|
||||
var protectedPin = await _stateService.GetProtectedPinAsync();
|
||||
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
|
||||
failed = decryptedPin != Pin;
|
||||
if (!failed)
|
||||
{
|
||||
Pin = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
}
|
||||
var response = await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
|
||||
passwordValid = true;
|
||||
var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey,
|
||||
HashPurpose.LocalAuthorization);
|
||||
await _cryptoService.SetMasterKeyHashAsync(localKeyHash);
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
failed = true;
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
||||
}
|
||||
if (failed)
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
|
||||
if (passwordValid)
|
||||
{
|
||||
if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions))
|
||||
{
|
||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||
if (invalidUnlockAttempts >= 5)
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
return;
|
||||
}
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
// Save the ForcePasswordResetReason to force a password reset after unlock
|
||||
await _stateService.SetForcePasswordResetReasonAsync(
|
||||
ForcePasswordResetReason.WeakMasterPasswordOnLogin);
|
||||
}
|
||||
|
||||
MasterPassword = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
|
||||
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
||||
await _cryptoService.SetMasterKeyAsync(masterKey);
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
|
||||
// Re-enable biometrics
|
||||
if (BiometricEnabled & !BiometricIntegrityValid)
|
||||
{
|
||||
await _biometricService.SetupBiometricAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig);
|
||||
var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync();
|
||||
var passwordValid = false;
|
||||
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
|
||||
|
||||
if (storedKeyHash != null)
|
||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||
if (invalidUnlockAttempts >= 5)
|
||||
{
|
||||
// Offline unlock possible
|
||||
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Online unlock required
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.ServerAuthorization);
|
||||
var request = new PasswordVerificationRequest();
|
||||
request.MasterPasswordHash = keyHash;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
|
||||
passwordValid = true;
|
||||
var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.LocalAuthorization);
|
||||
await _cryptoService.SetMasterKeyHashAsync(localKeyHash);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
if (passwordValid)
|
||||
{
|
||||
if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions))
|
||||
{
|
||||
// Save the ForcePasswordResetReason to force a password reset after unlock
|
||||
await _stateService.SetForcePasswordResetReasonAsync(
|
||||
ForcePasswordResetReason.WeakMasterPasswordOnLogin);
|
||||
}
|
||||
|
||||
MasterPassword = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
|
||||
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
||||
await _cryptoService.SetMasterKeyAsync(masterKey);
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
|
||||
// Re-enable biometrics
|
||||
if (BiometricEnabled & !BiometricIntegrityValid)
|
||||
{
|
||||
await _biometricService.SetupBiometricAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||
if (invalidUnlockAttempts >= 5)
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
return;
|
||||
}
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
_messagingService.Send("logout");
|
||||
return;
|
||||
}
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,25 +496,36 @@ namespace Bit.App.Pages
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
var secret = PinEnabled ? Pin : MasterPassword;
|
||||
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
|
||||
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length,
|
||||
nameof(FocusSecretEntry));
|
||||
}
|
||||
|
||||
public async Task PromptBiometricAsync()
|
||||
{
|
||||
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
||||
BiometricButtonVisible = BiometricIntegrityValid;
|
||||
if (!BiometricEnabled || !BiometricIntegrityValid)
|
||||
try
|
||||
{
|
||||
return;
|
||||
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
||||
BiometricButtonVisible = BiometricIntegrityValid;
|
||||
if (!BiometricEnabled || !BiometricIntegrityValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)),
|
||||
!PinEnabled && !HasMasterPassword);
|
||||
|
||||
await _stateService.SetBiometricLockedAsync(!success);
|
||||
if (success)
|
||||
{
|
||||
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
}
|
||||
}
|
||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
|
||||
await _stateService.SetBiometricLockedAsync(!success);
|
||||
if (success)
|
||||
catch (LegacyUserException)
|
||||
{
|
||||
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
await HandleLegacyUserAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,5 +548,29 @@ namespace Bit.App.Pages
|
||||
_messagingService.Send("unlocked");
|
||||
UnlockedAction?.Invoke();
|
||||
}
|
||||
|
||||
private async Task<bool> IsBiometricsEnabledAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _vaultTimeoutService.IsBiometricLockSetAsync() &&
|
||||
await _biometricService.CanUseBiometricsUnlockAsync();
|
||||
}
|
||||
catch (LegacyUserException)
|
||||
{
|
||||
await HandleLegacyUserAsync();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task HandleLegacyUserAsync()
|
||||
{
|
||||
// Legacy users must migrate on web vault.
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong,
|
||||
AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
await _vaultTimeoutService.LogOutAsync();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,16 @@
|
||||
<StackLayout HorizontalOptions="FillAndExpand">
|
||||
<Label
|
||||
StyleClass="text-md"
|
||||
Text="{u:I18n RememberThisDevice}"/>
|
||||
Text="{u:I18n RememberThisDevice}" />
|
||||
<Label
|
||||
StyleClass="box-sub-label"
|
||||
Text="{u:I18n TurnOffUsingPublicDevice}"/>
|
||||
Text="{u:I18n TurnOffUsingPublicDevice}" />
|
||||
</StackLayout>
|
||||
<Switch
|
||||
Scale="0.8"
|
||||
IsToggled="{Binding RememberThisDevice}"
|
||||
VerticalOptions="Center"/>
|
||||
AutomationId="RememberThisDeviceSwitch"
|
||||
VerticalOptions="Center" />
|
||||
</StackLayout>
|
||||
<StackLayout Margin="0, 20, 0, 0">
|
||||
<Button
|
||||
@@ -34,31 +35,34 @@
|
||||
Text="{u:I18n Continue}"
|
||||
StyleClass="btn-primary"
|
||||
Command="{Binding ContinueCommand}"
|
||||
IsVisible="{Binding IsNewUser}"/>
|
||||
IsVisible="{Binding IsNewUser}"
|
||||
AutomationId="ContinueButton" />
|
||||
<Button
|
||||
x:Name="_approveWithMyOtherDevice"
|
||||
Text="{u:I18n ApproveWithMyOtherDevice}"
|
||||
StyleClass="btn-primary"
|
||||
Command="{Binding ApproveWithMyOtherDeviceCommand}"
|
||||
IsVisible="{Binding ApproveWithMyOtherDeviceEnabled}"/>
|
||||
IsVisible="{Binding ApproveWithMyOtherDeviceEnabled}"
|
||||
AutomationId="ApproveWithMyOtherDeviceButton" />
|
||||
<Button
|
||||
x:Name="_requestAdminApproval"
|
||||
Text="{u:I18n RequestAdminApproval}"
|
||||
StyleClass="box-button-row"
|
||||
Command="{Binding RequestAdminApprovalCommand}"
|
||||
IsVisible="{Binding RequestAdminApprovalEnabled}"/>
|
||||
IsVisible="{Binding RequestAdminApprovalEnabled}"
|
||||
AutomationId="RequestAdminApprovalButton" />
|
||||
<Button
|
||||
x:Name="_approveWithMasterPassword"
|
||||
Text="{u:I18n ApproveWithMasterPassword}"
|
||||
StyleClass="box-button-row"
|
||||
Command="{Binding ApproveWithMasterPasswordCommand}"
|
||||
IsVisible="{Binding ApproveWithMasterPasswordEnabled}"/>
|
||||
IsVisible="{Binding ApproveWithMasterPasswordEnabled}"
|
||||
AutomationId="ApproveWithMasterPasswordButton" />
|
||||
<Label
|
||||
Text="{Binding LoggingInAsText}"
|
||||
StyleClass="text-sm"
|
||||
Margin="0,40,0,0"
|
||||
AutomationId="LoggingInAsLabel"
|
||||
/>
|
||||
AutomationId="LoggingInAsLabel" />
|
||||
<Label
|
||||
Text="{u:I18n NotYou}"
|
||||
StyleClass="text-md"
|
||||
|
||||
@@ -248,6 +248,14 @@ namespace Bit.App.Pages
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
if (response.RequiresEncryptionKeyMigration)
|
||||
{
|
||||
// Legacy users must migrate on web vault.
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong, AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.TwoFactor)
|
||||
{
|
||||
StartTwoFactorAction?.Invoke();
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1" x:Name="_closeItem"/>
|
||||
<ToolbarItem Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1" x:Name="_closeItem" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
@@ -29,15 +29,17 @@
|
||||
<Label
|
||||
Text="{Binding SubTitle}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,10"/>
|
||||
Margin="0,0,0,10"
|
||||
AutomationId="SubTitleLabel" />
|
||||
<Label
|
||||
Text="{Binding Description}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,24"/>
|
||||
Margin="0,0,0,24"
|
||||
AutomationId="DescriptionLabel" />
|
||||
<Label
|
||||
Text="{u:I18n FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold"/>
|
||||
FontAttributes="Bold" />
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
@@ -62,7 +64,7 @@
|
||||
Color="{DynamicResource DisabledIconColor}" />
|
||||
<Label
|
||||
Text="{Binding OtherOptions}"
|
||||
FontSize="Small"/>
|
||||
FontSize="Small" />
|
||||
<Label
|
||||
Text="{u:I18n ViewAllLoginOptions}"
|
||||
StyleClass="text-sm"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<StackLayout StyleClass="box">
|
||||
<Label Text="{u:I18n LogInSsoSummary}"
|
||||
StyleClass="text-md"
|
||||
HorizontalTextAlignment="Start"></Label>
|
||||
HorizontalTextAlignment="Start" />
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n OrgIdentifier}"
|
||||
@@ -32,13 +32,15 @@
|
||||
Keyboard="Default"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding LogInCommand}" />
|
||||
ReturnCommand="{Binding LogInCommand}"
|
||||
AutomationId="OrgIdentifierEntry" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 0">
|
||||
<Button Text="{u:I18n LogIn}"
|
||||
StyleClass="btn-primary"
|
||||
Clicked="LogIn_Clicked"></Button>
|
||||
Clicked="LogIn_Clicked"
|
||||
AutomationId="LogInButton" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
18
src/App/Pages/BaseModalContentPage.cs
Normal file
18
src/App/Pages/BaseModalContentPage.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class BaseModalContentPage : BaseContentPage
|
||||
{
|
||||
public BaseModalContentPage()
|
||||
{
|
||||
}
|
||||
|
||||
protected void PopModal_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -47,5 +50,24 @@ namespace Bit.App.Pages
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
|
||||
protected AsyncCommand CreateDefaultAsyncCommnad(Func<Task> execute, Func<object, bool> canExecute = null)
|
||||
{
|
||||
return new AsyncCommand(execute,
|
||||
canExecute,
|
||||
ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
protected async Task<bool> HasConnectivityAsync()
|
||||
{
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.Value.ShowDialogAsync(
|
||||
AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Styles;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
@@ -79,7 +80,7 @@ namespace Bit.App.Pages
|
||||
|
||||
_broadcasterService.Subscribe(nameof(GeneratorPage), (message) =>
|
||||
{
|
||||
if (message.Command == "updatedTheme")
|
||||
if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => _vm.RedrawPassword());
|
||||
}
|
||||
|
||||
124
src/App/Pages/PickerViewModel.cs
Normal file
124
src/App/Pages/PickerViewModel.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class PickerViewModel<TKey> : ExtendedViewModel
|
||||
{
|
||||
const string SELECTED_CHARACTER = "✓";
|
||||
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Func<TKey, Task<bool>> _onSelectionChangingAsync;
|
||||
private readonly string _title;
|
||||
|
||||
public Dictionary<TKey, string> _items;
|
||||
private TKey _selectedKey;
|
||||
private TKey _defaultSelectedKeyIfFailsToFind;
|
||||
private Func<TKey, Task> _afterSelectionChangedAsync;
|
||||
|
||||
public PickerViewModel(IDeviceActionService deviceActionService,
|
||||
ILogger logger,
|
||||
Func<TKey, Task<bool>> onSelectionChangingAsync,
|
||||
string title,
|
||||
Func<object, bool> canExecuteSelectOptionCommand = null,
|
||||
Action<Exception> onSelectOptionCommandException = null)
|
||||
{
|
||||
_deviceActionService = deviceActionService;
|
||||
_logger = logger;
|
||||
_onSelectionChangingAsync = onSelectionChangingAsync;
|
||||
_title = title;
|
||||
|
||||
SelectOptionCommand = new AsyncCommand(SelectOptionAsync, canExecuteSelectOptionCommand, onSelectOptionCommandException, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public AsyncCommand SelectOptionCommand { get; }
|
||||
|
||||
public TKey SelectedKey => _selectedKey;
|
||||
|
||||
public string SelectedValue
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_items.TryGetValue(_selectedKey, out var option))
|
||||
{
|
||||
return option;
|
||||
}
|
||||
|
||||
_selectedKey = _defaultSelectedKeyIfFailsToFind;
|
||||
return _items[_selectedKey];
|
||||
}
|
||||
}
|
||||
|
||||
public void Init(Dictionary<TKey, string> items, TKey currentSelectedKey, TKey defaultSelectedKeyIfFailsToFind, bool logIfKeyNotFound = true)
|
||||
{
|
||||
_items = items;
|
||||
_defaultSelectedKeyIfFailsToFind = defaultSelectedKeyIfFailsToFind;
|
||||
|
||||
Select(currentSelectedKey, logIfKeyNotFound);
|
||||
}
|
||||
|
||||
public void Select(TKey key, bool logIfKeyNotFound = true)
|
||||
{
|
||||
if (!_items.ContainsKey(key))
|
||||
{
|
||||
if (logIfKeyNotFound)
|
||||
{
|
||||
_logger.Error($"There is no {_title} options for key: {key}");
|
||||
}
|
||||
key = _defaultSelectedKeyIfFailsToFind;
|
||||
}
|
||||
|
||||
_selectedKey = key;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(SelectedValue)));
|
||||
}
|
||||
|
||||
private async Task SelectOptionAsync()
|
||||
{
|
||||
var selection = await _deviceActionService.DisplayActionSheetAsync(_title,
|
||||
AppResources.Cancel,
|
||||
null,
|
||||
_items.Select(o => CreateSelectableOption(o.Value, EqualityComparer<TKey>.Default.Equals(o.Key, _selectedKey)))
|
||||
.ToArray()
|
||||
);
|
||||
|
||||
if (selection == null || selection == AppResources.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sanitizedSelection = selection.Replace($"{SELECTED_CHARACTER} ", string.Empty);
|
||||
var optionKey = _items.First(o => o.Value == sanitizedSelection).Key;
|
||||
|
||||
if (EqualityComparer<TKey>.Default.Equals(optionKey, _selectedKey)
|
||||
||
|
||||
!await _onSelectionChangingAsync(optionKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedKey = optionKey;
|
||||
TriggerPropertyChanged(nameof(SelectedValue));
|
||||
|
||||
if (_afterSelectionChangedAsync != null)
|
||||
{
|
||||
await _afterSelectionChangedAsync(_selectedKey);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAfterSelectionChanged(Func<TKey, Task> afterSelectionChangedAsync) => _afterSelectionChangedAsync = afterSelectionChangedAsync;
|
||||
|
||||
private string CreateSelectableOption(string option, bool selected) => selected ? ToSelectedOption(option) : option;
|
||||
|
||||
private string ToSelectedOption(string option) => $"{SELECTED_CHARACTER} {option}";
|
||||
}
|
||||
}
|
||||
85
src/App/Pages/Settings/AboutSettingsPage.xaml
Normal file
85
src/App/Pages/Settings/AboutSettingsPage.xaml
Normal file
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:AboutSettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||
x:Class="Bit.App.Pages.AboutSettingsPage"
|
||||
Title="{u:I18n About}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:AboutSettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<StackLayout>
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n SubmitCrashLogs}"
|
||||
IsToggled="{Binding ShouldSubmitCrashLogs, Mode=TwoWay}"
|
||||
AutomationId="SubmitCrashLogsSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n BitwardenHelpCenter}"
|
||||
GoToLinkCommand="{Binding GoToHelpCenterCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n ContactBitwardenSupport}"
|
||||
GoToLinkCommand="{Binding ContactBitwardenSupportCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n WebVault}"
|
||||
GoToLinkCommand="{Binding GoToWebVaultCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n LearnOrg}"
|
||||
GoToLinkCommand="{Binding GoToLearnAboutOrgsCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n RateTheApp}"
|
||||
GoToLinkCommand="{Binding RateTheAppCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<StackLayout
|
||||
Padding="16,12"
|
||||
Orientation="Horizontal">
|
||||
<controls:CustomLabel
|
||||
Text="{Binding AppInfo}"
|
||||
MaxLines="10"
|
||||
StyleClass="box-footer-label"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
LineBreakMode="TailTruncation" />
|
||||
|
||||
<controls:IconLabel
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
TextColor="Black"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="Center"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyAppInformation}">
|
||||
<controls:IconLabel.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding CopyAppInfoCommand}" />
|
||||
</controls:IconLabel.GestureRecognizers>
|
||||
</controls:IconLabel>
|
||||
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
||||
37
src/App/Pages/Settings/AboutSettingsPage.xaml.cs
Normal file
37
src/App/Pages/Settings/AboutSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AboutSettingsPage : BaseContentPage
|
||||
{
|
||||
private readonly AboutSettingsPageViewModel _vm;
|
||||
|
||||
public AboutSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as AboutSettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
|
||||
|
||||
Navigation.PopAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/App/Pages/Settings/AboutSettingsPageViewModel.cs
Normal file
159
src/App/Pages/Settings/AboutSettingsPageViewModel.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
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;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class AboutSettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private bool _inited;
|
||||
private bool _shouldSubmitCrashLogs;
|
||||
|
||||
public AboutSettingsPageViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
|
||||
var environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||
var clipboardService = ServiceContainer.Resolve<IClipboardService>();
|
||||
|
||||
ToggleSubmitCrashLogsCommand = CreateDefaultAsyncCommnad(ToggleSubmitCrashLogsAsync);
|
||||
|
||||
GoToHelpCenterCommand = CreateDefaultAsyncCommnad(
|
||||
() => LaunchUriAsync(AppResources.LearnMoreAboutHowToUseBitwardenOnTheHelpCenter,
|
||||
AppResources.ContinueToHelpCenter,
|
||||
ExternalLinksConstants.HELP_CENTER));
|
||||
|
||||
ContactBitwardenSupportCommand = CreateDefaultAsyncCommnad(
|
||||
() => LaunchUriAsync(AppResources.ContactSupportDescriptionLong,
|
||||
AppResources.ContinueToContactSupport,
|
||||
ExternalLinksConstants.CONTACT_SUPPORT));
|
||||
|
||||
GoToWebVaultCommand = CreateDefaultAsyncCommnad(
|
||||
() => LaunchUriAsync(AppResources.ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp,
|
||||
AppResources.ContinueToWebApp,
|
||||
environmentService.GetWebVaultUrl()));
|
||||
|
||||
GoToLearnAboutOrgsCommand = CreateDefaultAsyncCommnad(
|
||||
() => LaunchUriAsync(AppResources.LearnAboutOrganizationsDescriptionLong,
|
||||
string.Format(AppResources.ContinueToX, ExternalLinksConstants.BITWARDEN_WEBSITE),
|
||||
ExternalLinksConstants.HELP_ABOUT_ORGANIZATIONS));
|
||||
|
||||
RateTheAppCommand = CreateDefaultAsyncCommnad(RateAppAsync);
|
||||
|
||||
CopyAppInfoCommand = CreateDefaultAsyncCommnad(
|
||||
() => clipboardService.CopyTextAsync(AppInfo));
|
||||
}
|
||||
|
||||
public bool ShouldSubmitCrashLogs
|
||||
{
|
||||
get => _shouldSubmitCrashLogs;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _shouldSubmitCrashLogs, value);
|
||||
((ICommand)ToggleSubmitCrashLogsCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
|
||||
public string AppInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
var appInfo = string.Format("{0}: {1} ({2})",
|
||||
AppResources.Version,
|
||||
_platformUtilsService.GetApplicationVersion(),
|
||||
_deviceActionService.GetBuildNumber());
|
||||
|
||||
return $"© Bitwarden Inc. 2015-{DateTime.Now.Year}\n\n{appInfo}";
|
||||
}
|
||||
}
|
||||
|
||||
public AsyncCommand ToggleSubmitCrashLogsCommand { get; }
|
||||
public ICommand GoToHelpCenterCommand { get; }
|
||||
public ICommand ContactBitwardenSupportCommand { get; }
|
||||
public ICommand GoToWebVaultCommand { get; }
|
||||
public ICommand GoToLearnAboutOrgsCommand { get; }
|
||||
public ICommand RateTheAppCommand { get; }
|
||||
public ICommand CopyAppInfoCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_shouldSubmitCrashLogs = await _logger.IsEnabled();
|
||||
|
||||
_inited = true;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(ShouldSubmitCrashLogs));
|
||||
ToggleSubmitCrashLogsCommand.RaiseCanExecuteChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ToggleSubmitCrashLogsAsync()
|
||||
{
|
||||
await _logger.SetEnabled(ShouldSubmitCrashLogs);
|
||||
_shouldSubmitCrashLogs = await _logger.IsEnabled();
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(ShouldSubmitCrashLogs)));
|
||||
}
|
||||
|
||||
private async Task LaunchUriAsync(string dialogText, string dialogTitle, string uri)
|
||||
{
|
||||
if (await _platformUtilsService.ShowDialogAsync(dialogText, dialogTitle, AppResources.Continue, AppResources.Cancel))
|
||||
{
|
||||
_platformUtilsService.LaunchUri(uri);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RateAppAsync()
|
||||
{
|
||||
if (await _platformUtilsService.ShowDialogAsync(AppResources.RateAppDescriptionLong, AppResources.ContinueToAppStore, AppResources.Continue, AppResources.Cancel))
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(_deviceActionService.RateApp);
|
||||
}
|
||||
}
|
||||
|
||||
/// INFO: Left here in case we need to debug push notifications
|
||||
/// <summary>
|
||||
/// Sets up app info plus debugging information for push notifications.
|
||||
/// Useful when trying to solve problems regarding push notifications.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// Add an IniAsync() method to be called on view appearing, change the AppInfo to be a normal property with setter
|
||||
/// and set the result of this method in the main thread to that property to show that in the UI.
|
||||
/// </example>
|
||||
// public async Task<string> GetAppInfoForPushNotificationsDebugAsync()
|
||||
// {
|
||||
// var stateService = ServiceContainer.Resolve<IStateService>();
|
||||
|
||||
// var appInfo = string.Format("{0}: {1} ({2})", AppResources.Version,
|
||||
// _platformUtilsService.GetApplicationVersion(), _deviceActionService.GetBuildNumber());
|
||||
|
||||
//#if DEBUG
|
||||
// var pushNotificationsRegistered = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService").IsRegisteredForPush;
|
||||
// var pnServerRegDate = await stateService.GetPushLastRegistrationDateAsync();
|
||||
// var pnServerError = await stateService.GetPushInstallationRegistrationErrorAsync();
|
||||
|
||||
// var pnServerRegDateMessage = default(DateTime) == pnServerRegDate ? "-" : $"{pnServerRegDate.GetValueOrDefault().ToShortDateString()}-{pnServerRegDate.GetValueOrDefault().ToShortTimeString()} UTC";
|
||||
// var errorMessage = string.IsNullOrEmpty(pnServerError) ? string.Empty : $"Push Notifications Server Registration error: {pnServerError}";
|
||||
|
||||
// var text = string.Format("© Bitwarden Inc. 2015-{0}\n\n{1}\nPush Notifications registered:{2}\nPush Notifications Server Last Date :{3}\n{4}", DateTime.Now.Year, appInfo, pushNotificationsRegistered, pnServerRegDateMessage, errorMessage);
|
||||
//#else
|
||||
// var text = string.Format("© Bitwarden Inc. 2015-{0}\n\n{1}", DateTime.Now.Year, appInfo);
|
||||
//#endif
|
||||
// return text;
|
||||
// }
|
||||
}
|
||||
}
|
||||
58
src/App/Pages/Settings/AppearanceSettingsPage.xaml
Normal file
58
src/App/Pages/Settings/AppearanceSettingsPage.xaml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:AppearanceSettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:Class="Bit.App.Pages.AppearanceSettingsPage"
|
||||
Title="{u:I18n Appearance}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:AppearanceSettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="5">
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n Language}"
|
||||
Subtitle="{u:I18n LanguageChangeRequiresAppRestart}"
|
||||
DisplayValue="{Binding LanguagePickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding LanguagePickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="LanguageChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n Theme}"
|
||||
Subtitle="{u:I18n ThemeDescription}"
|
||||
DisplayValue="{Binding ThemePickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding ThemePickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="ThemeChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n DefaultDarkTheme}"
|
||||
Subtitle="{u:I18n DefaultDarkThemeDescriptionLong}"
|
||||
DisplayValue="{Binding DefaultDarkThemePickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding DefaultDarkThemePickerViewModel.SelectOptionCommand}"
|
||||
IsVisible="{Binding ShowDefaultDarkThemePicker}"
|
||||
AutomationId="DefaultDarkThemeChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n ShowWebsiteIcons}"
|
||||
Subtitle="{u:I18n ShowWebsiteIconsDescription}"
|
||||
IsToggled="{Binding ShowWebsiteIcons}"
|
||||
IsEnabled="{Binding IsShowWebsiteIconsEnabled}"
|
||||
AutomationId="ShowWebsiteIconsSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</pages:BaseContentPage>
|
||||
44
src/App/Pages/Settings/AppearanceSettingsPage.xaml.cs
Normal file
44
src/App/Pages/Settings/AppearanceSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AppearanceSettingsPage : BaseContentPage
|
||||
{
|
||||
private readonly AppearanceSettingsPageViewModel _vm;
|
||||
|
||||
public AppearanceSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as AppearanceSettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
_vm.SubscribeEvents();
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
|
||||
|
||||
Navigation.PopModalAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_vm.UnsubscribeEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
203
src/App/Pages/Settings/AppearanceSettingsPageViewModel.cs
Normal file
203
src/App/Pages/Settings/AppearanceSettingsPageViewModel.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
using System;
|
||||
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.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class AppearanceSettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
|
||||
private bool _inited;
|
||||
private bool _showWebsiteIcons;
|
||||
|
||||
public AppearanceSettingsPageViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>();
|
||||
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
|
||||
LanguagePickerViewModel = new PickerViewModel<string>(
|
||||
deviceActionService,
|
||||
_logger,
|
||||
OnLanguageChangingAsync,
|
||||
AppResources.Language,
|
||||
_ => _inited,
|
||||
ex => HandleException(ex));
|
||||
|
||||
ThemePickerViewModel = new PickerViewModel<string>(
|
||||
deviceActionService,
|
||||
_logger,
|
||||
key => OnThemeChangingAsync(key, DefaultDarkThemePickerViewModel.SelectedKey),
|
||||
AppResources.Theme,
|
||||
_ => _inited,
|
||||
ex => HandleException(ex));
|
||||
ThemePickerViewModel.SetAfterSelectionChanged(_ =>
|
||||
MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(ShowDefaultDarkThemePicker));
|
||||
}));
|
||||
|
||||
DefaultDarkThemePickerViewModel = new PickerViewModel<string>(
|
||||
deviceActionService,
|
||||
_logger,
|
||||
key => OnThemeChangingAsync(ThemePickerViewModel.SelectedKey, key),
|
||||
AppResources.DefaultDarkTheme,
|
||||
_ => _inited,
|
||||
ex => HandleException(ex));
|
||||
|
||||
ToggleShowWebsiteIconsCommand = CreateDefaultAsyncCommnad(ToggleShowWebsiteIconsAsync, _ => _inited);
|
||||
}
|
||||
|
||||
public PickerViewModel<string> LanguagePickerViewModel { get; }
|
||||
public PickerViewModel<string> ThemePickerViewModel { get; }
|
||||
public PickerViewModel<string> DefaultDarkThemePickerViewModel { get; }
|
||||
|
||||
public bool ShowDefaultDarkThemePicker => ThemePickerViewModel.SelectedKey == string.Empty;
|
||||
|
||||
public bool ShowWebsiteIcons
|
||||
{
|
||||
get => _showWebsiteIcons;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _showWebsiteIcons, value))
|
||||
{
|
||||
((ICommand)ToggleShowWebsiteIconsCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsShowWebsiteIconsEnabled => ToggleShowWebsiteIconsCommand.CanExecute(null);
|
||||
|
||||
public AsyncCommand ToggleShowWebsiteIconsCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_showWebsiteIcons = !(await _stateService.GetDisableFaviconAsync() ?? false);
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(ShowWebsiteIcons)));
|
||||
|
||||
InitLanguagePicker();
|
||||
await InitThemePickerAsync();
|
||||
await InitDefaultDarkThemePickerAsync();
|
||||
|
||||
_inited = true;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
ToggleShowWebsiteIconsCommand.RaiseCanExecuteChanged();
|
||||
LanguagePickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
ThemePickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
DefaultDarkThemePickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private void InitLanguagePicker()
|
||||
{
|
||||
var options = new Dictionary<string, string>
|
||||
{
|
||||
[string.Empty] = AppResources.DefaultSystem
|
||||
};
|
||||
_i18nService.LocaleNames
|
||||
.ToList()
|
||||
.ForEach(pair => options[pair.Key] = pair.Value);
|
||||
|
||||
var selectedKey = _stateService.GetLocale() ?? string.Empty;
|
||||
|
||||
LanguagePickerViewModel.Init(options, selectedKey, string.Empty);
|
||||
}
|
||||
|
||||
private async Task InitThemePickerAsync()
|
||||
{
|
||||
var options = new Dictionary<string, string>
|
||||
{
|
||||
[string.Empty] = AppResources.ThemeDefault,
|
||||
[ThemeManager.Light] = AppResources.Light,
|
||||
[ThemeManager.Dark] = AppResources.Dark,
|
||||
[ThemeManager.Black] = AppResources.Black,
|
||||
[ThemeManager.Nord] = AppResources.Nord,
|
||||
[ThemeManager.SolarizedDark] = AppResources.SolarizedDark
|
||||
};
|
||||
|
||||
var selectedKey = await _stateService.GetThemeAsync() ?? string.Empty;
|
||||
|
||||
ThemePickerViewModel.Init(options, selectedKey, string.Empty);
|
||||
|
||||
TriggerPropertyChanged(nameof(ShowDefaultDarkThemePicker));
|
||||
}
|
||||
|
||||
private async Task InitDefaultDarkThemePickerAsync()
|
||||
{
|
||||
var options = new Dictionary<string, string>
|
||||
{
|
||||
[ThemeManager.Dark] = AppResources.Dark,
|
||||
[ThemeManager.Black] = AppResources.Black,
|
||||
[ThemeManager.Nord] = AppResources.Nord,
|
||||
[ThemeManager.SolarizedDark] = AppResources.SolarizedDark
|
||||
};
|
||||
|
||||
var selectedKey = await _stateService.GetAutoDarkThemeAsync() ?? ThemeManager.Dark;
|
||||
|
||||
DefaultDarkThemePickerViewModel.Init(options, selectedKey, ThemeManager.Dark);
|
||||
}
|
||||
|
||||
private async Task<bool> OnLanguageChangingAsync(string selectedLanguage)
|
||||
{
|
||||
_stateService.SetLocale(selectedLanguage == string.Empty ? (string)null : selectedLanguage);
|
||||
|
||||
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.LanguageChangeXDescription, LanguagePickerViewModel.SelectedValue), AppResources.Language, AppResources.Ok);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> OnThemeChangingAsync(string selectedTheme, string selectedDefaultDarkTheme)
|
||||
{
|
||||
await _stateService.SetThemeAsync(selectedTheme == string.Empty ? (string)null : selectedTheme);
|
||||
await _stateService.SetAutoDarkThemeAsync(selectedDefaultDarkTheme == string.Empty ? (string)null : selectedDefaultDarkTheme);
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
ThemeManager.SetTheme(Application.Current.Resources);
|
||||
_messagingService.Send(ThemeManager.UPDATED_THEME_MESSAGE_KEY);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ToggleShowWebsiteIconsAsync()
|
||||
{
|
||||
// TODO: [PS-961] Fix negative function names
|
||||
await _stateService.SetDisableFaviconAsync(!ShowWebsiteIcons);
|
||||
}
|
||||
|
||||
private void ToggleShowWebsiteIconsCommand_CanExecuteChanged(object sender, EventArgs e)
|
||||
{
|
||||
TriggerPropertyChanged(nameof(IsShowWebsiteIconsEnabled));
|
||||
}
|
||||
|
||||
internal void SubscribeEvents()
|
||||
{
|
||||
ToggleShowWebsiteIconsCommand.CanExecuteChanged += ToggleShowWebsiteIconsCommand_CanExecuteChanged;
|
||||
}
|
||||
|
||||
internal void UnsubscribeEvents()
|
||||
{
|
||||
ToggleShowWebsiteIconsCommand.CanExecuteChanged -= ToggleShowWebsiteIconsCommand_CanExecuteChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.AutofillServicesPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:AutofillServicesPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:AutofillServicesPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Padding="0" Spacing="20">
|
||||
<StackLayout
|
||||
StyleClass="box"
|
||||
IsVisible="{Binding AutofillServiceVisible}">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n AutofillService}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<RelativeLayout HorizontalOptions="End">
|
||||
<Switch
|
||||
x:Name="AutofillServiceSwitch"
|
||||
IsToggled="{Binding AutofillServiceToggled}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
<Button
|
||||
Clicked="ToggleAutofillService"
|
||||
StyleClass="box-overlay"
|
||||
RelativeLayout.XConstraint="0"
|
||||
RelativeLayout.YConstraint="0"
|
||||
RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView, ElementName=AutofillServiceSwitch, Property=Width}"
|
||||
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView, ElementName=AutofillServiceSwitch, Property=Height}" />
|
||||
</RelativeLayout>
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n AutofillServiceDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box"
|
||||
IsVisible="{Binding InlineAutofillVisible}">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n InlineAutofill}"
|
||||
StyleClass="box-label-regular"
|
||||
IsEnabled="{Binding InlineAutofillEnabled}"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<RelativeLayout HorizontalOptions="End">
|
||||
<Switch
|
||||
x:Name="InlineAutofillSwitch"
|
||||
IsEnabled="{Binding InlineAutofillEnabled}"
|
||||
IsToggled="{Binding InlineAutofillToggled}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
<Button
|
||||
Clicked="ToggleInlineAutofill"
|
||||
StyleClass="box-overlay"
|
||||
RelativeLayout.XConstraint="0"
|
||||
RelativeLayout.YConstraint="0"
|
||||
RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView, ElementName=InlineAutofillSwitch, Property=Width}"
|
||||
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView, ElementName=InlineAutofillSwitch, Property=Height}" />
|
||||
</RelativeLayout>
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n InlineAutofillDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch"
|
||||
IsEnabled="{Binding InlineAutofillEnabled}"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n Accessibility}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<RelativeLayout HorizontalOptions="End">
|
||||
<Switch
|
||||
x:Name="AccessibilitySwitch"
|
||||
IsToggled="{Binding AccessibilityToggled}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
<Button
|
||||
Command="{Binding ToggleAccessibilityCommand}"
|
||||
StyleClass="box-overlay"
|
||||
RelativeLayout.XConstraint="0"
|
||||
RelativeLayout.YConstraint="0"
|
||||
RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView, ElementName=AccessibilitySwitch, Property=Width}"
|
||||
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView, ElementName=AccessibilitySwitch, Property=Height}" />
|
||||
</RelativeLayout>
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{Binding AccessibilityDescriptionLabel}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box"
|
||||
IsVisible="{Binding DrawOverVisible}">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n DrawOver}"
|
||||
StyleClass="box-label-regular"
|
||||
IsEnabled="{Binding DrawOverEnabled}"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<RelativeLayout HorizontalOptions="End">
|
||||
<Switch
|
||||
x:Name="DrawOverSwitch"
|
||||
IsEnabled="{Binding DrawOverEnabled}"
|
||||
IsToggled="{Binding DrawOverToggled}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
<Button
|
||||
Clicked="ToggleDrawOver"
|
||||
StyleClass="box-overlay"
|
||||
RelativeLayout.XConstraint="0"
|
||||
RelativeLayout.YConstraint="0"
|
||||
RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView, ElementName=DrawOverSwitch, Property=Width}"
|
||||
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView, ElementName=DrawOverSwitch, Property=Height}" />
|
||||
</RelativeLayout>
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{Binding DrawOverDescriptionLabel}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch"
|
||||
IsEnabled="{Binding InlineAutofillEnabled}"/>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
@@ -1,66 +0,0 @@
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillServicesPage : BaseContentPage
|
||||
{
|
||||
private readonly AutofillServicesPageViewModel _vm;
|
||||
private readonly SettingsPage _settingsPage;
|
||||
private DateTime? _timerStarted = null;
|
||||
private TimeSpan _timerMaxLength = TimeSpan.FromMinutes(5);
|
||||
|
||||
public AutofillServicesPage(SettingsPage settingsPage)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as AutofillServicesPageViewModel;
|
||||
_vm.Page = this;
|
||||
_settingsPage = settingsPage;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
await _vm.InitAsync();
|
||||
_vm.UpdateEnabled();
|
||||
_timerStarted = DateTime.UtcNow;
|
||||
Device.StartTimer(new TimeSpan(0, 0, 0, 0, 500), () =>
|
||||
{
|
||||
if (_timerStarted == null || (DateTime.UtcNow - _timerStarted) > _timerMaxLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_vm.UpdateEnabled();
|
||||
return true;
|
||||
});
|
||||
base.OnAppearing();
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
_timerStarted = null;
|
||||
_settingsPage.BuildList();
|
||||
base.OnDisappearing();
|
||||
}
|
||||
|
||||
private void ToggleAutofillService(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.ToggleAutofillService();
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleInlineAutofill(object sender, EventArgs e)
|
||||
{
|
||||
_vm.ToggleInlineAutofill();
|
||||
}
|
||||
|
||||
private void ToggleDrawOver(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.ToggleDrawOver();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Services;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class AutofillServicesPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAutofillHandler _autofillHandler;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly MobileI18nService _i18nService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
private bool _autofillServiceToggled;
|
||||
private bool _inlineAutofillToggled;
|
||||
private bool _accessibilityToggled;
|
||||
private bool _drawOverToggled;
|
||||
private bool _inited;
|
||||
|
||||
public AutofillServicesPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService;
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
PageTitle = AppResources.AutofillServices;
|
||||
ToggleAccessibilityCommand = new AsyncCommand(ToggleAccessibilityAsync,
|
||||
onException: ex => _logger.Value.Exception(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
#region Autofill Service
|
||||
|
||||
public bool AutofillServiceVisible
|
||||
{
|
||||
get => _deviceActionService.SystemMajorVersion() >= 26;
|
||||
}
|
||||
|
||||
public bool AutofillServiceToggled
|
||||
{
|
||||
get => _autofillServiceToggled;
|
||||
set => SetProperty(ref _autofillServiceToggled, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(InlineAutofillEnabled)
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Inline Autofill
|
||||
|
||||
public bool InlineAutofillVisible
|
||||
{
|
||||
get => _deviceActionService.SystemMajorVersion() >= 30;
|
||||
}
|
||||
|
||||
public bool InlineAutofillEnabled
|
||||
{
|
||||
get => AutofillServiceToggled;
|
||||
}
|
||||
|
||||
public bool InlineAutofillToggled
|
||||
{
|
||||
get => _inlineAutofillToggled;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _inlineAutofillToggled, value))
|
||||
{
|
||||
var task = UpdateInlineAutofillToggledAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Accessibility
|
||||
|
||||
public ICommand ToggleAccessibilityCommand { get; }
|
||||
|
||||
public string AccessibilityDescriptionLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_deviceActionService.SystemMajorVersion() <= 22)
|
||||
{
|
||||
// Android 5
|
||||
return _i18nService.T("AccessibilityDescription");
|
||||
}
|
||||
if (_deviceActionService.SystemMajorVersion() == 23)
|
||||
{
|
||||
// Android 6
|
||||
return _i18nService.T("AccessibilityDescription2");
|
||||
}
|
||||
if (_deviceActionService.SystemMajorVersion() == 24 || _deviceActionService.SystemMajorVersion() == 25)
|
||||
{
|
||||
// Android 7
|
||||
return _i18nService.T("AccessibilityDescription3");
|
||||
}
|
||||
// Android 8+
|
||||
return _i18nService.T("AccessibilityDescription4");
|
||||
}
|
||||
}
|
||||
|
||||
public bool AccessibilityToggled
|
||||
{
|
||||
get => _accessibilityToggled;
|
||||
set => SetProperty(ref _accessibilityToggled, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(DrawOverEnabled)
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Draw-Over
|
||||
|
||||
public bool DrawOverVisible
|
||||
{
|
||||
get => _deviceActionService.SystemMajorVersion() >= 23;
|
||||
}
|
||||
|
||||
public string DrawOverDescriptionLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_deviceActionService.SystemMajorVersion() <= 23)
|
||||
{
|
||||
// Android 6
|
||||
return _i18nService.T("DrawOverDescription");
|
||||
}
|
||||
if (_deviceActionService.SystemMajorVersion() == 24 || _deviceActionService.SystemMajorVersion() == 25)
|
||||
{
|
||||
// Android 7
|
||||
return _i18nService.T("DrawOverDescription2");
|
||||
}
|
||||
// Android 8+
|
||||
return _i18nService.T("DrawOverDescription3");
|
||||
}
|
||||
}
|
||||
|
||||
public bool DrawOverEnabled
|
||||
{
|
||||
get => AccessibilityToggled;
|
||||
}
|
||||
|
||||
public bool DrawOverToggled
|
||||
{
|
||||
get => _drawOverToggled;
|
||||
set => SetProperty(ref _drawOverToggled, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
InlineAutofillToggled = await _stateService.GetInlineAutofillEnabledAsync() ?? true;
|
||||
_inited = true;
|
||||
}
|
||||
|
||||
public void ToggleAutofillService()
|
||||
{
|
||||
if (!AutofillServiceToggled)
|
||||
{
|
||||
_deviceActionService.OpenAutofillSettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
_autofillHandler.DisableAutofillService();
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleInlineAutofill()
|
||||
{
|
||||
if (!InlineAutofillEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
InlineAutofillToggled = !InlineAutofillToggled;
|
||||
}
|
||||
|
||||
public async Task ToggleAccessibilityAsync()
|
||||
{
|
||||
if (!_autofillHandler.AutofillAccessibilityServiceRunning())
|
||||
{
|
||||
var accept = await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText,
|
||||
AppResources.AccessibilityServiceDisclosure, AppResources.Accept,
|
||||
AppResources.Decline);
|
||||
if (!accept)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
_deviceActionService.OpenAccessibilitySettings();
|
||||
}
|
||||
|
||||
public void ToggleDrawOver()
|
||||
{
|
||||
if (!DrawOverEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_deviceActionService.OpenAccessibilityOverlayPermissionSettings();
|
||||
}
|
||||
|
||||
public void UpdateEnabled()
|
||||
{
|
||||
AutofillServiceToggled =
|
||||
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
|
||||
AccessibilityToggled = _autofillHandler.AutofillAccessibilityServiceRunning();
|
||||
DrawOverToggled = _autofillHandler.AutofillAccessibilityOverlayPermitted();
|
||||
}
|
||||
|
||||
private async Task UpdateInlineAutofillToggledAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
await _stateService.SetInlineAutofillEnabledAsync(InlineAutofillToggled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/App/Pages/Settings/AutofillSettingsPage.xaml
Normal file
133
src/App/Pages/Settings/AutofillSettingsPage.xaml
Normal file
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:AutofillSettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:Class="Bit.App.Pages.AutofillSettingsPage"
|
||||
Title="{u:I18n Autofill}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:AutofillSettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="5">
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n Autofill}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n AutofillServices}"
|
||||
Subtitle="{u:I18n AutofillServicesExplanationLong}"
|
||||
IsVisible="{Binding SupportsAndroidAutofillServices}"
|
||||
IsToggled="{Binding UseAutofillServices}"
|
||||
AutomationId="AutofillServicesSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n InlineAutofill}"
|
||||
Subtitle="{u:I18n UseInlineAutofillExplanationLong}"
|
||||
IsToggled="{Binding UseInlineAutofill}"
|
||||
IsVisible="{Binding ShowUseInlineAutofillToggle}"
|
||||
IsEnabled="{Binding UseAutofillServices}"
|
||||
AutomationId="InlineAutofillSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n Accessibility}"
|
||||
Subtitle="{Binding UseAccessibilityDescription}"
|
||||
IsToggled="{Binding UseAccessibility}"
|
||||
IsVisible="{Binding ShowUseAccessibilityToggle}"
|
||||
AutomationId="AccessibilitySwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n DrawOver}"
|
||||
Subtitle="{Binding UseDrawOverDescription}"
|
||||
IsToggled="{Binding UseDrawOver}"
|
||||
IsVisible="{Binding ShowUseDrawOverToggle}"
|
||||
IsEnabled="{Binding UseAccessibility}"
|
||||
AutomationId="DrawOverSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n PasswordAutofill}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
IsVisible="{Binding SupportsiOSAutofill}"
|
||||
AutomationId="PasswordAutofillLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToPasswordAutofillCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n AppExtension}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
IsVisible="{OnPlatform iOS=True, Android=False}"
|
||||
AutomationId="AppExtensionLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToAppExtensionCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<BoxView StyleClass="settings-box-row-separator" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n AdditionalOptions}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n CopyTotpAutomatically}"
|
||||
Subtitle="{u:I18n CopyTotpAutomaticallyDescription}"
|
||||
IsToggled="{Binding CopyTotpAutomatically}"
|
||||
AutomationId="CopyTotpAutomaticallySwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n AskToAddLogin}"
|
||||
Subtitle="{u:I18n AskToAddLoginDescription}"
|
||||
IsToggled="{Binding AskToAddLogin}"
|
||||
IsVisible="{Binding SupportsAndroidAutofillServices}"
|
||||
AutomationId="AskToAddLoginSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n DefaultUriMatchDetection}"
|
||||
Subtitle="{u:I18n DefaultUriMatchDetectionDescription}"
|
||||
DisplayValue="{Binding DefaultUriMatchDetectionPickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding DefaultUriMatchDetectionPickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="DefaultUriMatchDetectionChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<StackLayout
|
||||
Spacing="0"
|
||||
Padding="16,12"
|
||||
IsVisible="{Binding SupportsAndroidAutofillServices}"
|
||||
AutomationId="BlockAutoFillView">
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToBlockAutofillUrisCommand}" />
|
||||
</StackLayout.GestureRecognizers>
|
||||
<controls:CustomLabel
|
||||
MaxLines="2"
|
||||
Text="{u:I18n BlockAutoFill}"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
LineBreakMode="TailTruncation" />
|
||||
<Label
|
||||
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch"
|
||||
Margin="0,0,0,0"/>
|
||||
</StackLayout>
|
||||
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</pages:BaseContentPage>
|
||||
37
src/App/Pages/Settings/AutofillSettingsPage.xaml.cs
Normal file
37
src/App/Pages/Settings/AutofillSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillSettingsPage : BaseContentPage
|
||||
{
|
||||
AutofillSettingsPageViewModel _vm;
|
||||
|
||||
public AutofillSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as AutofillSettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
|
||||
|
||||
Navigation.PopAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
181
src/App/Pages/Settings/AutofillSettingsPageViewModel.android.cs
Normal file
181
src/App/Pages/Settings/AutofillSettingsPageViewModel.android.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Resources;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillSettingsPageViewModel
|
||||
{
|
||||
private bool _useAutofillServices;
|
||||
private bool _useInlineAutofill;
|
||||
private bool _useAccessibility;
|
||||
private bool _useDrawOver;
|
||||
private bool _askToAddLogin;
|
||||
|
||||
public bool SupportsAndroidAutofillServices => Device.RuntimePlatform == Device.Android && _deviceActionService.SupportsAutofillServices();
|
||||
|
||||
public bool UseAutofillServices
|
||||
{
|
||||
get => _useAutofillServices;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useAutofillServices, value))
|
||||
{
|
||||
((ICommand)ToggleUseAutofillServicesCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowUseInlineAutofillToggle => _deviceActionService.SupportsInlineAutofill();
|
||||
|
||||
public bool UseInlineAutofill
|
||||
{
|
||||
get => _useInlineAutofill;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useInlineAutofill, value))
|
||||
{
|
||||
((ICommand)ToggleUseInlineAutofillCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowUseAccessibilityToggle => Device.RuntimePlatform == Device.Android;
|
||||
|
||||
public string UseAccessibilityDescription => _deviceActionService.GetAutofillAccessibilityDescription();
|
||||
|
||||
public bool UseAccessibility
|
||||
{
|
||||
get => _useAccessibility;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useAccessibility, value))
|
||||
{
|
||||
((ICommand)ToggleUseAccessibilityCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowUseDrawOverToggle => _deviceActionService.SupportsDrawOver();
|
||||
|
||||
public bool UseDrawOver
|
||||
{
|
||||
get => _useDrawOver;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useDrawOver, value))
|
||||
{
|
||||
((ICommand)ToggleUseDrawOverCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string UseDrawOverDescription => _deviceActionService.GetAutofillDrawOverDescription();
|
||||
|
||||
public bool AskToAddLogin
|
||||
{
|
||||
get => _askToAddLogin;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _askToAddLogin, value))
|
||||
{
|
||||
((ICommand)ToggleAskToAddLoginCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AsyncCommand ToggleUseAutofillServicesCommand { get; private set; }
|
||||
public AsyncCommand ToggleUseInlineAutofillCommand { get; private set; }
|
||||
public AsyncCommand ToggleUseAccessibilityCommand { get; private set; }
|
||||
public AsyncCommand ToggleUseDrawOverCommand { get; private set; }
|
||||
public AsyncCommand ToggleAskToAddLoginCommand { get; private set; }
|
||||
public ICommand GoToBlockAutofillUrisCommand { get; private set; }
|
||||
|
||||
private void InitAndroidCommands()
|
||||
{
|
||||
ToggleUseAutofillServicesCommand = CreateDefaultAsyncCommnad(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseAutofillServices()), _ => _inited);
|
||||
ToggleUseInlineAutofillCommand = CreateDefaultAsyncCommnad(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseInlineAutofillEnabledAsync()), _ => _inited);
|
||||
ToggleUseAccessibilityCommand = CreateDefaultAsyncCommnad(ToggleUseAccessibilityAsync, _ => _inited);
|
||||
ToggleUseDrawOverCommand = CreateDefaultAsyncCommnad(() => MainThread.InvokeOnMainThreadAsync(() => ToggleDrawOver()), _ => _inited);
|
||||
ToggleAskToAddLoginCommand = CreateDefaultAsyncCommnad(ToggleAskToAddLoginAsync, _ => _inited);
|
||||
GoToBlockAutofillUrisCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushAsync(new BlockAutofillUrisPage()));
|
||||
}
|
||||
|
||||
private async Task InitAndroidAutofillSettingsAsync()
|
||||
{
|
||||
_useInlineAutofill = await _stateService.GetInlineAutofillEnabledAsync() ?? true;
|
||||
|
||||
await UpdateAndroidAutofillSettingsAsync();
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(UseInlineAutofill));
|
||||
});
|
||||
}
|
||||
|
||||
private async Task UpdateAndroidAutofillSettingsAsync()
|
||||
{
|
||||
_useAutofillServices =
|
||||
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
|
||||
_useAccessibility = _autofillHandler.AutofillAccessibilityServiceRunning();
|
||||
_useDrawOver = _autofillHandler.AutofillAccessibilityOverlayPermitted();
|
||||
_askToAddLogin = await _stateService.GetAutofillDisableSavePromptAsync() != true;
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(UseAutofillServices));
|
||||
TriggerPropertyChanged(nameof(UseAccessibility));
|
||||
TriggerPropertyChanged(nameof(UseDrawOver));
|
||||
TriggerPropertyChanged(nameof(AskToAddLogin));
|
||||
});
|
||||
}
|
||||
|
||||
private void ToggleUseAutofillServices()
|
||||
{
|
||||
if (UseAutofillServices)
|
||||
{
|
||||
_deviceActionService.OpenAutofillSettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
_autofillHandler.DisableAutofillService();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleUseInlineAutofillEnabledAsync()
|
||||
{
|
||||
await _stateService.SetInlineAutofillEnabledAsync(UseInlineAutofill);
|
||||
}
|
||||
|
||||
private async Task ToggleUseAccessibilityAsync()
|
||||
{
|
||||
if (!_autofillHandler.AutofillAccessibilityServiceRunning()
|
||||
&&
|
||||
!await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText, AppResources.AccessibilityServiceDisclosure,
|
||||
AppResources.Accept, AppResources.Decline))
|
||||
{
|
||||
_useAccessibility = false;
|
||||
await MainThread.InvokeOnMainThreadAsync(() => TriggerPropertyChanged(nameof(UseAccessibility)));
|
||||
return;
|
||||
}
|
||||
_deviceActionService.OpenAccessibilitySettings();
|
||||
}
|
||||
|
||||
private void ToggleDrawOver()
|
||||
{
|
||||
if (!UseAccessibility)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_deviceActionService.OpenAccessibilityOverlayPermissionSettings();
|
||||
}
|
||||
|
||||
private async Task ToggleAskToAddLoginAsync()
|
||||
{
|
||||
await _stateService.SetAutofillDisableSavePromptAsync(!AskToAddLogin);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/App/Pages/Settings/AutofillSettingsPageViewModel.cs
Normal file
111
src/App/Pages/Settings/AutofillSettingsPageViewModel.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillSettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAutofillHandler _autofillHandler;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
|
||||
private bool _inited;
|
||||
private bool _copyTotpAutomatically;
|
||||
|
||||
public AutofillSettingsPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
|
||||
DefaultUriMatchDetectionPickerViewModel = new PickerViewModel<UriMatchType>(
|
||||
_deviceActionService,
|
||||
ServiceContainer.Resolve<ILogger>(),
|
||||
DefaultUriMatchDetectionChangingAsync,
|
||||
AppResources.DefaultUriMatchDetection,
|
||||
_ => _inited,
|
||||
ex => HandleException(ex));
|
||||
|
||||
ToggleCopyTotpAutomaticallyCommand = CreateDefaultAsyncCommnad(ToggleCopyTotpAutomaticallyAsync, _ => _inited);
|
||||
|
||||
InitAndroidCommands();
|
||||
InitIOSCommands();
|
||||
}
|
||||
|
||||
public bool CopyTotpAutomatically
|
||||
{
|
||||
get => _copyTotpAutomatically;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _copyTotpAutomatically, value))
|
||||
{
|
||||
((ICommand)ToggleCopyTotpAutomaticallyCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PickerViewModel<UriMatchType> DefaultUriMatchDetectionPickerViewModel { get; }
|
||||
|
||||
public AsyncCommand ToggleCopyTotpAutomaticallyCommand { get; private set; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await InitAndroidAutofillSettingsAsync();
|
||||
|
||||
_copyTotpAutomatically = await _stateService.GetDisableAutoTotpCopyAsync() != true;
|
||||
|
||||
await InitDefaultUriMatchDetectionPickerViewModelAsync();
|
||||
|
||||
_inited = true;
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(CopyTotpAutomatically));
|
||||
|
||||
ToggleUseAutofillServicesCommand.RaiseCanExecuteChanged();
|
||||
ToggleUseInlineAutofillCommand.RaiseCanExecuteChanged();
|
||||
ToggleUseAccessibilityCommand.RaiseCanExecuteChanged();
|
||||
ToggleUseDrawOverCommand.RaiseCanExecuteChanged();
|
||||
DefaultUriMatchDetectionPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task InitDefaultUriMatchDetectionPickerViewModelAsync()
|
||||
{
|
||||
var options = new Dictionary<UriMatchType, string>
|
||||
{
|
||||
[UriMatchType.Domain] = AppResources.BaseDomain,
|
||||
[UriMatchType.Host] = AppResources.Host,
|
||||
[UriMatchType.StartsWith] = AppResources.StartsWith,
|
||||
[UriMatchType.RegularExpression] = AppResources.RegEx,
|
||||
[UriMatchType.Exact] = AppResources.Exact,
|
||||
[UriMatchType.Never] = AppResources.Never
|
||||
};
|
||||
|
||||
var defaultUriMatchDetection = ((UriMatchType?)await _stateService.GetDefaultUriMatchAsync()) ?? UriMatchType.Domain;
|
||||
|
||||
DefaultUriMatchDetectionPickerViewModel.Init(options, defaultUriMatchDetection, UriMatchType.Domain);
|
||||
}
|
||||
|
||||
private async Task ToggleCopyTotpAutomaticallyAsync()
|
||||
{
|
||||
await _stateService.SetDisableAutoTotpCopyAsync(!CopyTotpAutomatically);
|
||||
}
|
||||
|
||||
private async Task<bool> DefaultUriMatchDetectionChangingAsync(UriMatchType type)
|
||||
{
|
||||
await _stateService.SetDefaultUriMatchAsync((int?)type);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/App/Pages/Settings/AutofillSettingsPageViewModel.ios.cs
Normal file
19
src/App/Pages/Settings/AutofillSettingsPageViewModel.ios.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Windows.Input;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillSettingsPageViewModel
|
||||
{
|
||||
public bool SupportsiOSAutofill => Device.RuntimePlatform == Device.iOS && _deviceActionService.SupportsAutofillServices();
|
||||
|
||||
public ICommand GoToPasswordAutofillCommand { get; private set; }
|
||||
public ICommand GoToAppExtensionCommand { get; private set; }
|
||||
|
||||
private void InitIOSCommands()
|
||||
{
|
||||
GoToPasswordAutofillCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillPage())));
|
||||
GoToAppExtensionCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new ExtensionPage())));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,8 @@
|
||||
<Label
|
||||
Text="{u:I18n DisablePersonalVaultExportPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="DisablePrivateVaultPolicyLabel" />
|
||||
</Frame>
|
||||
<Grid
|
||||
RowSpacing="10"
|
||||
@@ -55,7 +56,8 @@
|
||||
SelectedIndex="{Binding FileFormatSelectedIndex}"
|
||||
SelectedIndexChanged="FileFormat_Changed"
|
||||
StyleClass="box-value"
|
||||
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}" />
|
||||
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="FileFormatPicker" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
@@ -72,7 +74,8 @@
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="End"
|
||||
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
|
||||
Margin="0,0,0,10"/>
|
||||
Margin="0,0,0,10"
|
||||
AutomationId="SendTOTPCodeButton" />
|
||||
</StackLayout>
|
||||
<Grid
|
||||
StyleClass="box-row"
|
||||
@@ -96,7 +99,8 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding ExportVaultCommand}" />
|
||||
ReturnCommand="{Binding ExportVaultCommand}"
|
||||
AutomationId="MasterPasswordEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -106,7 +110,8 @@
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
IsVisible="{Binding UseOTPVerification, Converter={StaticResource inverseBool}}"/>
|
||||
IsVisible="{Binding UseOTPVerification, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="TogglePasswordVisibilityButton" />
|
||||
<Label
|
||||
Text="{u:I18n ConfirmYourIdentity}"
|
||||
StyleClass="box-footer-label"
|
||||
@@ -128,7 +133,8 @@
|
||||
Clicked="ExportVault_Clicked"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="End"
|
||||
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"/>
|
||||
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="ExportVaultButton" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:DateTimeConverter x:Key="dateTime" />
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<xct:ItemSelectedEventArgsConverter x:Key="ItemSelectedEventArgsConverter" />
|
||||
<controls:SelectionChangedEventArgsConverter x:Key="SelectionChangedEventArgsConverter" />
|
||||
<DataTemplate
|
||||
@@ -80,18 +81,38 @@
|
||||
Command="{Binding RefreshCommand}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
BackgroundColor="{DynamicResource BackgroundColor}">
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding LoginRequests}"
|
||||
ItemTemplate="{StaticResource loginRequestTemplate}"
|
||||
SelectionMode="Single"
|
||||
ExtraDataForLogging="Login requests page" >
|
||||
<controls:ExtendedCollectionView.Behaviors>
|
||||
<xct:EventToCommandBehavior
|
||||
EventName="SelectionChanged"
|
||||
Command="{Binding AnswerRequestCommand}"
|
||||
EventArgsConverter="{StaticResource SelectionChangedEventArgsConverter}" />
|
||||
</controls:ExtendedCollectionView.Behaviors>
|
||||
</controls:ExtendedCollectionView>
|
||||
<StackLayout>
|
||||
<Image
|
||||
x:Name="_emptyPlaceholder"
|
||||
Source="empty_login_requests"
|
||||
HorizontalOptions="Center"
|
||||
WidthRequest="160"
|
||||
HeightRequest="160"
|
||||
Margin="0,70,0,0"
|
||||
IsVisible="{Binding HasLoginRequests, Converter={StaticResource inverseBool}}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n NoPendingRequests}" />
|
||||
<controls:CustomLabel
|
||||
StyleClass="box-label-regular"
|
||||
Text="{u:I18n NoPendingRequests}"
|
||||
FontAttributes="{OnPlatform iOS=Bold}"
|
||||
FontWeight="500"
|
||||
HorizontalTextAlignment="Center"
|
||||
Margin="14,10,14,0"/>
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding LoginRequests}"
|
||||
ItemTemplate="{StaticResource loginRequestTemplate}"
|
||||
SelectionMode="Single"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
ExtraDataForLogging="Login requests page" >
|
||||
<controls:ExtendedCollectionView.Behaviors>
|
||||
<xct:EventToCommandBehavior
|
||||
EventName="SelectionChanged"
|
||||
Command="{Binding AnswerRequestCommand}"
|
||||
EventArgsConverter="{StaticResource SelectionChangedEventArgsConverter}" />
|
||||
</controls:ExtendedCollectionView.Behaviors>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
</RefreshView>
|
||||
<controls:IconLabelButton
|
||||
VerticalOptions="End"
|
||||
@@ -99,6 +120,7 @@
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||
Label="{u:I18n DeclineAllRequests}"
|
||||
ButtonCommand="{Binding DeclineAllRequestsCommand}"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
AutomationId="DeleteAllRequestsButton" />
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -24,6 +27,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
base.OnAppearing();
|
||||
await LoadOnAppearedAsync(_mainLayout, false, _vm.RefreshAsync, _mainContent);
|
||||
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
@@ -33,6 +38,22 @@ namespace Bit.App.Pages
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task UpdateOnThemeChanged()
|
||||
{
|
||||
await base.UpdateOnThemeChanged();
|
||||
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
private void UpdatePlaceholder()
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
_emptyPlaceholder.Source = ImageSource.FromFile(ThemeManager.UsingLightTheme ? "empty_login_requests" : "empty_login_requests_dark"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -56,16 +57,14 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _isRefreshing, value);
|
||||
}
|
||||
|
||||
public bool HasLoginRequests => LoginRequests.Any();
|
||||
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsRefreshing = true;
|
||||
LoginRequests.ReplaceRange(await _authService.GetActivePasswordlessLoginRequestsAsync());
|
||||
if (!LoginRequests.Any())
|
||||
{
|
||||
Page.Navigation.PopModalAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -74,6 +73,7 @@ namespace Bit.App.Pages
|
||||
finally
|
||||
{
|
||||
IsRefreshing = false;
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(HasLoginRequests)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,4 +136,3 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.OptionsPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:OptionsPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:OptionsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
Text="{u:I18n Theme}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_themePicker"
|
||||
ItemsSource="{Binding ThemeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding ThemeSelectedIndex}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="ThemeSelectorPicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
StyleClass="box-footer-label"
|
||||
Text="{u:I18n ThemeDescription}" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box"
|
||||
IsVisible="{Binding ShowAutoDarkThemeOptions}">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
Text="{u:I18n DefaultDarkTheme}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_autoDarkThemePicker"
|
||||
ItemsSource="{Binding AutoDarkThemeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding AutoDarkThemeSelectedIndex}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="DefaultDarkThemePicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
StyleClass="box-footer-label"
|
||||
Text="{u:I18n DefaultDarkThemeDescription}" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
Text="{u:I18n DefaultUriMatchDetection}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_uriMatchPicker"
|
||||
ItemsSource="{Binding UriMatchOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding UriMatchSelectedIndex}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="DefaultUriMatchDetectionPicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n DefaultUriMatchDetectionDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
Text="{u:I18n ClearClipboard}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_clearClipboardPicker"
|
||||
ItemsSource="{Binding ClearClipboardOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding ClearClipboardSelectedIndex}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="ClearClipboardPicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n ClearClipboardDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
Text="{u:I18n Language}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_languagePicker"
|
||||
ItemsSource="{Binding LocalesOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding SelectedLocale}"
|
||||
ItemDisplayBinding="{Binding Value}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="LanguagePicker" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n LanguageChangeRequiresAppRestart}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n CopyTotpAutomatically}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding AutoTotpCopy}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationId="CopyTotpAutomaticallyToggle" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n CopyTotpAutomaticallyDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n ShowWebsiteIcons}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Favicon}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationId="ShowWebsiteIconsToggle" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n ShowWebsiteIconsDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n AutofillService, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n AskToAddLogin}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding AutofillSavePrompt}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n AskToAddLoginDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToBlockAutofillUrisCommand}" />
|
||||
</StackLayout.GestureRecognizers>
|
||||
<Label
|
||||
Text="{u:I18n BlockAutoFill}"
|
||||
StyleClass="box-label-regular" />
|
||||
<Label
|
||||
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
@@ -1,53 +0,0 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class OptionsPage : BaseContentPage
|
||||
{
|
||||
private readonly IAutofillHandler _autofillHandler;
|
||||
private readonly OptionsPageViewModel _vm;
|
||||
|
||||
public OptionsPage()
|
||||
{
|
||||
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as OptionsPageViewModel;
|
||||
_vm.Page = this;
|
||||
_themePicker.ItemDisplayBinding = new Binding("Value");
|
||||
_autoDarkThemePicker.ItemDisplayBinding = new Binding("Value");
|
||||
_uriMatchPicker.ItemDisplayBinding = new Binding("Value");
|
||||
_clearClipboardPicker.ItemDisplayBinding = new Binding("Value");
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
_vm.ShowAndroidAutofillSettings = _autofillHandler.SupportsAutofillService();
|
||||
}
|
||||
else
|
||||
{
|
||||
_themePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_autoDarkThemePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_uriMatchPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_clearClipboardPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_languagePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
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.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class OptionsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
|
||||
private bool _autofillSavePrompt;
|
||||
private bool _favicon;
|
||||
private bool _autoTotpCopy;
|
||||
private int _clearClipboardSelectedIndex;
|
||||
private int _themeSelectedIndex;
|
||||
private int _autoDarkThemeSelectedIndex;
|
||||
private int _uriMatchSelectedIndex;
|
||||
private KeyValuePair<string, string> _selectedLocale;
|
||||
private bool _inited;
|
||||
private bool _updatingAutofill;
|
||||
private bool _showAndroidAutofillSettings;
|
||||
|
||||
public OptionsPageViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
|
||||
PageTitle = AppResources.Options;
|
||||
var iosIos = Device.RuntimePlatform == Device.iOS;
|
||||
|
||||
ClearClipboardOptions = new List<KeyValuePair<int?, string>>
|
||||
{
|
||||
new KeyValuePair<int?, string>(null, AppResources.Never),
|
||||
new KeyValuePair<int?, string>(10, AppResources.TenSeconds),
|
||||
new KeyValuePair<int?, string>(20, AppResources.TwentySeconds),
|
||||
new KeyValuePair<int?, string>(30, AppResources.ThirtySeconds),
|
||||
new KeyValuePair<int?, string>(60, AppResources.OneMinute)
|
||||
};
|
||||
if (!iosIos)
|
||||
{
|
||||
ClearClipboardOptions.Add(new KeyValuePair<int?, string>(120, AppResources.TwoMinutes));
|
||||
ClearClipboardOptions.Add(new KeyValuePair<int?, string>(300, AppResources.FiveMinutes));
|
||||
}
|
||||
ThemeOptions = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(null, AppResources.ThemeDefault),
|
||||
new KeyValuePair<string, string>(ThemeManager.Light, AppResources.Light),
|
||||
new KeyValuePair<string, string>(ThemeManager.Dark, AppResources.Dark),
|
||||
new KeyValuePair<string, string>(ThemeManager.Black, AppResources.Black),
|
||||
new KeyValuePair<string, string>(ThemeManager.Nord, AppResources.Nord),
|
||||
new KeyValuePair<string, string>(ThemeManager.SolarizedDark, AppResources.SolarizedDark),
|
||||
};
|
||||
AutoDarkThemeOptions = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(ThemeManager.Dark, AppResources.Dark),
|
||||
new KeyValuePair<string, string>(ThemeManager.Black, AppResources.Black),
|
||||
new KeyValuePair<string, string>(ThemeManager.Nord, AppResources.Nord),
|
||||
new KeyValuePair<string, string>(ThemeManager.SolarizedDark, AppResources.SolarizedDark),
|
||||
};
|
||||
UriMatchOptions = new List<KeyValuePair<UriMatchType?, string>>
|
||||
{
|
||||
new KeyValuePair<UriMatchType?, string>(UriMatchType.Domain, AppResources.BaseDomain),
|
||||
new KeyValuePair<UriMatchType?, string>(UriMatchType.Host, AppResources.Host),
|
||||
new KeyValuePair<UriMatchType?, string>(UriMatchType.StartsWith, AppResources.StartsWith),
|
||||
new KeyValuePair<UriMatchType?, string>(UriMatchType.RegularExpression, AppResources.RegEx),
|
||||
new KeyValuePair<UriMatchType?, string>(UriMatchType.Exact, AppResources.Exact),
|
||||
new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never),
|
||||
};
|
||||
LocalesOptions = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
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; }
|
||||
public List<KeyValuePair<string, string>> ThemeOptions { get; set; }
|
||||
public List<KeyValuePair<string, string>> AutoDarkThemeOptions { get; set; }
|
||||
public List<KeyValuePair<UriMatchType?, string>> UriMatchOptions { get; set; }
|
||||
public List<KeyValuePair<string, string>> LocalesOptions { get; }
|
||||
|
||||
public int ClearClipboardSelectedIndex
|
||||
{
|
||||
get => _clearClipboardSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _clearClipboardSelectedIndex, value))
|
||||
{
|
||||
SaveClipboardChangedAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ThemeSelectedIndex
|
||||
{
|
||||
get => _themeSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _themeSelectedIndex, value,
|
||||
additionalPropertyNames: new[] { nameof(ShowAutoDarkThemeOptions) })
|
||||
)
|
||||
{
|
||||
SaveThemeAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowAutoDarkThemeOptions => ThemeOptions[ThemeSelectedIndex].Key == null;
|
||||
|
||||
public int AutoDarkThemeSelectedIndex
|
||||
{
|
||||
get => _autoDarkThemeSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _autoDarkThemeSelectedIndex, value))
|
||||
{
|
||||
SaveThemeAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int UriMatchSelectedIndex
|
||||
{
|
||||
get => _uriMatchSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _uriMatchSelectedIndex, value))
|
||||
{
|
||||
SaveDefaultUriAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public KeyValuePair<string, string> SelectedLocale
|
||||
{
|
||||
get => _selectedLocale;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedLocale, value))
|
||||
{
|
||||
UpdateCurrentLocaleAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Favicon
|
||||
{
|
||||
get => _favicon;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _favicon, value))
|
||||
{
|
||||
UpdateFaviconAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutoTotpCopy
|
||||
{
|
||||
get => _autoTotpCopy;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _autoTotpCopy, value))
|
||||
{
|
||||
UpdateAutoTotpCopyAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutofillSavePrompt
|
||||
{
|
||||
get => _autofillSavePrompt;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _autofillSavePrompt, value))
|
||||
{
|
||||
UpdateAutofillSavePromptAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowAndroidAutofillSettings
|
||||
{
|
||||
get => _showAndroidAutofillSettings;
|
||||
set => SetProperty(ref _showAndroidAutofillSettings, value);
|
||||
}
|
||||
|
||||
public ICommand GoToBlockAutofillUrisCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
|
||||
|
||||
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
|
||||
|
||||
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
||||
|
||||
var theme = await _stateService.GetThemeAsync();
|
||||
ThemeSelectedIndex = ThemeOptions.FindIndex(k => k.Key == theme);
|
||||
|
||||
var autoDarkTheme = await _stateService.GetAutoDarkThemeAsync() ?? "dark";
|
||||
AutoDarkThemeSelectedIndex = AutoDarkThemeOptions.FindIndex(k => k.Key == autoDarkTheme);
|
||||
|
||||
var defaultUriMatch = await _stateService.GetDefaultUriMatchAsync();
|
||||
UriMatchSelectedIndex = defaultUriMatch == null ? 0 :
|
||||
UriMatchOptions.FindIndex(k => (int?)k.Key == defaultUriMatch);
|
||||
|
||||
var clearClipboard = await _stateService.GetClearClipboardAsync();
|
||||
ClearClipboardSelectedIndex = ClearClipboardOptions.FindIndex(k => k.Key == clearClipboard);
|
||||
|
||||
var appLocale = _stateService.GetLocale();
|
||||
SelectedLocale = appLocale == null ? LocalesOptions.First() : LocalesOptions.FirstOrDefault(kv => kv.Key == appLocale);
|
||||
|
||||
_inited = true;
|
||||
}
|
||||
|
||||
private async Task UpdateAutoTotpCopyAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
// TODO: [PS-961] Fix negative function names
|
||||
await _stateService.SetDisableAutoTotpCopyAsync(!AutoTotpCopy);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateFaviconAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
// TODO: [PS-961] Fix negative function names
|
||||
await _stateService.SetDisableFaviconAsync(!Favicon);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveClipboardChangedAsync()
|
||||
{
|
||||
if (_inited && ClearClipboardSelectedIndex > -1)
|
||||
{
|
||||
await _stateService.SetClearClipboardAsync(ClearClipboardOptions[ClearClipboardSelectedIndex].Key);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveThemeAsync()
|
||||
{
|
||||
if (_inited && ThemeSelectedIndex > -1)
|
||||
{
|
||||
await _stateService.SetThemeAsync(ThemeOptions[ThemeSelectedIndex].Key);
|
||||
await _stateService.SetAutoDarkThemeAsync(AutoDarkThemeOptions[AutoDarkThemeSelectedIndex].Key);
|
||||
ThemeManager.SetTheme(Application.Current.Resources);
|
||||
_messagingService.Send("updatedTheme");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveDefaultUriAsync()
|
||||
{
|
||||
if (_inited && UriMatchSelectedIndex > -1)
|
||||
{
|
||||
await _stateService.SetDefaultUriMatchAsync((int?)UriMatchOptions[UriMatchSelectedIndex].Key);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateAutofillSavePromptAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
// TODO: [PS-961] Fix negative function names
|
||||
await _stateService.SetAutofillDisableSavePromptAsync(!AutofillSavePrompt);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateCurrentLocaleAsync()
|
||||
{
|
||||
if (!_inited)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_stateService.SetLocale(SelectedLocale.Key);
|
||||
|
||||
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.LanguageChangeXDescription, SelectedLocale.Value), AppResources.Language, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/App/Pages/Settings/OtherSettingsPage.xaml
Normal file
72
src/App/Pages/Settings/OtherSettingsPage.xaml
Normal file
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:OtherSettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:Class="Bit.App.Pages.OtherSettingsPage"
|
||||
Title="{u:I18n Other}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:OtherSettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<u:InverseBoolConverter x:Key="inverseBoolConverter" />
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="5">
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n EnableSyncOnRefresh}"
|
||||
IsToggled="{Binding EnableSyncOnRefresh}"
|
||||
Subtitle="{u:I18n EnableSyncOnRefreshDescription}"
|
||||
AutomationId="SyncOnRefreshSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<StackLayout StyleClass="box" Margin="0,12,0,0">
|
||||
<Button
|
||||
Text="{u:I18n SyncNow}"
|
||||
Command="{Binding SyncCommand}"
|
||||
AutomationId="SyncNowButton"></Button>
|
||||
<Label
|
||||
Text="{Binding LastSyncDisplay}"
|
||||
StyleClass="text-muted, text-sm"
|
||||
HorizontalTextAlignment="Start"
|
||||
Margin="0,10"
|
||||
AutomationId="LastSyncLabel" />
|
||||
</StackLayout>
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n ClearClipboard}"
|
||||
Subtitle="{u:I18n ClearClipboardDescription}"
|
||||
DisplayValue="{Binding ClearClipboardPickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding ClearClipboardPickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="ClearClipboardChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n AllowScreenCapture}"
|
||||
IsToggled="{Binding IsScreenCaptureAllowed}"
|
||||
IsEnabled="{Binding CanToggleeScreenCaptureAllowed}"
|
||||
AutomationId="AllowScreenCaptureSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n ConnectToWatch}"
|
||||
IsToggled="{Binding ShouldConnectToWatch}"
|
||||
IsEnabled="{Binding CanToggleShouldConnectToWatch}"
|
||||
IsVisible="{OnPlatform iOS=True, Android=False}"
|
||||
AutomationId="ConnectToWatchSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
44
src/App/Pages/Settings/OtherSettingsPage.xaml.cs
Normal file
44
src/App/Pages/Settings/OtherSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class OtherSettingsPage : BaseContentPage
|
||||
{
|
||||
private OtherSettingsPageViewModel _vm;
|
||||
|
||||
public OtherSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as OtherSettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
_vm.SubscribeEvents();
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
|
||||
|
||||
Navigation.PopAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_vm.UnsubscribeEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
245
src/App/Pages/Settings/OtherSettingsPageViewModel.cs
Normal file
245
src/App/Pages/Settings/OtherSettingsPageViewModel.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class OtherSettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private const int CLEAR_CLIPBOARD_NEVER_OPTION = -1;
|
||||
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly ILocalizeService _localizeService;
|
||||
private readonly IWatchDeviceService _watchDeviceService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private string _lastSyncDisplay = "--";
|
||||
private bool _inited;
|
||||
private bool _syncOnRefresh;
|
||||
private bool _isScreenCaptureAllowed;
|
||||
private bool _shouldConnectToWatch;
|
||||
|
||||
public OtherSettingsPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||
_localizeService = ServiceContainer.Resolve<ILocalizeService>();
|
||||
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
|
||||
SyncCommand = CreateDefaultAsyncCommnad(SyncAsync, _ => _inited);
|
||||
ToggleIsScreenCaptureAllowedCommand = CreateDefaultAsyncCommnad(ToggleIsScreenCaptureAllowedAsync, _ => _inited);
|
||||
ToggleShouldConnectToWatchCommand = CreateDefaultAsyncCommnad(ToggleShouldConnectToWatchAsync, _ => _inited);
|
||||
|
||||
ClearClipboardPickerViewModel = new PickerViewModel<int>(
|
||||
_deviceActionService,
|
||||
_logger,
|
||||
OnClearClipboardChangingAsync,
|
||||
AppResources.ClearClipboard,
|
||||
_ => _inited,
|
||||
ex => HandleException(ex));
|
||||
}
|
||||
|
||||
public bool EnableSyncOnRefresh
|
||||
{
|
||||
get => _syncOnRefresh;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _syncOnRefresh, value))
|
||||
{
|
||||
UpdateSyncOnRefreshAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string LastSyncDisplay
|
||||
{
|
||||
get => $"{AppResources.LastSync} {_lastSyncDisplay}";
|
||||
set => SetProperty(ref _lastSyncDisplay, value);
|
||||
}
|
||||
|
||||
public PickerViewModel<int> ClearClipboardPickerViewModel { get; }
|
||||
|
||||
public bool IsScreenCaptureAllowed
|
||||
{
|
||||
get => _isScreenCaptureAllowed;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isScreenCaptureAllowed, value))
|
||||
{
|
||||
((ICommand)ToggleIsScreenCaptureAllowedCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanToggleeScreenCaptureAllowed => ToggleIsScreenCaptureAllowedCommand.CanExecute(null);
|
||||
|
||||
public bool ShouldConnectToWatch
|
||||
{
|
||||
get => _shouldConnectToWatch;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _shouldConnectToWatch, value))
|
||||
{
|
||||
((ICommand)ToggleShouldConnectToWatchCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanToggleShouldConnectToWatch => ToggleShouldConnectToWatchCommand.CanExecute(null);
|
||||
|
||||
public AsyncCommand SyncCommand { get; }
|
||||
public AsyncCommand ToggleIsScreenCaptureAllowedCommand { get; }
|
||||
public AsyncCommand ToggleShouldConnectToWatchCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await SetLastSyncAsync();
|
||||
|
||||
EnableSyncOnRefresh = await _stateService.GetSyncOnRefreshAsync();
|
||||
|
||||
await InitClearClipboardAsync();
|
||||
|
||||
_isScreenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
|
||||
_shouldConnectToWatch = await _stateService.GetShouldConnectToWatchAsync();
|
||||
|
||||
_inited = true;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(IsScreenCaptureAllowed));
|
||||
TriggerPropertyChanged(nameof(ShouldConnectToWatch));
|
||||
SyncCommand.RaiseCanExecuteChanged();
|
||||
ClearClipboardPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
ToggleIsScreenCaptureAllowedCommand.RaiseCanExecuteChanged();
|
||||
ToggleShouldConnectToWatchCommand.RaiseCanExecuteChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task InitClearClipboardAsync()
|
||||
{
|
||||
var clearClipboardOptions = new Dictionary<int, string>
|
||||
{
|
||||
[CLEAR_CLIPBOARD_NEVER_OPTION] = AppResources.Never,
|
||||
[10] = AppResources.TenSeconds,
|
||||
[20] = AppResources.TwentySeconds,
|
||||
[30] = AppResources.ThirtySeconds,
|
||||
[60] = AppResources.OneMinute
|
||||
};
|
||||
if (Device.RuntimePlatform != Device.iOS)
|
||||
{
|
||||
clearClipboardOptions.Add(120, AppResources.TwoMinutes);
|
||||
clearClipboardOptions.Add(300, AppResources.FiveMinutes);
|
||||
}
|
||||
|
||||
var clearClipboard = await _stateService.GetClearClipboardAsync() ?? CLEAR_CLIPBOARD_NEVER_OPTION;
|
||||
|
||||
ClearClipboardPickerViewModel.Init(clearClipboardOptions, clearClipboard, CLEAR_CLIPBOARD_NEVER_OPTION);
|
||||
}
|
||||
|
||||
public async Task UpdateSyncOnRefreshAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
await _stateService.SetSyncOnRefreshAsync(_syncOnRefresh);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetLastSyncAsync()
|
||||
{
|
||||
var last = await _syncService.GetLastSyncAsync();
|
||||
if (last is null)
|
||||
{
|
||||
LastSyncDisplay = AppResources.Never;
|
||||
return;
|
||||
}
|
||||
|
||||
var localDate = last.Value.ToLocalTime();
|
||||
LastSyncDisplay = string.Format("{0} {1}",
|
||||
_localizeService.GetLocaleShortDate(localDate),
|
||||
_localizeService.GetLocaleShortTime(localDate));
|
||||
}
|
||||
|
||||
public async Task SyncAsync()
|
||||
{
|
||||
if (!await HasConnectivityAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
|
||||
await _syncService.SyncPasswordlessLoginRequestsAsync();
|
||||
var success = await _syncService.FullSyncAsync(true);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (!success)
|
||||
{
|
||||
await Page.DisplayAlert(null, AppResources.SyncingFailed, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
await SetLastSyncAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete);
|
||||
}
|
||||
|
||||
private async Task<bool> OnClearClipboardChangingAsync(int optionKey)
|
||||
{
|
||||
await _stateService.SetClearClipboardAsync(optionKey == CLEAR_CLIPBOARD_NEVER_OPTION ? (int?)null : optionKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ToggleIsScreenCaptureAllowedAsync()
|
||||
{
|
||||
if (IsScreenCaptureAllowed
|
||||
&&
|
||||
!await Page.DisplayAlert(AppResources.AllowScreenCapture, AppResources.AreYouSureYouWantToEnableScreenCapture, AppResources.Yes, AppResources.No))
|
||||
{
|
||||
_isScreenCaptureAllowed = !IsScreenCaptureAllowed;
|
||||
TriggerPropertyChanged(nameof(IsScreenCaptureAllowed));
|
||||
return;
|
||||
}
|
||||
|
||||
await _stateService.SetScreenCaptureAllowedAsync(IsScreenCaptureAllowed);
|
||||
await _deviceActionService.SetScreenCaptureAllowedAsync();
|
||||
}
|
||||
|
||||
private async Task ToggleShouldConnectToWatchAsync()
|
||||
{
|
||||
await _watchDeviceService.SetShouldConnectToWatchAsync(ShouldConnectToWatch);
|
||||
}
|
||||
|
||||
private void ToggleIsScreenCaptureAllowedCommand_CanExecuteChanged(object sender, EventArgs e)
|
||||
{
|
||||
TriggerPropertyChanged(nameof(CanToggleeScreenCaptureAllowed));
|
||||
}
|
||||
|
||||
private void ToggleShouldConnectToWatchCommand_CanExecuteChanged(object sender, EventArgs e)
|
||||
{
|
||||
TriggerPropertyChanged(nameof(CanToggleShouldConnectToWatch));
|
||||
}
|
||||
|
||||
internal void SubscribeEvents()
|
||||
{
|
||||
ToggleIsScreenCaptureAllowedCommand.CanExecuteChanged += ToggleIsScreenCaptureAllowedCommand_CanExecuteChanged;
|
||||
ToggleShouldConnectToWatchCommand.CanExecuteChanged += ToggleShouldConnectToWatchCommand_CanExecuteChanged;
|
||||
}
|
||||
|
||||
internal void UnsubscribeEvents()
|
||||
{
|
||||
ToggleIsScreenCaptureAllowedCommand.CanExecuteChanged -= ToggleIsScreenCaptureAllowedCommand_CanExecuteChanged;
|
||||
ToggleShouldConnectToWatchCommand.CanExecuteChanged -= ToggleShouldConnectToWatchCommand_CanExecuteChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
178
src/App/Pages/Settings/SecuritySettingsPage.xaml
Normal file
178
src/App/Pages/Settings/SecuritySettingsPage.xaml
Normal file
@@ -0,0 +1,178 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:SecuritySettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:Class="Bit.App.Pages.SecuritySettingsPage"
|
||||
Title="{u:I18n AccountSecurity}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SecuritySettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<u:IsNotNullConverter x:Key="isNotNullConverter" />
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="5">
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n ApproveLoginRequests}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices}"
|
||||
IsToggled="{Binding UseThisDeviceToApproveLoginRequests}"
|
||||
AutomationId="ApproveLoginRequestsMadeFromOtherDevicesSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n PendingLogInRequests}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
IsVisible="{Binding UseThisDeviceToApproveLoginRequests}"
|
||||
AutomationId="PendingLogInRequestsLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToPendingLogInRequestsCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<BoxView StyleClass="settings-box-row-separator" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n UnlockOptions}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{Binding UnlockWithBiometricsTitle}"
|
||||
IsToggled="{Binding CanUnlockWithBiometrics}"
|
||||
IsVisible="{Binding UnlockWithBiometricsTitle, Converter={StaticResource isNotNullConverter}}"
|
||||
AutomationId="CanUnlockWithBiometricsSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n UnlockWithPIN}"
|
||||
IsToggled="{Binding CanUnlockWithPin}"
|
||||
AutomationId="CanUnlockWithPinSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<BoxView StyleClass="settings-box-row-separator" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n SessionTimeout}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<Frame
|
||||
IsVisible="{Binding ShowVaultTimeoutPolicyInfo}"
|
||||
Padding="10"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}"
|
||||
AutomationId="VaultTimeoutPolicyLabel"
|
||||
Margin="16,5">
|
||||
<Label
|
||||
Text="{Binding VaultTimeoutPolicyDescription}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n SessionTimeout}"
|
||||
DisplayValue="{Binding VaultTimeoutPickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding VaultTimeoutPickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="VaultTimeoutChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<controls:BaseSettingItemView
|
||||
Title="{u:I18n Custom}"
|
||||
IsVisible="{Binding ShowCustomVaultTimeoutPicker}"
|
||||
AutomationId="CustomVaultTimeoutChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
ControlTemplate="{StaticResource SettingControlTemplate}">
|
||||
<TimePicker Time="{Binding CustomVaultTimeoutTime}" Format="HH:mm"
|
||||
FontSize="Small"
|
||||
TextColor="{DynamicResource MutedColor}"
|
||||
StyleClass="list-sub" Margin="-5"
|
||||
HorizontalOptions="End"
|
||||
ios:TimePicker.UpdateMode="WhenFinished"
|
||||
AutomationId="SettingCustomVaultTimeoutPicker"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{Binding CustomVaultTimeoutTimeVerbalized}"/>
|
||||
</controls:BaseSettingItemView>
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n SessionTimeoutAction}"
|
||||
DisplayValue="{Binding VaultTimeoutActionPickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding VaultTimeoutActionPickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="VaultTimeoutActionChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<BoxView StyleClass="settings-box-row-separator" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n Other}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n AccountFingerprintPhrase}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="AccountFingerprintPhraseLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding ShowAccountFingerprintPhraseCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n TwoStepLogin}"
|
||||
GoToLinkCommand="{Binding GoToTwoStepLoginCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="TwoStepLoginLinkItemView" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n ChangeMasterPassword}"
|
||||
GoToLinkCommand="{Binding GoToChangeMasterPasswordCommand}"
|
||||
IsVisible="{Binding ShowChangeMasterPassword}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="ChangeMasterPasswordLinkItemView" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n LockNow}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="LockNowLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding LockCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n LogOut}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="LogOutLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding LogOutCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n DeleteAccount}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="DeleteAccountLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding DeleteAccountCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</pages:BaseContentPage>
|
||||
37
src/App/Pages/Settings/SecuritySettingsPage.xaml.cs
Normal file
37
src/App/Pages/Settings/SecuritySettingsPage.xaml.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SecuritySettingsPage : BaseContentPage
|
||||
{
|
||||
private SecuritySettingsPageViewModel _vm;
|
||||
|
||||
public SecuritySettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SecuritySettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
|
||||
|
||||
Navigation.PopAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
547
src/App/Pages/Settings/SecuritySettingsPageViewModel.cs
Normal file
547
src/App/Pages/Settings/SecuritySettingsPageViewModel.cs
Normal file
@@ -0,0 +1,547 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Pages.Accounts;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SecuritySettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private const int NEVER_SESSION_TIMEOUT_VALUE = -2;
|
||||
private const int CUSTOM_VAULT_TIMEOUT_VALUE = -100;
|
||||
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly IBiometricService _biometricsService;
|
||||
private readonly IUserPinService _userPinService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private bool _inited;
|
||||
private bool _useThisDeviceToApproveLoginRequests;
|
||||
private bool _supportsBiometric, _canUnlockWithBiometrics;
|
||||
private bool _canUnlockWithPin;
|
||||
private bool _hasMasterPassword;
|
||||
private int? _maximumVaultTimeoutPolicy;
|
||||
private string _vaultTimeoutActionPolicy;
|
||||
private TimeSpan? _customVaultTimeoutTime;
|
||||
|
||||
public SecuritySettingsPageViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||
_biometricsService = ServiceContainer.Resolve<IBiometricService>();
|
||||
_userPinService = ServiceContainer.Resolve<IUserPinService>();
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>();
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>();
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
|
||||
VaultTimeoutPickerViewModel = new PickerViewModel<int>(
|
||||
_deviceActionService,
|
||||
_logger,
|
||||
OnVaultTimeoutChangingAsync,
|
||||
AppResources.SessionTimeout,
|
||||
_ => _inited,
|
||||
ex => HandleException(ex));
|
||||
VaultTimeoutPickerViewModel.SetAfterSelectionChanged(_ => MainThread.InvokeOnMainThreadAsync(TriggerUpdateCustomVaultTimeoutPicker));
|
||||
|
||||
VaultTimeoutActionPickerViewModel = new PickerViewModel<VaultTimeoutAction>(
|
||||
_deviceActionService,
|
||||
_logger,
|
||||
OnVaultTimeoutActionChangingAsync,
|
||||
AppResources.SessionTimeoutAction,
|
||||
_ => _inited && !HasVaultTimeoutActionPolicy,
|
||||
ex => HandleException(ex));
|
||||
|
||||
ToggleUseThisDeviceToApproveLoginRequestsCommand = CreateDefaultAsyncCommnad(ToggleUseThisDeviceToApproveLoginRequestsAsync, _ => _inited);
|
||||
GoToPendingLogInRequestsCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new LoginPasswordlessRequestsListPage())));
|
||||
ToggleCanUnlockWithBiometricsCommand = CreateDefaultAsyncCommnad(ToggleCanUnlockWithBiometricsAsync, _ => _inited);
|
||||
ToggleCanUnlockWithPinCommand = CreateDefaultAsyncCommnad(ToggleCanUnlockWithPinAsync, _ => _inited);
|
||||
ShowAccountFingerprintPhraseCommand = CreateDefaultAsyncCommnad(ShowAccountFingerprintPhraseAsync);
|
||||
GoToTwoStepLoginCommand = CreateDefaultAsyncCommnad(() => GoToWebVaultSettingsAsync(AppResources.TwoStepLoginDescriptionLong, AppResources.ContinueToWebApp));
|
||||
GoToChangeMasterPasswordCommand = CreateDefaultAsyncCommnad(() => GoToWebVaultSettingsAsync(AppResources.ChangeMasterPasswordDescriptionLong, AppResources.ContinueToWebApp));
|
||||
LockCommand = CreateDefaultAsyncCommnad(() => _vaultTimeoutService.LockAsync(true, true));
|
||||
LogOutCommand = CreateDefaultAsyncCommnad(LogOutAsync);
|
||||
DeleteAccountCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage())));
|
||||
}
|
||||
|
||||
public bool UseThisDeviceToApproveLoginRequests
|
||||
{
|
||||
get => _useThisDeviceToApproveLoginRequests;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useThisDeviceToApproveLoginRequests, value))
|
||||
{
|
||||
((ICommand)ToggleUseThisDeviceToApproveLoginRequestsCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string UnlockWithBiometricsTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_supportsBiometric)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var biometricName = AppResources.Biometrics;
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
biometricName = _deviceActionService.SupportsFaceBiometric()
|
||||
? AppResources.FaceID
|
||||
: AppResources.TouchID;
|
||||
}
|
||||
|
||||
return string.Format(AppResources.UnlockWith, biometricName);
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanUnlockWithBiometrics
|
||||
{
|
||||
get => _canUnlockWithBiometrics;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _canUnlockWithBiometrics, value))
|
||||
{
|
||||
((ICommand)ToggleCanUnlockWithBiometricsCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanUnlockWithPin
|
||||
{
|
||||
get => _canUnlockWithPin;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _canUnlockWithPin, value))
|
||||
{
|
||||
((ICommand)ToggleCanUnlockWithPinCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TimeSpan? CustomVaultTimeoutTime
|
||||
{
|
||||
get => _customVaultTimeoutTime;
|
||||
set
|
||||
{
|
||||
var oldValue = _customVaultTimeoutTime;
|
||||
|
||||
if (SetProperty(ref _customVaultTimeoutTime, value, additionalPropertyNames: new string[] { nameof(CustomVaultTimeoutTimeVerbalized) }) && value.HasValue)
|
||||
{
|
||||
UpdateVaultTimeoutAsync((int)value.Value.TotalMinutes)
|
||||
.FireAndForget(ex =>
|
||||
{
|
||||
HandleException(ex);
|
||||
MainThread.BeginInvokeOnMainThread(() => SetProperty(ref _customVaultTimeoutTime, oldValue));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string CustomVaultTimeoutTimeVerbalized => CustomVaultTimeoutTime?.Verbalize(A11yExtensions.TimeSpanVerbalizationMode.HoursAndMinutes);
|
||||
|
||||
public bool ShowCustomVaultTimeoutPicker => VaultTimeoutPickerViewModel.SelectedKey == CUSTOM_VAULT_TIMEOUT_VALUE;
|
||||
|
||||
public bool ShowVaultTimeoutPolicyInfo => _maximumVaultTimeoutPolicy.HasValue || HasVaultTimeoutActionPolicy;
|
||||
|
||||
public string VaultTimeoutPolicyDescription
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!ShowVaultTimeoutPolicyInfo)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
static string LocalizeTimeoutAction(string actionPolicy)
|
||||
{
|
||||
return actionPolicy == Policy.ACTION_LOCK ? AppResources.Lock : AppResources.LogOut;
|
||||
};
|
||||
|
||||
if (!_maximumVaultTimeoutPolicy.HasValue)
|
||||
{
|
||||
return string.Format(AppResources.VaultTimeoutActionPolicyInEffect, LocalizeTimeoutAction(_vaultTimeoutActionPolicy));
|
||||
}
|
||||
|
||||
var hours = Math.Floor((float)_maximumVaultTimeoutPolicy / 60);
|
||||
var minutes = _maximumVaultTimeoutPolicy % 60;
|
||||
|
||||
return string.IsNullOrWhiteSpace(_vaultTimeoutActionPolicy)
|
||||
? string.Format(AppResources.VaultTimeoutPolicyInEffect, hours, minutes)
|
||||
: string.Format(AppResources.VaultTimeoutPolicyWithActionInEffect, hours, minutes, LocalizeTimeoutAction(_vaultTimeoutActionPolicy));
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowChangeMasterPassword { get; private set; }
|
||||
|
||||
private bool IsVaultTimeoutActionLockAllowed => _hasMasterPassword || _canUnlockWithBiometrics || _canUnlockWithPin;
|
||||
|
||||
private int? CurrentVaultTimeout => GetRawVaultTimeoutFrom(VaultTimeoutPickerViewModel.SelectedKey);
|
||||
|
||||
private bool IncludeLinksWithSubscriptionInfo => Device.RuntimePlatform != Device.iOS;
|
||||
|
||||
private bool HasVaultTimeoutActionPolicy => !string.IsNullOrEmpty(_vaultTimeoutActionPolicy);
|
||||
|
||||
public PickerViewModel<int> VaultTimeoutPickerViewModel { get; }
|
||||
public PickerViewModel<VaultTimeoutAction> VaultTimeoutActionPickerViewModel { get; }
|
||||
|
||||
public AsyncCommand ToggleUseThisDeviceToApproveLoginRequestsCommand { get; }
|
||||
public ICommand GoToPendingLogInRequestsCommand { get; }
|
||||
public AsyncCommand ToggleCanUnlockWithBiometricsCommand { get; }
|
||||
public AsyncCommand ToggleCanUnlockWithPinCommand { get; }
|
||||
public ICommand ShowAccountFingerprintPhraseCommand { get; }
|
||||
public ICommand GoToTwoStepLoginCommand { get; }
|
||||
public ICommand GoToChangeMasterPasswordCommand { get; }
|
||||
public ICommand LockCommand { get; }
|
||||
public ICommand LogOutCommand { get; }
|
||||
public ICommand DeleteAccountCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var decryptionOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
// set default true for backwards compatibility
|
||||
_hasMasterPassword = decryptionOptions?.HasMasterPassword ?? true;
|
||||
_useThisDeviceToApproveLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
|
||||
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
|
||||
_canUnlockWithBiometrics = await _vaultTimeoutService.IsBiometricLockSetAsync();
|
||||
_canUnlockWithPin = await _vaultTimeoutService.GetPinLockTypeAsync() != Core.Services.PinLockType.Disabled;
|
||||
|
||||
await LoadPoliciesAsync();
|
||||
await InitVaultTimeoutPickerAsync();
|
||||
await InitVaultTimeoutActionPickerAsync();
|
||||
|
||||
ShowChangeMasterPassword = IncludeLinksWithSubscriptionInfo && await _userVerificationService.HasMasterPasswordAsync();
|
||||
|
||||
_inited = true;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(UseThisDeviceToApproveLoginRequests));
|
||||
TriggerPropertyChanged(nameof(UnlockWithBiometricsTitle));
|
||||
TriggerPropertyChanged(nameof(CanUnlockWithBiometrics));
|
||||
TriggerPropertyChanged(nameof(CanUnlockWithPin));
|
||||
TriggerPropertyChanged(nameof(ShowVaultTimeoutPolicyInfo));
|
||||
TriggerPropertyChanged(nameof(VaultTimeoutPolicyDescription));
|
||||
TriggerPropertyChanged(nameof(ShowChangeMasterPassword));
|
||||
TriggerUpdateCustomVaultTimeoutPicker();
|
||||
ToggleUseThisDeviceToApproveLoginRequestsCommand.RaiseCanExecuteChanged();
|
||||
ToggleCanUnlockWithBiometricsCommand.RaiseCanExecuteChanged();
|
||||
ToggleCanUnlockWithPinCommand.RaiseCanExecuteChanged();
|
||||
VaultTimeoutPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
VaultTimeoutActionPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LoadPoliciesAsync()
|
||||
{
|
||||
if (!await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var maximumVaultTimeoutPolicy = await _policyService.FirstOrDefault(PolicyType.MaximumVaultTimeout);
|
||||
_maximumVaultTimeoutPolicy = maximumVaultTimeoutPolicy?.GetInt(Policy.MINUTES_KEY);
|
||||
_vaultTimeoutActionPolicy = maximumVaultTimeoutPolicy?.GetString(Policy.ACTION_KEY);
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(VaultTimeoutActionPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged);
|
||||
}
|
||||
|
||||
private async Task InitVaultTimeoutPickerAsync()
|
||||
{
|
||||
var options = new Dictionary<int, string>
|
||||
{
|
||||
[0] = AppResources.Immediately,
|
||||
[1] = AppResources.OneMinute,
|
||||
[5] = AppResources.FiveMinutes,
|
||||
[15] = AppResources.FifteenMinutes,
|
||||
[30] = AppResources.ThirtyMinutes,
|
||||
[60] = AppResources.OneHour,
|
||||
[240] = AppResources.FourHours,
|
||||
[-1] = AppResources.OnRestart,
|
||||
[NEVER_SESSION_TIMEOUT_VALUE] = AppResources.Never
|
||||
};
|
||||
|
||||
if (_maximumVaultTimeoutPolicy.HasValue)
|
||||
{
|
||||
options = options.Where(t => t.Key >= 0 && t.Key <= _maximumVaultTimeoutPolicy.Value)
|
||||
.ToDictionary(v => v.Key, v => v.Value);
|
||||
}
|
||||
|
||||
options.Add(CUSTOM_VAULT_TIMEOUT_VALUE, AppResources.Custom);
|
||||
|
||||
var vaultTimeout = await _vaultTimeoutService.GetVaultTimeout() ?? NEVER_SESSION_TIMEOUT_VALUE;
|
||||
VaultTimeoutPickerViewModel.Init(options, vaultTimeout, CUSTOM_VAULT_TIMEOUT_VALUE, false);
|
||||
|
||||
if (VaultTimeoutPickerViewModel.SelectedKey == CUSTOM_VAULT_TIMEOUT_VALUE)
|
||||
{
|
||||
_customVaultTimeoutTime = TimeSpan.FromMinutes(vaultTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitVaultTimeoutActionPickerAsync()
|
||||
{
|
||||
var options = new Dictionary<VaultTimeoutAction, string>();
|
||||
if (IsVaultTimeoutActionLockAllowed)
|
||||
{
|
||||
options.Add(VaultTimeoutAction.Lock, AppResources.Lock);
|
||||
}
|
||||
options.Add(VaultTimeoutAction.Logout, AppResources.LogOut);
|
||||
|
||||
var timeoutAction = await _vaultTimeoutService.GetVaultTimeoutAction() ?? VaultTimeoutAction.Lock;
|
||||
if (!IsVaultTimeoutActionLockAllowed && timeoutAction == VaultTimeoutAction.Lock)
|
||||
{
|
||||
timeoutAction = VaultTimeoutAction.Logout;
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, VaultTimeoutAction.Logout);
|
||||
}
|
||||
|
||||
VaultTimeoutActionPickerViewModel.Init(options, timeoutAction, IsVaultTimeoutActionLockAllowed ? VaultTimeoutAction.Lock : VaultTimeoutAction.Logout);
|
||||
}
|
||||
|
||||
private async Task ToggleUseThisDeviceToApproveLoginRequestsAsync()
|
||||
{
|
||||
if (UseThisDeviceToApproveLoginRequests
|
||||
&&
|
||||
!await Page.DisplayAlert(AppResources.ApproveLoginRequests, AppResources.UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices, AppResources.Yes, AppResources.No))
|
||||
{
|
||||
_useThisDeviceToApproveLoginRequests = !UseThisDeviceToApproveLoginRequests;
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(UseThisDeviceToApproveLoginRequests)));
|
||||
return;
|
||||
}
|
||||
|
||||
await _stateService.SetApprovePasswordlessLoginsAsync(UseThisDeviceToApproveLoginRequests);
|
||||
|
||||
if (!UseThisDeviceToApproveLoginRequests || await _pushNotificationService.AreNotificationsSettingsEnabledAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var openAppSettingsResult = await _platformUtilsService.ShowDialogAsync(
|
||||
AppResources.ReceivePushNotificationsForNewLoginRequests,
|
||||
string.Empty,
|
||||
AppResources.Settings,
|
||||
AppResources.NoThanks
|
||||
);
|
||||
if (openAppSettingsResult)
|
||||
{
|
||||
_deviceActionService.OpenAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleCanUnlockWithBiometricsAsync()
|
||||
{
|
||||
if (!_canUnlockWithBiometrics)
|
||||
{
|
||||
await UpdateVaultTimeoutActionIfNeededAsync();
|
||||
await _biometricsService.SetCanUnlockWithBiometricsAsync(CanUnlockWithBiometrics);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_supportsBiometric
|
||||
||
|
||||
!await _platformUtilsService.AuthenticateBiometricAsync(null, Device.RuntimePlatform == Device.Android ? "." : null))
|
||||
{
|
||||
_canUnlockWithBiometrics = false;
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));
|
||||
return;
|
||||
}
|
||||
|
||||
await _biometricsService.SetCanUnlockWithBiometricsAsync(CanUnlockWithBiometrics);
|
||||
}
|
||||
|
||||
public async Task ToggleCanUnlockWithPinAsync()
|
||||
{
|
||||
if (!CanUnlockWithPin)
|
||||
{
|
||||
await _vaultTimeoutService.ClearAsync();
|
||||
await UpdateVaultTimeoutActionIfNeededAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var newPin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN,
|
||||
AppResources.SetPINDescription, null, AppResources.Submit, AppResources.Cancel, true);
|
||||
if (string.IsNullOrWhiteSpace(newPin))
|
||||
{
|
||||
_canUnlockWithPin = false;
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithPin)));
|
||||
return;
|
||||
}
|
||||
|
||||
var requireMasterPasswordOnRestart = await _userVerificationService.HasMasterPasswordAsync()
|
||||
&&
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PINRequireMasterPasswordRestart,
|
||||
AppResources.UnlockWithPIN,
|
||||
AppResources.Yes,
|
||||
AppResources.No);
|
||||
|
||||
await _userPinService.SetupPinAsync(newPin, requireMasterPasswordOnRestart);
|
||||
}
|
||||
|
||||
private async Task UpdateVaultTimeoutActionIfNeededAsync()
|
||||
{
|
||||
if (IsVaultTimeoutActionLockAllowed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VaultTimeoutActionPickerViewModel.Select(VaultTimeoutAction.Logout);
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, VaultTimeoutAction.Logout);
|
||||
_deviceActionService.Toast(AppResources.VaultTimeoutActionChangedToLogOut);
|
||||
}
|
||||
|
||||
private async Task<bool> OnVaultTimeoutChangingAsync(int newTimeout)
|
||||
{
|
||||
if (newTimeout == NEVER_SESSION_TIMEOUT_VALUE
|
||||
&&
|
||||
!await _platformUtilsService.ShowDialogAsync(AppResources.NeverLockWarning, AppResources.Warning, AppResources.Yes, AppResources.Cancel))
|
||||
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newTimeout == CUSTOM_VAULT_TIMEOUT_VALUE)
|
||||
{
|
||||
_customVaultTimeoutTime = TimeSpan.FromMinutes(0);
|
||||
}
|
||||
|
||||
return await UpdateVaultTimeoutAsync(newTimeout);
|
||||
}
|
||||
|
||||
private async Task<bool> UpdateVaultTimeoutAsync(int newTimeout)
|
||||
{
|
||||
var rawTimeout = GetRawVaultTimeoutFrom(newTimeout);
|
||||
|
||||
if (rawTimeout > _maximumVaultTimeoutPolicy)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutToLarge, AppResources.Warning);
|
||||
VaultTimeoutPickerViewModel.Select(_maximumVaultTimeoutPolicy.Value, false);
|
||||
|
||||
if (VaultTimeoutPickerViewModel.SelectedKey == CUSTOM_VAULT_TIMEOUT_VALUE)
|
||||
{
|
||||
_customVaultTimeoutTime = TimeSpan.FromMinutes(_maximumVaultTimeoutPolicy.Value);
|
||||
}
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(TriggerUpdateCustomVaultTimeoutPicker);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(rawTimeout, VaultTimeoutActionPickerViewModel.SelectedKey);
|
||||
|
||||
await _cryptoService.RefreshKeysAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TriggerUpdateCustomVaultTimeoutPicker()
|
||||
{
|
||||
TriggerPropertyChanged(nameof(ShowCustomVaultTimeoutPicker));
|
||||
TriggerPropertyChanged(nameof(CustomVaultTimeoutTime));
|
||||
}
|
||||
|
||||
private int? GetRawVaultTimeoutFrom(int vaultTimeoutPickerKey)
|
||||
{
|
||||
if (vaultTimeoutPickerKey == NEVER_SESSION_TIMEOUT_VALUE)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (vaultTimeoutPickerKey == CUSTOM_VAULT_TIMEOUT_VALUE
|
||||
&&
|
||||
CustomVaultTimeoutTime.HasValue)
|
||||
{
|
||||
return (int)CustomVaultTimeoutTime.Value.TotalMinutes;
|
||||
}
|
||||
|
||||
return vaultTimeoutPickerKey;
|
||||
}
|
||||
|
||||
private async Task<bool> OnVaultTimeoutActionChangingAsync(VaultTimeoutAction timeoutActionKey)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_vaultTimeoutActionPolicy))
|
||||
{
|
||||
// do nothing if we have a policy set
|
||||
return false;
|
||||
}
|
||||
|
||||
if (timeoutActionKey == VaultTimeoutAction.Logout
|
||||
&&
|
||||
!await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutLogOutConfirmation, AppResources.Warning, AppResources.Yes, AppResources.Cancel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, timeoutActionKey);
|
||||
_messagingService.Send(AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ShowAccountFingerprintPhraseAsync()
|
||||
{
|
||||
List<string> fingerprint;
|
||||
try
|
||||
{
|
||||
fingerprint = await _cryptoService.GetFingerprintAsync(await _stateService.GetActiveUserIdAsync());
|
||||
}
|
||||
catch (Exception e) when (e.Message == "No public key available.")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var phrase = string.Join("-", fingerprint);
|
||||
var text = $"{AppResources.YourAccountsFingerprint}:\n\n{phrase}";
|
||||
|
||||
var learnMore = await _platformUtilsService.ShowDialogAsync(text, AppResources.FingerprintPhrase,
|
||||
AppResources.LearnMore, AppResources.Close);
|
||||
if (learnMore)
|
||||
{
|
||||
_platformUtilsService.LaunchUri(ExternalLinksConstants.HELP_FINGERPRINT_PHRASE);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GoToWebVaultSettingsAsync(string dialogText, string dialogTitle)
|
||||
{
|
||||
if (await _platformUtilsService.ShowDialogAsync(dialogText, dialogTitle, AppResources.Continue, AppResources.Cancel))
|
||||
{
|
||||
_platformUtilsService.LaunchUri(string.Format(ExternalLinksConstants.WEB_VAULT_SETTINGS_FORMAT, _environmentService.GetWebVaultUrl()));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogOutAsync()
|
||||
{
|
||||
if (await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation, AppResources.LogOut, AppResources.Yes, AppResources.Cancel))
|
||||
{
|
||||
_messagingService.Send(AccountsManagerMessageCommands.LOGOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public interface ISettingsPageListItem
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -6,118 +6,27 @@
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:SettingsPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
Title="{u:I18n Settings}"
|
||||
x:Name="_page">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:StringHasValueConverter x:Key="stringHasValue" />
|
||||
|
||||
<DataTemplate
|
||||
x:Key="regularTemplate"
|
||||
x:DataType="pages:SettingsPageListItem">
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform">
|
||||
<Frame
|
||||
IsVisible="{Binding UseFrame}"
|
||||
Padding="10"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{Binding Name, Mode=OneWay}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<controls:CustomLabel IsVisible="{Binding UseFrame, Converter={StaticResource inverseBool}}"
|
||||
Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="{Binding LineBreakMode}"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
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"
|
||||
AutomationId="{Binding AutomationIdSettingStatus}" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
<DataTemplate
|
||||
x:Key="timePickerTemplate"
|
||||
x:DataType="pages:SettingsPageListItem">
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform">
|
||||
<Frame
|
||||
IsVisible="{Binding UseFrame}"
|
||||
Padding="10"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}"
|
||||
AutomationId="SettingActivePolicyTextLabel">
|
||||
<Label
|
||||
Text="{Binding Name, Mode=OneWay}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<Label IsVisible="{Binding UseFrame, Converter={StaticResource inverseBool}}"
|
||||
Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="{Binding LineBreakMode}"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title"/>
|
||||
<TimePicker Time="{Binding Time}" Format="HH:mm"
|
||||
PropertyChanged="OnTimePickerPropertyChanged"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="Center"
|
||||
FontSize="Small"
|
||||
TextColor="{Binding SubLabelColor}"
|
||||
StyleClass="list-sub" Margin="-5"
|
||||
AutomationId="SettingCustomVaultTimeoutPicker" />
|
||||
<controls:ExtendedStackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="ActivateTimePicker"/>
|
||||
</controls:ExtendedStackLayout.GestureRecognizers>
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate
|
||||
x:Key="headerTemplate"
|
||||
x:DataType="pages:SettingsPageHeaderListItem">
|
||||
<StackLayout
|
||||
Padding="0" Spacing="0" VerticalOptions="FillAndExpand"
|
||||
StyleClass="list-row-header-container, list-row-header-container-platform">
|
||||
<BoxView
|
||||
StyleClass="list-section-separator-top, list-section-separator-top-platform" />
|
||||
<StackLayout StyleClass="list-row-header, list-row-header-platform">
|
||||
<Label
|
||||
Text="{Binding Title}"
|
||||
StyleClass="list-header, list-header-platform" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" />
|
||||
<StackLayout BindableLayout.ItemsSource="{Binding SettingsItems}">
|
||||
<BindableLayout.ItemTemplate>
|
||||
<DataTemplate x:DataType="pages:SettingsPageListItem">
|
||||
<StackLayout>
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding BindingContext.ExecuteSettingItemCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"/>
|
||||
</StackLayout.GestureRecognizers>
|
||||
<controls:CustomLabel
|
||||
Text="{Binding Name}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="{Binding AutomationId}" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
</DataTemplate>
|
||||
|
||||
<pages:SettingsPageListItemSelector
|
||||
x:Key="listItemDataTemplateSelector"
|
||||
HeaderTemplate="{StaticResource headerTemplate}"
|
||||
RegularTemplate="{StaticResource regularTemplate}"
|
||||
TimePickerTemplate="{StaticResource timePickerTemplate}" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding GroupedItems}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Settings Page" />
|
||||
|
||||
</DataTemplate>
|
||||
</BindableLayout.ItemTemplate>
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -1,33 +1,17 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Controls;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SettingsPage : BaseContentPage
|
||||
{
|
||||
private readonly TabsPage _tabsPage;
|
||||
private SettingsPageViewModel _vm;
|
||||
|
||||
public SettingsPage(TabsPage tabsPage)
|
||||
{
|
||||
_tabsPage = tabsPage;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
|
||||
public void BuildList()
|
||||
{
|
||||
_vm.BuildList();
|
||||
var vm = BindingContext as SettingsPageViewModel;
|
||||
vm.Page = this;
|
||||
}
|
||||
|
||||
protected override bool OnBackButtonPressed()
|
||||
@@ -39,35 +23,5 @@ namespace Bit.App.Pages
|
||||
}
|
||||
return base.OnBackButtonPressed();
|
||||
}
|
||||
|
||||
void ActivateTimePicker(object sender, EventArgs args)
|
||||
{
|
||||
var stackLayout = (ExtendedStackLayout)sender;
|
||||
SettingsPageListItem item = (SettingsPageListItem)stackLayout.BindingContext;
|
||||
if (item.ShowTimeInput)
|
||||
{
|
||||
var timePicker = stackLayout.Children.Where(x => x is TimePicker).FirstOrDefault();
|
||||
((TimePicker)timePicker)?.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
async void OnTimePickerPropertyChanged(object sender, PropertyChangedEventArgs args)
|
||||
{
|
||||
var s = (TimePicker)sender;
|
||||
var time = s.Time.TotalMinutes;
|
||||
if (s.IsFocused && args.PropertyName == "Time")
|
||||
{
|
||||
await _vm.VaultTimeoutAsync(false, (int)time);
|
||||
}
|
||||
}
|
||||
|
||||
private void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item)
|
||||
{
|
||||
_vm?.ExecuteSettingItemCommand.Execute(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SettingsPageHeaderListItem : ISettingsPageListItem
|
||||
{
|
||||
public SettingsPageHeaderListItem(string title)
|
||||
{
|
||||
Title = title;
|
||||
}
|
||||
|
||||
public string Title { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SettingsPageListGroup : List<SettingsPageListItem>
|
||||
{
|
||||
public SettingsPageListGroup(List<SettingsPageListItem> groupItems, string name, bool doUpper = true,
|
||||
bool first = false)
|
||||
{
|
||||
AddRange(groupItems);
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
Name = "-";
|
||||
}
|
||||
else if (doUpper)
|
||||
{
|
||||
Name = name.ToUpperInvariant();
|
||||
}
|
||||
else
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
First = first;
|
||||
}
|
||||
|
||||
public bool First { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,29 @@
|
||||
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
|
||||
{
|
||||
public class SettingsPageListItem : ISettingsPageListItem
|
||||
public class SettingsPageListItem
|
||||
{
|
||||
public string Icon { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string SubLabel { get; set; }
|
||||
public TimeSpan? Time { get; set; }
|
||||
public bool UseFrame { get; set; }
|
||||
public Func<Task> ExecuteAsync { get; set; }
|
||||
private readonly string _nameResourceKey;
|
||||
|
||||
public bool SubLabelTextEnabled => SubLabel == AppResources.On;
|
||||
public string LineBreakMode => SubLabel == null ? "TailTruncation" : "";
|
||||
public bool ShowSubLabel => SubLabel.Length != 0;
|
||||
public bool ShowTimeInput => Time != null;
|
||||
public Color SubLabelColor => SubLabelTextEnabled ?
|
||||
ThemeManager.GetResourceColor("SuccessColor") :
|
||||
ThemeManager.GetResourceColor("MutedColor");
|
||||
|
||||
public string AutomationIdSettingName
|
||||
public SettingsPageListItem(string nameResourceKey, Func<Task> executeAsync)
|
||||
{
|
||||
get
|
||||
{
|
||||
return AutomationIdsHelper.AddSuffixFor(
|
||||
UseFrame ? "EnabledPolicy"
|
||||
: AutomationIdsHelper.ToEnglishTitleCase(Name)
|
||||
, SuffixType.Cell);
|
||||
}
|
||||
_nameResourceKey = nameResourceKey;
|
||||
ExecuteAsync = executeAsync;
|
||||
}
|
||||
|
||||
public string AutomationIdSettingStatus
|
||||
public string Name => AppResources.ResourceManager.GetString(_nameResourceKey);
|
||||
|
||||
public Func<Task> ExecuteAsync { get; }
|
||||
|
||||
public string AutomationId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UseFrame)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return AutomationIdsHelper.AddSuffixFor(AutomationIdsHelper.ToEnglishTitleCase(Name), SuffixType.SettingValue);
|
||||
return AutomationIdsHelper.AddSuffixFor(AutomationIdsHelper.ToEnglishTitleCase(_nameResourceKey), SuffixType.Cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SettingsPageListItemSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate HeaderTemplate { get; set; }
|
||||
public DataTemplate RegularTemplate { get; set; }
|
||||
public DataTemplate TimePickerTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
|
||||
{
|
||||
if (item is SettingsPageHeaderListItem)
|
||||
{
|
||||
return HeaderTemplate;
|
||||
}
|
||||
if (item is SettingsPageListItem listItem)
|
||||
{
|
||||
return listItem.ShowTimeInput ? TimePickerTemplate : RegularTemplate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Pages.Accounts;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -17,887 +8,30 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public class SettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAutofillHandler _autofillHandler;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IBiometricService _biometricService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ILocalizeService _localizeService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ILogger _loggerService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly IWatchDeviceService _watchDeviceService;
|
||||
private const int CustomVaultTimeoutValue = -100;
|
||||
|
||||
private bool _supportsBiometric;
|
||||
private bool _pin;
|
||||
private bool _biometric;
|
||||
private bool _screenCaptureAllowed;
|
||||
private string _lastSyncDate;
|
||||
private string _vaultTimeoutDisplayValue;
|
||||
private string _vaultTimeoutActionDisplayValue;
|
||||
private bool _showChangeMasterPassword;
|
||||
private bool _reportLoggingEnabled;
|
||||
private bool _approvePasswordlessLoginRequests;
|
||||
private bool _shouldConnectToWatch;
|
||||
private bool _hasMasterPassword;
|
||||
private readonly static List<KeyValuePair<string, int?>> VaultTimeoutOptions =
|
||||
new List<KeyValuePair<string, int?>>
|
||||
{
|
||||
new KeyValuePair<string, int?>(AppResources.Immediately, 0),
|
||||
new KeyValuePair<string, int?>(AppResources.OneMinute, 1),
|
||||
new KeyValuePair<string, int?>(AppResources.FiveMinutes, 5),
|
||||
new KeyValuePair<string, int?>(AppResources.FifteenMinutes, 15),
|
||||
new KeyValuePair<string, int?>(AppResources.ThirtyMinutes, 30),
|
||||
new KeyValuePair<string, int?>(AppResources.OneHour, 60),
|
||||
new KeyValuePair<string, int?>(AppResources.FourHours, 240),
|
||||
new KeyValuePair<string, int?>(AppResources.OnRestart, -1),
|
||||
new KeyValuePair<string, int?>(AppResources.Never, null),
|
||||
new KeyValuePair<string, int?>(AppResources.Custom, CustomVaultTimeoutValue),
|
||||
};
|
||||
private readonly static List<KeyValuePair<string, VaultTimeoutAction>> VaultTimeoutActionOptions =
|
||||
new List<KeyValuePair<string, VaultTimeoutAction>>
|
||||
{
|
||||
new KeyValuePair<string, VaultTimeoutAction>(AppResources.Lock, VaultTimeoutAction.Lock),
|
||||
new KeyValuePair<string, VaultTimeoutAction>(AppResources.LogOut, VaultTimeoutAction.Logout),
|
||||
};
|
||||
|
||||
private Policy _vaultTimeoutPolicy;
|
||||
private int? _vaultTimeout;
|
||||
private List<KeyValuePair<string, int?>> _vaultTimeoutOptions = VaultTimeoutOptions;
|
||||
private List<KeyValuePair<string, VaultTimeoutAction>> _vaultTimeoutActionOptions = VaultTimeoutActionOptions;
|
||||
|
||||
public SettingsPageViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||
_authService = ServiceContainer.Resolve<IAuthService>();
|
||||
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
||||
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
|
||||
PageTitle = AppResources.Settings;
|
||||
ExecuteSettingItemCommand = new AsyncCommand<SettingsPageListItem>(item => item.ExecuteAsync(),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
ExecuteSettingItemCommand = new AsyncCommand<SettingsPageListItem>(item => item.ExecuteAsync(), onException: _loggerService.Exception, allowsMultipleExecutions: false);
|
||||
SettingsItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem(nameof(AppResources.AccountSecurity), () => NavigateToAsync(new SecuritySettingsPage())),
|
||||
new SettingsPageListItem(nameof(AppResources.Autofill), () => NavigateToAsync(new AutofillSettingsPage())),
|
||||
new SettingsPageListItem(nameof(AppResources.Vault), () => NavigateToAsync(new VaultSettingsPage())),
|
||||
new SettingsPageListItem(nameof(AppResources.Appearance), () => NavigateToAsync(new AppearanceSettingsPage())),
|
||||
new SettingsPageListItem(nameof(AppResources.Other), () => NavigateToAsync(new OtherSettingsPage())),
|
||||
new SettingsPageListItem(nameof(AppResources.About), () => NavigateToAsync(new AboutSettingsPage()))
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsVaultTimeoutActionLockAllowed => _hasMasterPassword || _biometric || _pin;
|
||||
|
||||
public ObservableRangeCollection<ISettingsPageListItem> GroupedItems { get; set; }
|
||||
public List<SettingsPageListItem> SettingsItems { get; }
|
||||
|
||||
public IAsyncCommand<SettingsPageListItem> ExecuteSettingItemCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
private async Task NavigateToAsync(Page page)
|
||||
{
|
||||
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)
|
||||
{
|
||||
lastSync = lastSync.Value.ToLocalTime();
|
||||
_lastSyncDate = string.Format("{0} {1}",
|
||||
_localizeService.GetLocaleShortDate(lastSync.Value),
|
||||
_localizeService.GetLocaleShortTime(lastSync.Value));
|
||||
}
|
||||
|
||||
_vaultTimeoutPolicy = null;
|
||||
_vaultTimeoutOptions = VaultTimeoutOptions;
|
||||
_vaultTimeoutActionOptions = VaultTimeoutActionOptions;
|
||||
|
||||
_vaultTimeout = await _vaultTimeoutService.GetVaultTimeout();
|
||||
_vaultTimeoutDisplayValue = _vaultTimeoutOptions.FirstOrDefault(o => o.Value == _vaultTimeout).Key;
|
||||
_vaultTimeoutDisplayValue ??= _vaultTimeoutOptions.Where(o => o.Value == CustomVaultTimeoutValue).First().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))
|
||||
{
|
||||
// if we have a vault timeout policy, we need to filter the timeout options
|
||||
_vaultTimeoutPolicy = (await _policyService.GetAll(PolicyType.MaximumVaultTimeout)).First();
|
||||
var policyMinutes = _vaultTimeoutPolicy.GetInt(Policy.MINUTES_KEY);
|
||||
_vaultTimeoutOptions = _vaultTimeoutOptions.Where(t =>
|
||||
t.Value <= policyMinutes &&
|
||||
(t.Value > 0 || t.Value == CustomVaultTimeoutValue) &&
|
||||
t.Value != null).ToList();
|
||||
}
|
||||
_screenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
|
||||
|
||||
if (_vaultTimeoutDisplayValue == null)
|
||||
{
|
||||
_vaultTimeoutDisplayValue = AppResources.Custom;
|
||||
}
|
||||
|
||||
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() && await _userVerificationService.HasMasterPasswordAsync();
|
||||
_reportLoggingEnabled = await _loggerService.IsEnabled();
|
||||
_approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
|
||||
_shouldConnectToWatch = await _stateService.GetShouldConnectToWatchAsync();
|
||||
|
||||
BuildList();
|
||||
}
|
||||
|
||||
public async Task AboutAsync()
|
||||
{
|
||||
var debugText = string.Format("{0}: {1} ({2})", AppResources.Version,
|
||||
_platformUtilsService.GetApplicationVersion(), _deviceActionService.GetBuildNumber());
|
||||
|
||||
#if DEBUG
|
||||
var pushNotificationsRegistered = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService").IsRegisteredForPush;
|
||||
var pnServerRegDate = await _stateService.GetPushLastRegistrationDateAsync();
|
||||
var pnServerError = await _stateService.GetPushInstallationRegistrationErrorAsync();
|
||||
|
||||
var pnServerRegDateMessage = default(DateTime) == pnServerRegDate ? "-" : $"{pnServerRegDate.GetValueOrDefault().ToShortDateString()}-{pnServerRegDate.GetValueOrDefault().ToShortTimeString()} UTC";
|
||||
var errorMessage = string.IsNullOrEmpty(pnServerError) ? string.Empty : $"Push Notifications Server Registration error: {pnServerError}";
|
||||
|
||||
var text = string.Format("© Bitwarden Inc. 2015-{0}\n\n{1}\nPush Notifications registered:{2}\nPush Notifications Server Last Date :{3}\n{4}", DateTime.Now.Year, debugText, pushNotificationsRegistered, pnServerRegDateMessage, errorMessage);
|
||||
#else
|
||||
var text = string.Format("© Bitwarden Inc. 2015-{0}\n\n{1}", DateTime.Now.Year, debugText);
|
||||
#endif
|
||||
|
||||
var copy = await _platformUtilsService.ShowDialogAsync(text, AppResources.Bitwarden, AppResources.Copy,
|
||||
AppResources.Close);
|
||||
if (copy)
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(debugText);
|
||||
}
|
||||
}
|
||||
|
||||
public void Help()
|
||||
{
|
||||
_platformUtilsService.LaunchUri("https://bitwarden.com/help/");
|
||||
}
|
||||
|
||||
public async Task FingerprintAsync()
|
||||
{
|
||||
List<string> fingerprint;
|
||||
try
|
||||
{
|
||||
fingerprint = await _cryptoService.GetFingerprintAsync(await _stateService.GetActiveUserIdAsync());
|
||||
}
|
||||
catch (Exception e) when (e.Message == "No public key available.")
|
||||
{
|
||||
return;
|
||||
}
|
||||
var phrase = string.Join("-", fingerprint);
|
||||
var text = string.Format("{0}:\n\n{1}", AppResources.YourAccountsFingerprint, phrase);
|
||||
var learnMore = await _platformUtilsService.ShowDialogAsync(text, AppResources.FingerprintPhrase,
|
||||
AppResources.LearnMore, AppResources.Close);
|
||||
if (learnMore)
|
||||
{
|
||||
_platformUtilsService.LaunchUri("https://bitwarden.com/help/fingerprint-phrase/");
|
||||
}
|
||||
}
|
||||
|
||||
public void Rate()
|
||||
{
|
||||
_deviceActionService.RateApp();
|
||||
}
|
||||
|
||||
public void Import()
|
||||
{
|
||||
_platformUtilsService.LaunchUri("https://bitwarden.com/help/import-data/");
|
||||
}
|
||||
|
||||
public void WebVault()
|
||||
{
|
||||
_platformUtilsService.LaunchUri(_environmentService.GetWebVaultUrl());
|
||||
}
|
||||
|
||||
public async Task ShareAsync()
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LearnOrgConfirmation,
|
||||
AppResources.LearnOrg, AppResources.Yes, AppResources.Cancel);
|
||||
if (confirmed)
|
||||
{
|
||||
_platformUtilsService.LaunchUri("https://bitwarden.com/help/about-organizations/");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TwoStepAsync()
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.TwoStepLoginConfirmation,
|
||||
AppResources.TwoStepLogin, AppResources.Yes, AppResources.Cancel);
|
||||
if (confirmed)
|
||||
{
|
||||
_platformUtilsService.LaunchUri($"{_environmentService.GetWebVaultUrl()}/#/settings");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ChangePasswordAsync()
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ChangePasswordConfirmation,
|
||||
AppResources.ChangeMasterPassword, AppResources.Yes, AppResources.Cancel);
|
||||
if (confirmed)
|
||||
{
|
||||
_platformUtilsService.LaunchUri($"{_environmentService.GetWebVaultUrl()}/#/settings");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogOutAsync()
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation,
|
||||
AppResources.LogOut, AppResources.Yes, AppResources.Cancel);
|
||||
if (confirmed)
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LockAsync()
|
||||
{
|
||||
await _vaultTimeoutService.LockAsync(true, true);
|
||||
}
|
||||
|
||||
public async Task VaultTimeoutAsync(bool promptOptions = true, int? newTimeout = 0)
|
||||
{
|
||||
var oldTimeout = _vaultTimeout;
|
||||
|
||||
var options = _vaultTimeoutOptions.Select(
|
||||
o => o.Key == _vaultTimeoutDisplayValue ? $"✓ {o.Key}" : o.Key).ToArray();
|
||||
if (promptOptions)
|
||||
{
|
||||
var selection = await Page.DisplayActionSheet(AppResources.VaultTimeout,
|
||||
AppResources.Cancel, null, options);
|
||||
if (selection == null || selection == AppResources.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var cleanSelection = selection.Replace("✓ ", string.Empty);
|
||||
var selectionOption = _vaultTimeoutOptions.FirstOrDefault(o => o.Key == cleanSelection);
|
||||
|
||||
// Check if the selected Timeout action is "Never" and if it's different from the previous selected value
|
||||
if (selectionOption.Value == null && selectionOption.Value != oldTimeout)
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.NeverLockWarning,
|
||||
AppResources.Warning, AppResources.Yes, AppResources.Cancel);
|
||||
if (!confirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
_vaultTimeoutDisplayValue = selectionOption.Key;
|
||||
newTimeout = selectionOption.Value;
|
||||
}
|
||||
|
||||
if (_vaultTimeoutPolicy != null)
|
||||
{
|
||||
var maximumTimeout = _vaultTimeoutPolicy.GetInt(Policy.MINUTES_KEY);
|
||||
|
||||
if (newTimeout > maximumTimeout)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutToLarge, AppResources.Warning);
|
||||
var timeout = await _vaultTimeoutService.GetVaultTimeout();
|
||||
_vaultTimeoutDisplayValue = _vaultTimeoutOptions.FirstOrDefault(o => o.Value == timeout).Key ??
|
||||
AppResources.Custom;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(newTimeout,
|
||||
GetVaultTimeoutActionFromKey(_vaultTimeoutActionDisplayValue));
|
||||
|
||||
if (newTimeout != CustomVaultTimeoutValue)
|
||||
{
|
||||
_vaultTimeout = newTimeout;
|
||||
}
|
||||
if (oldTimeout != newTimeout)
|
||||
{
|
||||
await _cryptoService.RefreshKeysAsync();
|
||||
await Device.InvokeOnMainThreadAsync(BuildList);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoggerReportingAsync()
|
||||
{
|
||||
var options = new[]
|
||||
{
|
||||
CreateSelectableOption(AppResources.Yes, _reportLoggingEnabled),
|
||||
CreateSelectableOption(AppResources.No, !_reportLoggingEnabled),
|
||||
};
|
||||
|
||||
var selection = await Page.DisplayActionSheet(AppResources.SubmitCrashLogsDescription, AppResources.Cancel, null, options);
|
||||
|
||||
if (selection == null || selection == AppResources.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _loggerService.SetEnabled(CompareSelection(selection, AppResources.Yes));
|
||||
_reportLoggingEnabled = await _loggerService.IsEnabled();
|
||||
BuildList();
|
||||
}
|
||||
|
||||
public async Task ApproveLoginRequestsAsync()
|
||||
{
|
||||
var options = new[]
|
||||
{
|
||||
CreateSelectableOption(AppResources.Yes, _approvePasswordlessLoginRequests),
|
||||
CreateSelectableOption(AppResources.No, !_approvePasswordlessLoginRequests),
|
||||
};
|
||||
|
||||
var selection = await Page.DisplayActionSheet(AppResources.UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices, AppResources.Cancel, null, options);
|
||||
|
||||
if (selection == null || selection == AppResources.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_approvePasswordlessLoginRequests = CompareSelection(selection, AppResources.Yes);
|
||||
await _stateService.SetApprovePasswordlessLoginsAsync(_approvePasswordlessLoginRequests);
|
||||
|
||||
BuildList();
|
||||
|
||||
if (!_approvePasswordlessLoginRequests || await _pushNotificationService.AreNotificationsSettingsEnabledAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var openAppSettingsResult = await _platformUtilsService.ShowDialogAsync(AppResources.ReceivePushNotificationsForNewLoginRequests, title: string.Empty, confirmText: AppResources.Settings, cancelText: AppResources.NoThanks);
|
||||
if (openAppSettingsResult)
|
||||
{
|
||||
_deviceActionService.OpenAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task VaultTimeoutActionAsync()
|
||||
{
|
||||
if (_vaultTimeoutPolicy != null &&
|
||||
!string.IsNullOrEmpty(_vaultTimeoutPolicy.GetString(Policy.ACTION_KEY)))
|
||||
{
|
||||
// do nothing if we have a policy set
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var cleanSelection = selection.Replace("✓ ", string.Empty);
|
||||
if (cleanSelection == AppResources.LogOut)
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutLogOutConfirmation,
|
||||
AppResources.Warning, AppResources.Yes, AppResources.Cancel);
|
||||
if (!confirmed)
|
||||
{
|
||||
// Reset to lock and continue process as if lock were selected
|
||||
cleanSelection = AppResources.Lock;
|
||||
}
|
||||
}
|
||||
var selectionOption = _vaultTimeoutActionOptions.FirstOrDefault(o => o.Key == cleanSelection);
|
||||
var changed = _vaultTimeoutActionDisplayValue != selectionOption.Key;
|
||||
_vaultTimeoutActionDisplayValue = selectionOption.Key;
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(_vaultTimeout,
|
||||
selectionOption.Value);
|
||||
if (changed)
|
||||
{
|
||||
_messagingService.Send("vaultTimeoutActionChanged");
|
||||
}
|
||||
BuildList();
|
||||
}
|
||||
|
||||
public async Task UpdatePinAsync()
|
||||
{
|
||||
_pin = !_pin;
|
||||
if (_pin)
|
||||
{
|
||||
var pin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN,
|
||||
AppResources.SetPINDescription, null, AppResources.Submit, AppResources.Cancel, true);
|
||||
if (!string.IsNullOrWhiteSpace(pin))
|
||||
{
|
||||
var masterPassOnRestart = false;
|
||||
if (await _userVerificationService.HasMasterPasswordAsync())
|
||||
{
|
||||
masterPassOnRestart = await _platformUtilsService.ShowDialogAsync(
|
||||
AppResources.PINRequireMasterPasswordRestart, AppResources.UnlockWithPIN,
|
||||
AppResources.Yes, AppResources.No);
|
||||
}
|
||||
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
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)
|
||||
{
|
||||
await _stateService.SetPinKeyEncryptedUserKeyEphemeralAsync(protectedPinKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stateService.SetPinKeyEncryptedUserKeyAsync(protectedPinKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_pin = false;
|
||||
}
|
||||
}
|
||||
if (!_pin)
|
||||
{
|
||||
await _vaultTimeoutService.ClearAsync();
|
||||
await UpdateVaultTimeoutActionIfNeededAsync();
|
||||
}
|
||||
BuildList();
|
||||
}
|
||||
|
||||
public async Task UpdateBiometricAsync()
|
||||
{
|
||||
var current = _biometric;
|
||||
if (_biometric)
|
||||
{
|
||||
_biometric = false;
|
||||
}
|
||||
else if (await _platformUtilsService.SupportsBiometricAsync())
|
||||
{
|
||||
_biometric = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
Device.RuntimePlatform == Device.Android ? "." : null);
|
||||
}
|
||||
if (_biometric == current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_biometric)
|
||||
{
|
||||
await _biometricService.SetupBiometricAsync();
|
||||
await _stateService.SetBiometricUnlockAsync(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stateService.SetBiometricUnlockAsync(null);
|
||||
await UpdateVaultTimeoutActionIfNeededAsync();
|
||||
}
|
||||
await _stateService.SetBiometricLockedAsync(false);
|
||||
await _cryptoService.RefreshKeysAsync();
|
||||
BuildList();
|
||||
}
|
||||
|
||||
public void BuildList()
|
||||
{
|
||||
//TODO: Refactor this once navigation is abstracted so that it doesn't depend on Page, e.g. Page.Navigation.PushModalAsync...
|
||||
|
||||
var doUpper = Device.RuntimePlatform != Device.Android;
|
||||
var autofillItems = new List<SettingsPageListItem>();
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
autofillItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.AutofillServices,
|
||||
SubLabel = _autofillHandler.AutofillServicesEnabled() ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage)))
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
autofillItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.PasswordAutofill,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillPage()))
|
||||
});
|
||||
}
|
||||
autofillItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.AppExtension,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExtensionPage()))
|
||||
});
|
||||
}
|
||||
var manageItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.Folders,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new FoldersPage()))
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.Sync,
|
||||
SubLabel = _lastSyncDate,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new SyncPage()))
|
||||
}
|
||||
};
|
||||
var securityItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.VaultTimeout,
|
||||
SubLabel = _vaultTimeoutDisplayValue,
|
||||
ExecuteAsync = () => VaultTimeoutAsync() },
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.VaultTimeoutAction,
|
||||
SubLabel = _vaultTimeoutActionDisplayValue,
|
||||
ExecuteAsync = () => VaultTimeoutActionAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.UnlockWithPIN,
|
||||
SubLabel = _pin ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => UpdatePinAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ApproveLoginRequests,
|
||||
SubLabel = _approvePasswordlessLoginRequests ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => ApproveLoginRequestsAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.LockNow,
|
||||
ExecuteAsync = () => LockAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.TwoStepLogin,
|
||||
ExecuteAsync = () => TwoStepAsync()
|
||||
}
|
||||
};
|
||||
if (_approvePasswordlessLoginRequests)
|
||||
{
|
||||
manageItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.PendingLogInRequests,
|
||||
ExecuteAsync = () => PendingLoginRequestsAsync()
|
||||
});
|
||||
}
|
||||
if (_supportsBiometric || _biometric)
|
||||
{
|
||||
var biometricName = AppResources.Biometrics;
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
biometricName = _deviceActionService.SupportsFaceBiometric() ? AppResources.FaceID :
|
||||
AppResources.TouchID;
|
||||
}
|
||||
var item = new SettingsPageListItem
|
||||
{
|
||||
Name = string.Format(AppResources.UnlockWith, biometricName),
|
||||
SubLabel = _biometric ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => UpdateBiometricAsync()
|
||||
};
|
||||
securityItems.Insert(2, item);
|
||||
}
|
||||
if (_vaultTimeoutDisplayValue == AppResources.Custom)
|
||||
{
|
||||
securityItems.Insert(1, new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.Custom,
|
||||
Time = TimeSpan.FromMinutes(Math.Abs((double)_vaultTimeout.GetValueOrDefault())),
|
||||
});
|
||||
}
|
||||
if (_vaultTimeoutPolicy != null)
|
||||
{
|
||||
var policyMinutes = _vaultTimeoutPolicy.GetInt(Policy.MINUTES_KEY);
|
||||
var policyAction = _vaultTimeoutPolicy.GetString(Policy.ACTION_KEY);
|
||||
|
||||
if (policyMinutes.HasValue || !string.IsNullOrWhiteSpace(policyAction))
|
||||
{
|
||||
string policyAlert;
|
||||
if (policyMinutes.HasValue && string.IsNullOrWhiteSpace(policyAction))
|
||||
{
|
||||
policyAlert = string.Format(AppResources.VaultTimeoutPolicyInEffect,
|
||||
Math.Floor((float)policyMinutes / 60),
|
||||
policyMinutes % 60);
|
||||
}
|
||||
else if (!policyMinutes.HasValue && !string.IsNullOrWhiteSpace(policyAction))
|
||||
{
|
||||
policyAlert = string.Format(AppResources.VaultTimeoutActionPolicyInEffect,
|
||||
policyAction == Policy.ACTION_LOCK ? AppResources.Lock : AppResources.LogOut);
|
||||
}
|
||||
else
|
||||
{
|
||||
policyAlert = string.Format(AppResources.VaultTimeoutPolicyWithActionInEffect,
|
||||
Math.Floor((float)policyMinutes / 60),
|
||||
policyMinutes % 60,
|
||||
policyAction == Policy.ACTION_LOCK ? AppResources.Lock : AppResources.LogOut);
|
||||
}
|
||||
securityItems.Insert(0, new SettingsPageListItem
|
||||
{
|
||||
Name = policyAlert,
|
||||
UseFrame = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
securityItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.AllowScreenCapture,
|
||||
SubLabel = _screenCaptureAllowed ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => SetScreenCaptureAllowedAsync()
|
||||
});
|
||||
}
|
||||
var accountItems = new List<SettingsPageListItem>();
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
accountItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ConnectToWatch,
|
||||
SubLabel = _shouldConnectToWatch ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => ToggleWatchConnectionAsync()
|
||||
});
|
||||
}
|
||||
accountItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.FingerprintPhrase,
|
||||
ExecuteAsync = () => FingerprintAsync()
|
||||
});
|
||||
accountItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.LogOut,
|
||||
ExecuteAsync = () => LogOutAsync()
|
||||
});
|
||||
if (_showChangeMasterPassword)
|
||||
{
|
||||
accountItems.Insert(0, new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ChangeMasterPassword,
|
||||
ExecuteAsync = () => ChangePasswordAsync()
|
||||
});
|
||||
}
|
||||
var toolsItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ImportItems,
|
||||
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Import())
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ExportVault,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()))
|
||||
}
|
||||
};
|
||||
if (IncludeLinksWithSubscriptionInfo())
|
||||
{
|
||||
toolsItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.LearnOrg,
|
||||
ExecuteAsync = () => ShareAsync()
|
||||
});
|
||||
toolsItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.WebVault,
|
||||
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => WebVault())
|
||||
});
|
||||
}
|
||||
|
||||
var otherItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.Options,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new OptionsPage()))
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.About,
|
||||
ExecuteAsync = () => AboutAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.HelpAndFeedback,
|
||||
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Help())
|
||||
},
|
||||
#if !FDROID
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.SubmitCrashLogs,
|
||||
SubLabel = _reportLoggingEnabled ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => LoggerReportingAsync()
|
||||
},
|
||||
#endif
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.RateTheApp,
|
||||
ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Rate())
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.DeleteAccount,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage()))
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: improve this. Leaving this as is to reduce error possibility on the hotfix.
|
||||
var settingsListGroupItems = new List<SettingsPageListGroup>()
|
||||
{
|
||||
new SettingsPageListGroup(autofillItems, AppResources.Autofill, doUpper, true),
|
||||
new SettingsPageListGroup(manageItems, AppResources.Manage, doUpper),
|
||||
new SettingsPageListGroup(securityItems, AppResources.Security, doUpper),
|
||||
new SettingsPageListGroup(accountItems, AppResources.Account, doUpper),
|
||||
new SettingsPageListGroup(toolsItems, AppResources.Tools, doUpper),
|
||||
new SettingsPageListGroup(otherItems, AppResources.Other, doUpper)
|
||||
};
|
||||
|
||||
// TODO: refactor this
|
||||
if (Device.RuntimePlatform == Device.Android
|
||||
||
|
||||
GroupedItems.Any())
|
||||
{
|
||||
var items = new List<ISettingsPageListItem>();
|
||||
foreach (var itemGroup in settingsListGroupItems)
|
||||
{
|
||||
items.Add(new SettingsPageHeaderListItem(itemGroup.Name));
|
||||
items.AddRange(itemGroup);
|
||||
}
|
||||
|
||||
GroupedItems.ReplaceRange(items);
|
||||
}
|
||||
else
|
||||
{
|
||||
// HACK: we need this on iOS, so that it doesn't crash when adding coming from an empty list
|
||||
var first = true;
|
||||
var items = new List<ISettingsPageListItem>();
|
||||
foreach (var itemGroup in settingsListGroupItems)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
items.Add(new SettingsPageHeaderListItem(itemGroup.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
items.AddRange(itemGroup);
|
||||
}
|
||||
|
||||
if (settingsListGroupItems.Any())
|
||||
{
|
||||
GroupedItems.ReplaceRange(new List<ISettingsPageListItem> { new SettingsPageHeaderListItem(settingsListGroupItems[0].Name) });
|
||||
GroupedItems.AddRange(items);
|
||||
}
|
||||
else
|
||||
{
|
||||
GroupedItems.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PendingLoginRequestsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var requests = await _authService.GetActivePasswordlessLoginRequestsAsync();
|
||||
if (requests == null || !requests.Any())
|
||||
{
|
||||
_platformUtilsService.ShowToast("info", null, AppResources.NoPendingRequests);
|
||||
return;
|
||||
}
|
||||
|
||||
Page.Navigation.PushModalAsync(new NavigationPage(new LoginPasswordlessRequestsListPage())).FireAndForget();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IncludeLinksWithSubscriptionInfo()
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private VaultTimeoutAction GetVaultTimeoutActionFromKey(string key)
|
||||
{
|
||||
return _vaultTimeoutActionOptions.FirstOrDefault(o => o.Key == key).Value;
|
||||
}
|
||||
|
||||
private int? GetVaultTimeoutFromKey(string key)
|
||||
{
|
||||
return _vaultTimeoutOptions.FirstOrDefault(o => o.Key == key).Value;
|
||||
}
|
||||
|
||||
private string CreateSelectableOption(string option, bool selected) => selected ? ToSelectedOption(option) : option;
|
||||
|
||||
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == ToSelectedOption(compareTo);
|
||||
|
||||
private string ToSelectedOption(string option) => $"✓ {option}";
|
||||
|
||||
public async Task SetScreenCaptureAllowedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_screenCaptureAllowed
|
||||
&&
|
||||
!await Page.DisplayAlert(AppResources.AllowScreenCapture, AppResources.AreYouSureYouWantToEnableScreenCapture, AppResources.Yes, AppResources.No))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _stateService.SetScreenCaptureAllowedAsync(!_screenCaptureAllowed);
|
||||
_screenCaptureAllowed = !_screenCaptureAllowed;
|
||||
await _deviceActionService.SetScreenCaptureAllowedAsync();
|
||||
BuildList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_loggerService.Exception(ex);
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleWatchConnectionAsync()
|
||||
{
|
||||
_shouldConnectToWatch = !_shouldConnectToWatch;
|
||||
|
||||
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);
|
||||
await Page.Navigation.PushAsync(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.SyncPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:SyncPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SyncPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n EnableSyncOnRefresh}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding EnableSyncOnRefresh}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n EnableSyncOnRefreshDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<Button Text="{u:I18n SyncVaultNow}" Clicked="Sync_Clicked"></Button>
|
||||
<Label StyleClass="text-muted, text-sm" HorizontalTextAlignment="Center" Margin="0,10">
|
||||
<Label.FormattedText>
|
||||
<FormattedString>
|
||||
<Span Text="{u:I18n LastSync}" />
|
||||
<Span Text=" " />
|
||||
<Span Text="{Binding LastSync}" />
|
||||
</FormattedString>
|
||||
</Label.FormattedText>
|
||||
</Label>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
@@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SyncPage : BaseContentPage
|
||||
{
|
||||
private readonly SyncPageViewModel _vm;
|
||||
|
||||
public SyncPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SyncPageViewModel;
|
||||
_vm.Page = this;
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
|
||||
private async void Sync_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SyncAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SyncPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly ILocalizeService _localizeService;
|
||||
|
||||
private string _lastSync = "--";
|
||||
private bool _inited;
|
||||
private bool _syncOnRefresh;
|
||||
|
||||
public SyncPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
|
||||
|
||||
PageTitle = AppResources.Sync;
|
||||
}
|
||||
|
||||
public bool EnableSyncOnRefresh
|
||||
{
|
||||
get => _syncOnRefresh;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _syncOnRefresh, value))
|
||||
{
|
||||
var task = UpdateSyncOnRefreshAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string LastSync
|
||||
{
|
||||
get => _lastSync;
|
||||
set => SetProperty(ref _lastSync, value);
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await SetLastSyncAsync();
|
||||
EnableSyncOnRefresh = await _stateService.GetSyncOnRefreshAsync();
|
||||
_inited = true;
|
||||
}
|
||||
|
||||
public async Task UpdateSyncOnRefreshAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
await _stateService.SetSyncOnRefreshAsync(_syncOnRefresh);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetLastSyncAsync()
|
||||
{
|
||||
var last = await _syncService.GetLastSyncAsync();
|
||||
if (last != null)
|
||||
{
|
||||
var localDate = last.Value.ToLocalTime();
|
||||
LastSync = string.Format("{0} {1}",
|
||||
_localizeService.GetLocaleShortDate(localDate),
|
||||
_localizeService.GetLocaleShortTime(localDate));
|
||||
}
|
||||
else
|
||||
{
|
||||
LastSync = AppResources.Never;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SyncAsync()
|
||||
{
|
||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
|
||||
await _syncService.SyncPasswordlessLoginRequestsAsync();
|
||||
var success = await _syncService.FullSyncAsync(true);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (success)
|
||||
{
|
||||
await SetLastSyncAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Page.DisplayAlert(null, AppResources.SyncingFailed, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/App/Pages/Settings/VaultSettingsPage.xaml
Normal file
48
src/App/Pages/Settings/VaultSettingsPage.xaml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:VaultSettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:Class="Bit.App.Pages.VaultSettingsPage"
|
||||
Title="{u:I18n Vault}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:VaultSettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<StackLayout>
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n Folders}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="FoldersLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToFoldersCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n ExportVault}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="ExportVaultLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToExportVaultCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n ImportItems}"
|
||||
GoToLinkCommand="{Binding GoToImportItemsCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="ImportItemsLinkItemView" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
</StackLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
12
src/App/Pages/Settings/VaultSettingsPage.xaml.cs
Normal file
12
src/App/Pages/Settings/VaultSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class VaultSettingsPage : BaseContentPage
|
||||
{
|
||||
public VaultSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
var vm = BindingContext as VaultSettingsPageViewModel;
|
||||
vm.Page = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/App/Pages/Settings/VaultSettingsPageViewModel.cs
Normal file
51
src/App/Pages/Settings/VaultSettingsPageViewModel.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class VaultSettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
|
||||
public VaultSettingsPageViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||
|
||||
GoToFoldersCommand = new AsyncCommand(() => Page.Navigation.PushModalAsync(new NavigationPage(new FoldersPage())),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
GoToExportVaultCommand = new AsyncCommand(() => Page.Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage())),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
GoToImportItemsCommand = new AsyncCommand(GoToImportItemsAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public ICommand GoToFoldersCommand { get; }
|
||||
public ICommand GoToExportVaultCommand { get; }
|
||||
public ICommand GoToImportItemsCommand { get; }
|
||||
|
||||
private async Task GoToImportItemsAsync()
|
||||
{
|
||||
var webVaultUrl = _environmentService.GetWebVaultUrl();
|
||||
var body = string.Format(AppResources.YouCanImportDataToYourVaultOnX, webVaultUrl);
|
||||
if (await _platformUtilsService.ShowDialogAsync(body, AppResources.ContinueToWebApp, AppResources.Continue, AppResources.Cancel))
|
||||
{
|
||||
_platformUtilsService.LaunchUri(webVaultUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
||||
using Bit.App.Effects;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Data;
|
||||
@@ -137,7 +138,7 @@ namespace Bit.App.Pages
|
||||
await groupingsPage.HideAccountSwitchingOverlayAsync();
|
||||
}
|
||||
|
||||
_messagingService.Send("updatedTheme");
|
||||
_messagingService.Send(ThemeManager.UPDATED_THEME_MESSAGE_KEY);
|
||||
if (navPage.RootPage is GroupingsPage)
|
||||
{
|
||||
// Load something?
|
||||
@@ -146,10 +147,6 @@ namespace Bit.App.Pages
|
||||
{
|
||||
await genPage.InitAsync();
|
||||
}
|
||||
else if (navPage.RootPage is SettingsPage settingsPage)
|
||||
{
|
||||
await settingsPage.InitAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||
_cipherDomain = await _cipherService.SaveAttachmentRawWithServerAsync(
|
||||
_cipherDomain, FileName, FileData);
|
||||
_cipherDomain, Cipher, FileName, FileData);
|
||||
Cipher = await _cipherDomain.DecryptAsync();
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.AttachementAdded);
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _cipher, value, additionalPropertyNames: AdditionalPropertiesToRaiseOnCipherChanged);
|
||||
}
|
||||
|
||||
public string CreationDate => string.Format(AppResources.CreatedX, Cipher.CreationDate.ToShortDateString());
|
||||
public string CreationDate => string.Format(AppResources.CreatedXY, Cipher.CreationDate.ToShortDateString(), Cipher.CreationDate.ToShortTimeString());
|
||||
|
||||
public AsyncCommand CheckPasswordCommand { get; }
|
||||
|
||||
|
||||
@@ -229,9 +229,9 @@
|
||||
Margin="0,10,0,0"
|
||||
IsVisible="{Binding ShowPasskeyInfo}"/>
|
||||
<Entry
|
||||
Text="{u:I18n AvailableForTwoStepLogin}"
|
||||
Text="{Binding CreationDate}"
|
||||
IsEnabled="False"
|
||||
StyleClass="box-value,text-muted"
|
||||
StyleClass="box-value,text-muted"
|
||||
IsVisible="{Binding ShowPasskeyInfo}" />
|
||||
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
@@ -650,38 +650,6 @@
|
||||
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">
|
||||
|
||||
@@ -296,7 +296,6 @@ 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,7 +308,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 bool ShowPasskeyInfo => Cipher?.HasFido2Credential == true && !CloneMode;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
@@ -371,7 +370,7 @@ namespace Bit.App.Pages
|
||||
if (Cipher.Type == CipherType.Login)
|
||||
{
|
||||
// passkeys can't be cloned
|
||||
Cipher.Login.Fido2Key = null;
|
||||
Cipher.Login.Fido2Credentials = null;
|
||||
}
|
||||
}
|
||||
if (appOptions?.OtpData != null && Cipher.Type == CipherType.Login)
|
||||
|
||||
@@ -199,12 +199,12 @@
|
||||
Text="{u:I18n Passkey}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"
|
||||
IsVisible="{Binding Cipher.Login.Fido2Key, Converter={StaticResource notNull}}"/>
|
||||
IsVisible="{Binding Cipher.Login.MainFido2Credential, Converter={StaticResource notNull}}"/>
|
||||
<Entry
|
||||
Text="{u:I18n AvailableForTwoStepLogin}"
|
||||
Text="{Binding CreationDate}"
|
||||
IsEnabled="False"
|
||||
StyleClass="box-value,text-muted"
|
||||
IsVisible="{Binding Cipher.Login.Fido2Key, Converter={StaticResource notNull}}" />
|
||||
IsVisible="{Binding Cipher.Login.MainFido2Credential, Converter={StaticResource notNull}}" />
|
||||
<Grid StyleClass="box-row"
|
||||
IsVisible="{Binding ShowTotp}"
|
||||
AutomationId="ItemRow">
|
||||
@@ -579,64 +579,6 @@
|
||||
</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">
|
||||
|
||||
@@ -148,7 +148,6 @@ 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
|
||||
{
|
||||
@@ -649,11 +648,6 @@ 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)
|
||||
{
|
||||
@@ -708,18 +702,12 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task<bool> CanCloneAsync()
|
||||
{
|
||||
if (Cipher.Type == CipherType.Fido2Key)
|
||||
if (!Cipher.HasFido2Credential)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PasskeyWillNotBeCopied);
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Cipher.Type == CipherType.Login && Cipher.Login?.Fido2Key != null)
|
||||
{
|
||||
return await _platformUtilsService.ShowDialogAsync(AppResources.ThePasskeyWillNotBeCopiedToTheClonedItemDoYouWantToContinueCloningThisItem, AppResources.PasskeyWillNotBeCopied, AppResources.Yes, AppResources.No);
|
||||
}
|
||||
|
||||
return true;
|
||||
return await _platformUtilsService.ShowDialogAsync(AppResources.ThePasskeyWillNotBeCopiedToTheClonedItemDoYouWantToContinueCloningThisItem, AppResources.PasskeyWillNotBeCopied, AppResources.Yes, AppResources.No);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,24 +130,27 @@ namespace Bit.App.Pages
|
||||
|
||||
await LoadOnAppearedAsync(_mainLayout, false, async () =>
|
||||
{
|
||||
if (!_syncService.SyncInProgress || (await _cipherService.GetAllAsync()).Any())
|
||||
if (_previousPage == null)
|
||||
{
|
||||
try
|
||||
if (!_syncService.SyncInProgress || (await _cipherService.GetAllAsync()).Any())
|
||||
{
|
||||
await _vm.LoadAsync();
|
||||
try
|
||||
{
|
||||
await _vm.LoadAsync();
|
||||
}
|
||||
catch (Exception e) when (e.Message.Contains("No key."))
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
await _vm.LoadAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception e) when (e.Message.Contains("No key."))
|
||||
else
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
await _vm.LoadAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
if (!_vm.Loaded)
|
||||
{
|
||||
await _vm.LoadAsync();
|
||||
await Task.Delay(5000);
|
||||
if (!_vm.Loaded)
|
||||
{
|
||||
await _vm.LoadAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
await ShowPreviousPageAsync();
|
||||
|
||||
@@ -60,9 +60,6 @@ namespace Bit.App.Pages
|
||||
case CipherType.Identity:
|
||||
_name = AppResources.TypeIdentity;
|
||||
break;
|
||||
case CipherType.Fido2Key:
|
||||
_name = AppResources.Passkey;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -111,9 +108,6 @@ namespace Bit.App.Pages
|
||||
case CipherType.Identity:
|
||||
_icon = BitwardenIcons.IdCard;
|
||||
break;
|
||||
case CipherType.Fido2Key:
|
||||
_icon = BitwardenIcons.Passkey;
|
||||
break;
|
||||
default:
|
||||
_icon = BitwardenIcons.Globe;
|
||||
break;
|
||||
|
||||
@@ -569,7 +569,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
Filter = c => !c.IsDeleted
|
||||
&&
|
||||
Type.Value.IsEqualToOrCanSignIn(c.Type);
|
||||
Type.Value == c.Type;
|
||||
}
|
||||
else if (FolderId != null)
|
||||
{
|
||||
@@ -636,9 +636,7 @@ namespace Bit.App.Pages
|
||||
NoFolderCiphers.Add(c);
|
||||
}
|
||||
|
||||
// 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)
|
||||
_typeCounts[c.Type] = _typeCounts.TryGetValue(c.Type, out var currentTypeCount)
|
||||
? currentTypeCount + 1
|
||||
: 1;
|
||||
}
|
||||
|
||||
@@ -113,15 +113,9 @@ namespace Bit.App.Pages
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||
var error = await _cipherService.ShareWithServerAsync(cipherView, OrganizationId, checkedCollectionIds);
|
||||
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);
|
||||
|
||||
364
src/App/Resources/AppResources.Designer.cs
generated
364
src/App/Resources/AppResources.Designer.cs
generated
@@ -229,6 +229,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Account fingerprint phrase.
|
||||
/// </summary>
|
||||
public static string AccountFingerprintPhrase {
|
||||
get {
|
||||
return ResourceManager.GetString("AccountFingerprintPhrase", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Locked.
|
||||
/// </summary>
|
||||
@@ -256,6 +265,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Account logged out..
|
||||
/// </summary>
|
||||
public static string AccountLoggedOutBiometricExceeded {
|
||||
get {
|
||||
return ResourceManager.GetString("AccountLoggedOutBiometricExceeded", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Account logged out successfully.
|
||||
/// </summary>
|
||||
@@ -274,6 +292,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Account security.
|
||||
/// </summary>
|
||||
public static string AccountSecurity {
|
||||
get {
|
||||
return ResourceManager.GetString("AccountSecurity", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Switched to next available account.
|
||||
/// </summary>
|
||||
@@ -346,6 +373,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Additional options.
|
||||
/// </summary>
|
||||
public static string AdditionalOptions {
|
||||
get {
|
||||
return ResourceManager.GetString("AdditionalOptions", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Add new attachment.
|
||||
/// </summary>
|
||||
@@ -418,6 +454,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to addy.io.
|
||||
/// </summary>
|
||||
public static string AddyIo {
|
||||
get {
|
||||
return ResourceManager.GetString("AddyIo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Admin approval requested.
|
||||
/// </summary>
|
||||
@@ -508,15 +553,6 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to addy.io.
|
||||
/// </summary>
|
||||
public static string AddyIo {
|
||||
get {
|
||||
return ResourceManager.GetString("AddyIo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to A notification has been sent to your device..
|
||||
/// </summary>
|
||||
@@ -553,6 +589,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Appearance.
|
||||
/// </summary>
|
||||
public static string Appearance {
|
||||
get {
|
||||
return ResourceManager.GetString("Appearance", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to App extension.
|
||||
/// </summary>
|
||||
@@ -877,6 +922,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The Android Autofill Framework is used to assist in filling login information into other apps on your device..
|
||||
/// </summary>
|
||||
public static string AutofillServicesExplanationLong {
|
||||
get {
|
||||
return ResourceManager.GetString("AutofillServicesExplanationLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your logins are now easily accessible right from your keyboard while logging into apps and websites..
|
||||
/// </summary>
|
||||
@@ -1264,6 +1318,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bitwarden Help Center.
|
||||
/// </summary>
|
||||
public static string BitwardenHelpCenter {
|
||||
get {
|
||||
return ResourceManager.GetString("BitwardenHelpCenter", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Black.
|
||||
/// </summary>
|
||||
@@ -1436,11 +1499,11 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?.
|
||||
/// Looks up a localized string similar to You can change your master password on the Bitwarden web app..
|
||||
/// </summary>
|
||||
public static string ChangePasswordConfirmation {
|
||||
public static string ChangeMasterPasswordDescriptionLong {
|
||||
get {
|
||||
return ResourceManager.GetString("ChangePasswordConfirmation", resourceCulture);
|
||||
return ResourceManager.GetString("ChangeMasterPasswordDescriptionLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1597,6 +1660,24 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Contact Bitwarden support.
|
||||
/// </summary>
|
||||
public static string ContactBitwardenSupport {
|
||||
get {
|
||||
return ResourceManager.GetString("ContactBitwardenSupport", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com..
|
||||
/// </summary>
|
||||
public static string ContactSupportDescriptionLong {
|
||||
get {
|
||||
return ResourceManager.GetString("ContactSupportDescriptionLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Continue.
|
||||
/// </summary>
|
||||
@@ -1606,6 +1687,51 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Continue to app store?.
|
||||
/// </summary>
|
||||
public static string ContinueToAppStore {
|
||||
get {
|
||||
return ResourceManager.GetString("ContinueToAppStore", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Continue to contact support?.
|
||||
/// </summary>
|
||||
public static string ContinueToContactSupport {
|
||||
get {
|
||||
return ResourceManager.GetString("ContinueToContactSupport", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Continue to Help center?.
|
||||
/// </summary>
|
||||
public static string ContinueToHelpCenter {
|
||||
get {
|
||||
return ResourceManager.GetString("ContinueToHelpCenter", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Continue to web app?.
|
||||
/// </summary>
|
||||
public static string ContinueToWebApp {
|
||||
get {
|
||||
return ResourceManager.GetString("ContinueToWebApp", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Continue to {0}?.
|
||||
/// </summary>
|
||||
public static string ContinueToX {
|
||||
get {
|
||||
return ResourceManager.GetString("ContinueToX", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy.
|
||||
/// </summary>
|
||||
@@ -1615,6 +1741,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy app information.
|
||||
/// </summary>
|
||||
public static string CopyAppInformation {
|
||||
get {
|
||||
return ResourceManager.GetString("CopyAppInformation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy application.
|
||||
/// </summary>
|
||||
@@ -1697,7 +1832,7 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login..
|
||||
/// Looks up a localized string similar to If a login has an authenticator key, copy the TOTP verification code to your clipboard when you auto-fill the login..
|
||||
/// </summary>
|
||||
public static string CopyTotpAutomaticallyDescription {
|
||||
get {
|
||||
@@ -1733,11 +1868,11 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Created {0}.
|
||||
/// Looks up a localized string similar to Created {0}, {1}.
|
||||
/// </summary>
|
||||
public static string CreatedX {
|
||||
public static string CreatedXY {
|
||||
get {
|
||||
return ResourceManager.GetString("CreatedX", resourceCulture);
|
||||
return ResourceManager.GetString("CreatedXY", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1904,11 +2039,11 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Choose the dark theme to use when using Default (System) theme while your device's dark mode is in use..
|
||||
/// Looks up a localized string similar to Choose the dark theme to use when your device’s dark mode is in use.
|
||||
/// </summary>
|
||||
public static string DefaultDarkThemeDescription {
|
||||
public static string DefaultDarkThemeDescriptionLong {
|
||||
get {
|
||||
return ResourceManager.GetString("DefaultDarkThemeDescription", resourceCulture);
|
||||
return ResourceManager.GetString("DefaultDarkThemeDescriptionLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2353,6 +2488,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Encryption key migration required. Please login through the web vault to update your encryption key..
|
||||
/// </summary>
|
||||
public static string EncryptionKeyMigrationRequiredDescriptionLong {
|
||||
get {
|
||||
return ResourceManager.GetString("EncryptionKeyMigrationRequiredDescriptionLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Enter your account email address to receive your master password hint..
|
||||
/// </summary>
|
||||
@@ -2542,6 +2686,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Explore more features of your Bitwarden account on the web app..
|
||||
/// </summary>
|
||||
public static string ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp {
|
||||
get {
|
||||
return ResourceManager.GetString("ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Export vault.
|
||||
/// </summary>
|
||||
@@ -3622,6 +3775,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website..
|
||||
/// </summary>
|
||||
public static string LearnAboutOrganizationsDescriptionLong {
|
||||
get {
|
||||
return ResourceManager.GetString("LearnAboutOrganizationsDescriptionLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Learn more.
|
||||
/// </summary>
|
||||
@@ -3631,6 +3793,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Learn more about how to use Bitwarden on the Help center..
|
||||
/// </summary>
|
||||
public static string LearnMoreAboutHowToUseBitwardenOnTheHelpCenter {
|
||||
get {
|
||||
return ResourceManager.GetString("LearnMoreAboutHowToUseBitwardenOnTheHelpCenter", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Learn about organizations.
|
||||
/// </summary>
|
||||
@@ -4786,6 +4957,24 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to One hour and one minute.
|
||||
/// </summary>
|
||||
public static string OneHourAndOneMinute {
|
||||
get {
|
||||
return ResourceManager.GetString("OneHourAndOneMinute", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to One hour and {0} minutes.
|
||||
/// </summary>
|
||||
public static string OneHourAndXMinute {
|
||||
get {
|
||||
return ResourceManager.GetString("OneHourAndXMinute", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 1 minute.
|
||||
/// </summary>
|
||||
@@ -5345,6 +5534,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Help others find out if Bitwarden is right for them. Visit the app store and leave a rating now..
|
||||
/// </summary>
|
||||
public static string RateAppDescriptionLong {
|
||||
get {
|
||||
return ResourceManager.GetString("RateAppDescriptionLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Rate the app.
|
||||
/// </summary>
|
||||
@@ -5948,6 +6146,24 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Session timeout.
|
||||
/// </summary>
|
||||
public static string SessionTimeout {
|
||||
get {
|
||||
return ResourceManager.GetString("SessionTimeout", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Session timeout action.
|
||||
/// </summary>
|
||||
public static string SessionTimeoutAction {
|
||||
get {
|
||||
return ResourceManager.GetString("SessionTimeoutAction", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Set master password.
|
||||
/// </summary>
|
||||
@@ -6254,6 +6470,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Sync now.
|
||||
/// </summary>
|
||||
public static string SyncNow {
|
||||
get {
|
||||
return ResourceManager.GetString("SyncNow", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Sync vault now.
|
||||
/// </summary>
|
||||
@@ -6434,16 +6659,6 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to This item cannot be shared with the organization because there is one already with the same passkey..
|
||||
/// </summary>
|
||||
public static string ThisItemCannotBeSharedWithTheOrganizationBecauseThereIsOneAlreadyWithTheSamePasskey {
|
||||
get {
|
||||
return ResourceManager.GetString("ThisItemCannotBeSharedWithTheOrganizationBecauseThereIsOneAlreadyWithTheSamePassk" +
|
||||
"ey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to This request is no longer valid.
|
||||
/// </summary>
|
||||
@@ -6498,6 +6713,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Too many attempts.
|
||||
/// </summary>
|
||||
public static string TooManyAttempts {
|
||||
get {
|
||||
return ResourceManager.GetString("TooManyAttempts", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to TOTP.
|
||||
/// </summary>
|
||||
@@ -6598,11 +6822,11 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?.
|
||||
/// Looks up a localized string similar to Make your account more secure by setting up two-step login in the Bitwarden web app..
|
||||
/// </summary>
|
||||
public static string TwoStepLoginConfirmation {
|
||||
public static string TwoStepLoginDescriptionLong {
|
||||
get {
|
||||
return ResourceManager.GetString("TwoStepLoginConfirmation", resourceCulture);
|
||||
return ResourceManager.GetString("TwoStepLoginDescriptionLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6751,7 +6975,7 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unlocking may fail due to insufficient memory. Decrease your KDF memory settings to resolve..
|
||||
/// Looks up a localized string similar to Unlocking may fail due to insufficient memory. Decrease your KDF memory settings or set up biometric unlock to resolve..
|
||||
/// </summary>
|
||||
public static string UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve {
|
||||
get {
|
||||
@@ -6759,6 +6983,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unlock options.
|
||||
/// </summary>
|
||||
public static string UnlockOptions {
|
||||
get {
|
||||
return ResourceManager.GetString("UnlockOptions", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unlock vault.
|
||||
/// </summary>
|
||||
@@ -6957,6 +7190,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay..
|
||||
/// </summary>
|
||||
public static string UseInlineAutofillExplanationLong {
|
||||
get {
|
||||
return ResourceManager.GetString("UseInlineAutofillExplanationLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Username.
|
||||
/// </summary>
|
||||
@@ -6976,7 +7218,7 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use this device to approve login requests made from other devices..
|
||||
/// Looks up a localized string similar to Use this device to approve login requests made from other devices.
|
||||
/// </summary>
|
||||
public static string UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices {
|
||||
get {
|
||||
@@ -7020,6 +7262,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Vault.
|
||||
/// </summary>
|
||||
public static string Vault {
|
||||
get {
|
||||
return ResourceManager.GetString("Vault", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Vault: {0}.
|
||||
/// </summary>
|
||||
@@ -7281,15 +7532,6 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Visit our website to get help, news, email us, and/or learn more about how to use Bitwarden..
|
||||
/// </summary>
|
||||
public static string VisitOurWebsiteDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("VisitOurWebsiteDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Warning.
|
||||
/// </summary>
|
||||
@@ -7416,6 +7658,33 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} hours.
|
||||
/// </summary>
|
||||
public static string XHours {
|
||||
get {
|
||||
return ResourceManager.GetString("XHours", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} hours and one minute.
|
||||
/// </summary>
|
||||
public static string XHoursAndOneMinute {
|
||||
get {
|
||||
return ResourceManager.GetString("XHoursAndOneMinute", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} hours and {1} minutes.
|
||||
/// </summary>
|
||||
public static string XHoursAndYMinutes {
|
||||
get {
|
||||
return ResourceManager.GetString("XHoursAndYMinutes", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} minutes ago.
|
||||
/// </summary>
|
||||
@@ -7443,6 +7712,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You can import data to your vault on {0}..
|
||||
/// </summary>
|
||||
public static string YouCanImportDataToYourVaultOnX {
|
||||
get {
|
||||
return ResourceManager.GetString("YouCanImportDataToYourVaultOnX", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You cannot edit passkey application because it would invalidate the passkey.
|
||||
/// </summary>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Besoek ons webwerf</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>Besoek ons webwerf vir hulp, nuus, om ’n e-pos aan ons te stuur en/of om meer te leer oor die gebruik van Bitwarden.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Webwerf</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Verander hoofwagwoord</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>U kan u hoofwagwoord op die bitwarden.com-webkluis verander. Wil u die webwerf nou besoek?</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>Sluit</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>Tweestapaantekening</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>Tweestapsaantekening maak u rekening veiliger deur u aantekenpoging te bevestig met ’n ander toestel soos ’n beveiligingsleutel, waarmerktoep, SMS, telefoonoproep of e-pos. U kan tweestapsaantekening in die webkluis op bitwarden.com aktiveer. Wil u die webwerf nou besoek?</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>Ontgrendel met {0}</value>
|
||||
</data>
|
||||
@@ -954,6 +945,9 @@ Skandering gebeur outomaties.</value>
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>U kan nie hierdie funksie gebruik tot u u enkripsiesleutel bygewerk het nie.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Encryption key migration required. Please login through the web vault to update your encryption key.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>Leer meer</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@ Skandering gebeur outomaties.</value>
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Verstekdonkertema</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Kies welke donkertema om te gebruik as stelseltema wanneer u toestel se donkertema geaktiveer is.</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Kopieer notas</value>
|
||||
</data>
|
||||
@@ -2651,10 +2642,6 @@ Wil u na die rekening omskakel?</value>
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Passkeys</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Created {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Application</value>
|
||||
</data>
|
||||
@@ -2761,4 +2748,116 @@ Wil u na die rekening omskakel?</value>
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Logging in on</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Vault</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Account security</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Bitwarden Help Center</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Contact Bitwarden support</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Copy app information</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Sync now</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Unlock options</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Session timeout</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Session timeout action</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Account fingerprint phrase</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>One hour and one minute</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>One hour and {0} minutes</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} hours and one minute</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} hours and {1} minutes</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} hours</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Additional options</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Continue to web app?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Continue to {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Continue to Help center?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Continue to contact support?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Continue to app store?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Make your account more secure by setting up two-step login in the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>You can change your master password on the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>You can import data to your vault on {0}.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Learn more about how to use Bitwarden on the Help center.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Explore more features of your Bitwarden account on the web app.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Help others find out if Bitwarden is right for them. Visit the app store and leave a rating now.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Choose the dark theme to use when your device’s dark mode is in use</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Created {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>Too many attempts</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Account logged out.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>زيارة موقعنا على الانترنت</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>قم بزيارة موقعنا الإلكتروني للحصول على المساعدة، واقرأ الأخبار، واكتب لنا و/أو تعلم كيفية استخدام bitwarden بشكل أفضل.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>موقع الويب</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>تغيير كلمة المرور الرئيسية</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>يمكنك تغيير كلمة المرور الرئيسية من خزنة الويب في bitwarden.com. هل تريد زيارة الموقع الآن؟</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>إغلاق</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>تحديد المصادقة الثنائية</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>تجعل المصادقة الثنائية المعامل حسابك أكثر أمانًا من خلال طلب إدخال رمز أمان مع كل معرف من تطبيق المصادقة. يمكن تنشيط تعريف العامل المزدوج في خزنة الويب في bitwarden.com هل تريد زيارة الموقع الآن؟</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>فتح مع {0}</value>
|
||||
</data>
|
||||
@@ -954,6 +945,9 @@
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>لا يمكنك استخدام هذه الميزة حتى تقوم بتحديث مفتاح التشفير الخاص بك.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>مطلوب ترحيل مفتاح التشفير. الرجاء تسجيل الدخول بواسطة مخزن الويب لتحديث مفتاح التشفير الخاص بك.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>اعرف المزيد</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>السمة المظلمة الافتراضية</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>اختر السمة المظلمة لاستخدامها عند استخدام السمة الافتراضية (النظام) أثناء تمكين الوضع المظلم لجهازك</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>نسخ الملاحظة</value>
|
||||
</data>
|
||||
@@ -2652,10 +2643,6 @@
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>مفاتيح المرور</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>أُنشِئ {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>تطبيق</value>
|
||||
</data>
|
||||
@@ -2762,4 +2749,116 @@
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>جارٍ تسجيل الدخول</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Vault</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Account security</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Bitwarden Help Center</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Contact Bitwarden support</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Copy app information</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Sync now</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Unlock options</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Session timeout</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Session timeout action</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Account fingerprint phrase</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>One hour and one minute</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>One hour and {0} minutes</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} hours and one minute</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} hours and {1} minutes</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} hours</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Additional options</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Continue to web app?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Continue to {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Continue to Help center?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Continue to contact support?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Continue to app store?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Make your account more secure by setting up two-step login in the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>You can change your master password on the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>You can import data to your vault on {0}.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Learn more about how to use Bitwarden on the Help center.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Explore more features of your Bitwarden account on the web app.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Help others find out if Bitwarden is right for them. Visit the app store and leave a rating now.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Choose the dark theme to use when your device’s dark mode is in use</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Created {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>لقد أجريت محاولات كثيرة</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>تم تسجيل الخروج من الحساب.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Veb saytımızı ziyarət edin</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>Kömək almaq, xəbərlərə baxmaq, bizə e-poçt göndərmək və/və ya "Bitwarden"in necə istifadə edildiyini öyrənmək üçün veb saytımızı ziyarət edin.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Veb sayt</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Ana parolu dəyişdir</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>Ana parolunuzu bitwarden.com veb anbarında dəyişdirə bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>Bağla</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>İki mərhələli giriş</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>İki mərhələli giriş, güvənlik açarı, kimlik təsdiqləyici tətbiq, SMS, telefon zəngi və ya e-poçt kimi digər cihazlarla girişinizi təsdiqləməyinizi tələb edərək hesabınızı daha da güvənli edir. İki mərhələli giriş, bitwarden.com veb anbarında fəallaşdırıla bilər. Veb saytı indi ziyarət etmək istəyirsiniz?</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>{0} ilə kilidi açın</value>
|
||||
</data>
|
||||
@@ -954,6 +945,9 @@ Skan prosesi avtomatik baş tutacaq.</value>
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>Şifrələmə açarınızı güncəlləyənə qədər bu özəlliyi istifadə edə bilməzsiniz.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Şifrələmə açarının daşınması tələb olunur. Şifrələmə açarınızı güncəlləmək üçün zəhmət olmasa veb anbar üzərindən giriş edin.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>Daha ətraflı</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@ Skan prosesi avtomatik baş tutacaq.</value>
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>İlkin tünd tema</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Cihazınızda tünd rejim fəal olanda İlkin (Sistem) temanı istifadə edərkən istifadə ediləcək tünd temanı seçin</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Qeydləri kopyala</value>
|
||||
</data>
|
||||
@@ -2645,38 +2636,34 @@ Bu hesaba keçmək istəyirsiniz?</value>
|
||||
<value>Bu cihazı xatırla</value>
|
||||
</data>
|
||||
<data name="Passkey" xml:space="preserve">
|
||||
<value>Passkey</value>
|
||||
<value>Keçid açarı</value>
|
||||
</data>
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Passkeys</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Created {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
<value>Keçid açarı</value>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Application</value>
|
||||
<value>Tətbiq</value>
|
||||
</data>
|
||||
<data name="YouCannotEditPasskeyApplicationBecauseItWouldInvalidateThePasskey" xml:space="preserve">
|
||||
<value>You cannot edit passkey application because it would invalidate the passkey</value>
|
||||
<value>Keçid açarı tətbiqinə düzəliş edə bilməzsiniz, çünki bu, keçid açarını yararsız edəcək</value>
|
||||
</data>
|
||||
<data name="PasskeyWillNotBeCopied" xml:space="preserve">
|
||||
<value>Passkey will not be copied</value>
|
||||
<value>Keçid açarı kopyalanmır</value>
|
||||
</data>
|
||||
<data name="ThePasskeyWillNotBeCopiedToTheClonedItemDoYouWantToContinueCloningThisItem" xml:space="preserve">
|
||||
<value>The passkey will not be copied to the cloned item. Do you want to continue cloning this item?</value>
|
||||
<value>Keçid açarı, klonlanmış elementə kopyalanmayacaq. Bu elementi klonlamağa davam etmək istəyirsiniz?</value>
|
||||
</data>
|
||||
<data name="CopyApplication" xml:space="preserve">
|
||||
<value>Copy application</value>
|
||||
<value>Tətbiqi kopyala</value>
|
||||
</data>
|
||||
<data name="AvailableForTwoStepLogin" xml:space="preserve">
|
||||
<value>Available for two-step login</value>
|
||||
<value>İki addımlı giriş üçün əlçatandır</value>
|
||||
</data>
|
||||
<data name="MasterPasswordRePromptHelp" xml:space="preserve">
|
||||
<value>Ana parolu təkrar soruş köməyi</value>
|
||||
</data>
|
||||
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
|
||||
<value>Unlocking may fail due to insufficient memory. Decrease your KDF memory settings or set up biometric unlock to resolve.</value>
|
||||
<value>Yetərsiz yaddaşa görə kilid açma uğursuz ola bilər. Həll etmək üçün KDF yaddaş tənzimləmələrinizi azaldın və ya biometrik kilid açmanı quraşdırın.</value>
|
||||
</data>
|
||||
<data name="InvalidAPIKey" xml:space="preserve">
|
||||
<value>Yararsız API açarı</value>
|
||||
@@ -2703,7 +2690,7 @@ Bu hesaba keçmək istəyirsiniz?</value>
|
||||
<value>Anbar vaxt bitməsi əməliyyatı "çıxış et" olaraq dəyişdirildi</value>
|
||||
</data>
|
||||
<data name="ThisItemCannotBeSharedWithTheOrganizationBecauseThereIsOneAlreadyWithTheSamePasskey" xml:space="preserve">
|
||||
<value>This item cannot be shared with the organization because there is one already with the same passkey.</value>
|
||||
<value>Bu element təşkilatla paylaşıla bilmir, çünki eyni keçid açarına sahib bir element artıq mövcuddur.</value>
|
||||
</data>
|
||||
<data name="BlockAutoFill" xml:space="preserve">
|
||||
<value>Avto-doldurmanı əngəllə</value>
|
||||
@@ -2760,4 +2747,116 @@ Bu hesaba keçmək istəyirsiniz?</value>
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Giriş edilir</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Anbar</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Görünüş</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Hesab güvənliyi</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Bitwarden Kömək Mərkəzi</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Bitwarden dəstəyi ilə əlaqə saxla</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Tətbiq məlumatlarını kopyala</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>İndi sinxr.</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Kilid açma seçimləri</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Seans vaxt bitməsi</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Seans vaxt bitmə əməliyyatı</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Hesab barmaq izi ifadəsi</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>Bir saat bir dəqiqə</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>Bir saat {0} dəqiqə</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} saat bir dəqiqə</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} saat {1} dəqiqə</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} saat</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>Android Avto-doldurma Çərçivəsi, giriş məlumatlarını cihazınızdakı digər tətbiqlərə doldurmağa kömək etmək üçün istifadə olunur.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Seçdiyiniz klaviatura dəstəkləyirsə sətir daxili avto-doldurmani istifadə edin. Əks halda, ilkin örtük istifadə edin.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Əlavə seçimlər</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Veb tətbiqlə davam edilsin?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>{0} ilə davam edilsin?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Kömək mərkəzi ilə davam edilsin?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Dəstək komandası ilə əlaqə qurmağa davam edilsin?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Tətbiq mağazası ilə davam edilsin?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden veb tətbiqində iki addımlı girişi quraraq hesabınızı daha güvənli edin.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>Ana parolunuzu Bitwarden veb tətbiqində dəyişdirə bilərsiniz.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>Datanı {0} üzərindən anbarınıza köçürə bilərsiniz.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Kömək mərkəzində Bitwarden-in necə istifadə ediləcəyi ilə bağlı ətraflı öyrənin.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Axtardığınızı tapa bilmirsiniz? bitwarden.com üzərindən Bitwarden dəstəyinə müraciət edin.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Veb tətbiqində Bitwarden hesabınızın daha çox özəlliyini kəşf edin.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden, bir təşkilat hesabı istifadə edərək anbar elementlərinizi başqaları ilə paylaşmağınıza icazə verər. bitwarden.com veb saytında ətraflı öyrənin.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Başqalarının Bitwarden-in onlar üçün uyğun olub-olmadığını öyrənməkdə kömək edin. Tətbiq mağazasını ziyarət edin və tətbiqimizi qiymətləndirin.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Cihazınızda qaranlıq rejim açıq olduqda istifadə ediləcək qaranlıq temanı seçin</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Yaradıldı: {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>Həddən artıq cəhd</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Hesabdan çıxış edildi.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Наведайце наш вэб-сайт</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>Наведайце наш вэб-сайт, каб атрымаць дапамогу, прачытаць апошнія навіны, звязацца з намі і/або даведацца больш пра тое, як карыстацца Bitwarden.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Вэб-сайт</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Змяніць асноўны пароль</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>Вы можаце змяніць свой асноўны пароль у вэб-сховішчы на bitwarden.com. Перайсці на вэб-сайт зараз?</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>Закрыць</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>Двухэтапны ўваход</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>Двухэтапны ўваход робіць ваш уліковы запіс больш бяспечным, патрабуючы пацвярджэнне ўваходу на іншай прыладзе з выкарыстаннем ключа бяспекі, праграмы аўтэнтыфікацыі, SMS, тэлефоннага званка або электроннай пошты. Двухэтапны ўваход уключаецца на bitwarden.com. Перайсці на вэб-сайт, каб зрабіць гэта?</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>Разблакіраваць з {0}</value>
|
||||
</data>
|
||||
@@ -953,6 +944,9 @@
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>Вы не зможаце выкарыстоўваць гэту функцыю, пакуль не абнавіце свой ключ шыфравання.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Encryption key migration required. Please login through the web vault to update your encryption key.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>Даведацца больш</value>
|
||||
</data>
|
||||
@@ -1554,9 +1548,6 @@
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Прадвызначана цёмная тэма</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Выбіраць цёмную тэму для яе выкарыстання ў выпадку, калі яна з'яўляецца прадвызначанай (сістэмнай) для вашай прылады.</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Скапіяваць нататку</value>
|
||||
</data>
|
||||
@@ -2651,10 +2642,6 @@
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Passkeys</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Created {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Праграма</value>
|
||||
</data>
|
||||
@@ -2761,4 +2748,116 @@
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Logging in on</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Vault</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Account security</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Bitwarden Help Center</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Contact Bitwarden support</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Copy app information</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Sync now</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Unlock options</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Session timeout</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Session timeout action</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Account fingerprint phrase</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>One hour and one minute</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>One hour and {0} minutes</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} hours and one minute</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} hours and {1} minutes</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} hours</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Additional options</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Continue to web app?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Continue to {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Continue to Help center?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Continue to contact support?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Continue to app store?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Make your account more secure by setting up two-step login in the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>You can change your master password on the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>You can import data to your vault on {0}.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Learn more about how to use Bitwarden on the Help center.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Explore more features of your Bitwarden account on the web app.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Help others find out if Bitwarden is right for them. Visit the app store and leave a rating now.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Choose the dark theme to use when your device’s dark mode is in use</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Created {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>Too many attempts</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Account logged out.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Посетете нашия сайт</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>Посетете сайта ни за помощ, новини, начини да се свържете с нас и как да ползвате Bitwarden.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Сайт</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Промяна на главната парола</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>Главната парола на трезор може да се промени чрез сайта bitwarden.com. Искате ли да го посетите?</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>Затваряне</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>Двустепенно удостоверяване</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>Двустепенното вписване защищава регистрацията ви като ви кара да потвърдите влизането си чрез устройство-ключ, приложение за идентификация, мобилно съобщение, телефонно обаждане или е-поща. Двустепенното вписване може да се включи чрез сайта bitwarden.com. Искате ли да го посетите?</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>Отключване с {0}</value>
|
||||
</data>
|
||||
@@ -954,6 +945,9 @@
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>Трябва да обновите шифриращия си ключ, за да използвате тази възможност.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Необходима е промяна на шифриращия ключ. Впишете се в трезора си по уеб, за да обновите своя шифриращ ключ.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>Научете повече</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Стандартен тъмен облик</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Изберете тъмния облик, който да се ползва, когато е избран стандартният (от системата) облик и тъмният режим на устройството Ви е включен.</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Копиране на бележките</value>
|
||||
</data>
|
||||
@@ -2651,10 +2642,6 @@ select Add TOTP to store the key safely</value>
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Passkeys</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Създадено на {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Приложение</value>
|
||||
</data>
|
||||
@@ -2761,4 +2748,116 @@ select Add TOTP to store the key safely</value>
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Вписване в</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Трезор</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Външен вид</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Защита на регистрацията</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Помощен център на Битуорден</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Свържете се с поддръжката на Битуорден</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Копиране на информацията за приложението</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Синхронизиране сега</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Unlock options</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Изтичане на времето за сесията</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Действие при изтичане на времето за сесията</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Уникална фраза, идентифицираща регистрацията Ви</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>Един час и една минута</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>Един час и {0} минути</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} часа и една минута</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} часа и {1} минути</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} часа</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>Подсистемата за автоматично попълване на Андроид се използва за попълване на данните за вход в други приложения на устройството.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Използвайте вграденото автоматично попълване, ако избраната от Вас клавиатура го поддържа. Иначе използвайте слоя по подразбиране.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Допълнителни настройки</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Продължаване към уеб приложението?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Продължаване към {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Продължаване към помощния център?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Продължаване към връзка с поддръжката?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Продължаване към магазина за приложения?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Подобрете защитата на регистрацията си, като настроите двустепенното удостоверяване в уеб приложението на Битуорден.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>Може да промените главната си парола в уеб приложението на Битуорден.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>Може да внесете данни в трезора си на {0}.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Научете повече относно това как да използвате Битуорден в помощния център.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Не намирате това, което търсите? Свържете се с поддръжката на Битуорден на bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Разгледайте още от възможностите на регистрацията си в Битуорден в уеб приложението.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Битуорден позволява да споделяте части от трезора си чрез използването на организация. Научете повече на сайта bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Помогнете на другите да разберат дали Битуорден е подходящ за тях. Посетете магазина за приложения и дайте оценка още сега.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Изберете коя тъмна тема да се използва когато е включен тъмният режим на устройството Ви</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Създадено на {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>Твърде много опити</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Акаунтът е отписан.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>আমাদের ওয়েবসাইটে ঢু মেরে আসুন</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>সাহায্য পেতে, খবর পেতে, আমাদের ইমেল করতে এবং/অথবা বিটওয়ার্ডেন কীভাবে ব্যবহার করবেন সে সম্পর্কে আরও জানতে আমাদের ওয়েবসাইট দেখুন।</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>ওয়েবসাইট</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>প্রধান পাসওয়ার্ড পরিবর্তন</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>আপনি bitwarden.com ওয়েব ভল্ট থেকে প্রধান পাসওয়ার্ডটি পরিবর্তন করতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>বন্ধ করুন</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>Two-step login</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>Unlock with {0}</value>
|
||||
</data>
|
||||
@@ -916,7 +907,7 @@ Scanning will happen automatically.</value>
|
||||
<value>Copy TOTP</value>
|
||||
</data>
|
||||
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||
<value>If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login.</value>
|
||||
<value>If a login has an authenticator key, copy the TOTP verification code to your clipboard when you auto-fill the login.</value>
|
||||
</data>
|
||||
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||
<value>Copy TOTP automatically</value>
|
||||
@@ -954,6 +945,9 @@ Scanning will happen automatically.</value>
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>আপনি আপনার এনক্রিপশন কী হালনাগাদ না করা পর্যন্ত এই বৈশিষ্ট্যটি ব্যবহার করতে পারবেন না।</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Encryption key migration required. Please login through the web vault to update your encryption key.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>আরও জানুন</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@ Scanning will happen automatically.</value>
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Default dark theme</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Choose the dark theme to use when using Default (System) theme while your device's dark mode is in use.</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Copy note</value>
|
||||
</data>
|
||||
@@ -2364,7 +2355,7 @@ select Add TOTP to store the key safely</value>
|
||||
<value>Approve login requests</value>
|
||||
</data>
|
||||
<data name="UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices" xml:space="preserve">
|
||||
<value>Use this device to approve login requests made from other devices.</value>
|
||||
<value>Use this device to approve login requests made from other devices</value>
|
||||
</data>
|
||||
<data name="AllowNotifications" xml:space="preserve">
|
||||
<value>Allow notifications</value>
|
||||
@@ -2652,10 +2643,6 @@ Do you want to switch to this account?</value>
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Passkeys</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Created {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Application</value>
|
||||
</data>
|
||||
@@ -2762,4 +2749,116 @@ Do you want to switch to this account?</value>
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Logging in on</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Vault</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Account security</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Bitwarden Help Center</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Contact Bitwarden support</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Copy app information</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Sync now</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Unlock options</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Session timeout</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Session timeout action</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Account fingerprint phrase</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>One hour and one minute</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>One hour and {0} minutes</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} hours and one minute</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} hours and {1} minutes</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} hours</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Additional options</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Continue to web app?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Continue to {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Continue to Help center?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Continue to contact support?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Continue to app store?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Make your account more secure by setting up two-step login in the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>You can change your master password on the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>You can import data to your vault on {0}.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Learn more about how to use Bitwarden on the Help center.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Explore more features of your Bitwarden account on the web app.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Help others find out if Bitwarden is right for them. Visit the app store and leave a rating now.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Choose the dark theme to use when your device’s dark mode is in use</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Created {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>Too many attempts</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Account logged out.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Posjetite našu Web Stranicu </value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>Posjeti našu Web stranicu kako bi pronašli pomoć, novosti, poslali nam e-poštu i/ili saznali više o tome kako koristiti Bitwarden.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Web stranica</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Promijenite glavnu lozinku</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>Možete da promjenite svoju glavnu lozinku na bitwarden.com web trezoru. Da li želite da posjetite web stranicu sada?</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>Zatvori</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>Prijava u dva koraka</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>Prijava u dva koraka čini Vaš nalog sigurnijim tako što zahteva da verifikujete svoje podatke pomoću drugog uređaja, kao što su bezbednosni ključ, aplikacija za potvrdu autentičnosti, SMS, telefonski poziv ili e-pošta. Prijavljivanje u dva koraka može se omogućiti na bitwarden.com veb trezoru. Da li želite da posetite veb stranicu sada?</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>Otključajte sa {0}</value>
|
||||
</data>
|
||||
@@ -954,6 +945,9 @@ Skeniranje će biti izvršeno automatski.</value>
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>Ovu funkciju ne možete koristiti dok ne ažurirate ključ za šifrovanje.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Encryption key migration required. Please login through the web vault to update your encryption key.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>Saznajte više</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@ Skeniranje će biti izvršeno automatski.</value>
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Zadana tamna tema</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Odaberi korištenje tamne teme kada je uređaj već zadano koristi.</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Kopiraj bilješke</value>
|
||||
</data>
|
||||
@@ -2362,7 +2353,7 @@ Skeniranje će biti izvršeno automatski.</value>
|
||||
<value>Approve login requests</value>
|
||||
</data>
|
||||
<data name="UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices" xml:space="preserve">
|
||||
<value>Use this device to approve login requests made from other devices.</value>
|
||||
<value>Use this device to approve login requests made from other devices</value>
|
||||
</data>
|
||||
<data name="AllowNotifications" xml:space="preserve">
|
||||
<value>Allow notifications</value>
|
||||
@@ -2650,10 +2641,6 @@ Skeniranje će biti izvršeno automatski.</value>
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Passkeys</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Created {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Application</value>
|
||||
</data>
|
||||
@@ -2760,4 +2747,116 @@ Skeniranje će biti izvršeno automatski.</value>
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Logging in on</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Vault</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Account security</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Bitwarden Help Center</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Contact Bitwarden support</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Copy app information</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Sync now</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Unlock options</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Session timeout</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Session timeout action</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Account fingerprint phrase</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>One hour and one minute</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>One hour and {0} minutes</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} hours and one minute</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} hours and {1} minutes</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} hours</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Additional options</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Continue to web app?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Continue to {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Continue to Help center?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Continue to contact support?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Continue to app store?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Make your account more secure by setting up two-step login in the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>You can change your master password on the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>You can import data to your vault on {0}.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Learn more about how to use Bitwarden on the Help center.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Explore more features of your Bitwarden account on the web app.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Help others find out if Bitwarden is right for them. Visit the app store and leave a rating now.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Choose the dark theme to use when your device’s dark mode is in use</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Created {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>Too many attempts</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Account logged out.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Visiteu el nostre lloc web</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>Visiteu el nostre lloc web per obtenir ajuda, notícies, enviar-nos un correu electrònic i obtenir més informació sobre com utilitzar Bitwarden.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Lloc web</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Canvia la contrasenya mestra</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>Podeu canviar la contrasenya mestra a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>Tanca</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>Inici de sessió en dues passes</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>L'inici de sessió en dues passes fa que el vostre compte siga més segur, ja que obliga a verificar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dues passes a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>Desbloqueja amb {0}</value>
|
||||
</data>
|
||||
@@ -954,6 +945,9 @@ L'escaneig es farà automàticament.</value>
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>No podeu utilitzar aquesta característica fins que no actualitzeu la vostra clau de xifratge.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Encryption key migration required. Please login through the web vault to update your encryption key.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>Més informació</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@ L'escaneig es farà automàticament.</value>
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Tema fosc per defecte</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Tria el tema fosc quan feu servir el tema predeterminat (sistema) mentre el mode fosc del vostre dispositiu està habilitat</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Copia nota</value>
|
||||
</data>
|
||||
@@ -2651,10 +2642,6 @@ Voleu canviar a aquest compte?</value>
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Claus de pas</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Creats {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Aplicació</value>
|
||||
</data>
|
||||
@@ -2761,4 +2748,116 @@ Voleu canviar a aquest compte?</value>
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Inici de sessió en</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Vault</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Account security</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Bitwarden Help Center</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Contact Bitwarden support</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Copy app information</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Sync now</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Unlock options</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Session timeout</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Session timeout action</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Account fingerprint phrase</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>One hour and one minute</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>One hour and {0} minutes</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} hours and one minute</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} hours and {1} minutes</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} hours</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Additional options</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Continue to web app?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Continue to {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Continue to Help center?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Continue to contact support?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Continue to app store?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Make your account more secure by setting up two-step login in the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>You can change your master password on the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>You can import data to your vault on {0}.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Learn more about how to use Bitwarden on the Help center.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Explore more features of your Bitwarden account on the web app.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Help others find out if Bitwarden is right for them. Visit the app store and leave a rating now.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Choose the dark theme to use when your device’s dark mode is in use</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Created {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>Too many attempts</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Account logged out.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Navštivte naše webové stránky</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>Navštivte náš web pro získání nápovědy, novinek, kontaktu, nebo návodů, jak používat Bitwarden.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Webová stránka</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Změnit hlavní heslo</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>Hlavní heslo si můžete změnit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>Zavřít</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>Dvoufázové přihlášení</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>Dvoufázové přihlášení činí Váš účet mnohem bezpečnějším díky nutnosti po každém úspěšném přihlášení zadat ověřovací kód získaný z bezpečnostního klíče, aplikace, SMS, telefonního hovoru nebo e-mailu. Dvoufázové přihlášení lze aktivovat na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>Odemknout pomocí {0}</value>
|
||||
</data>
|
||||
@@ -954,6 +945,9 @@ Načtení proběhne automaticky.</value>
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>Dokud neaktualizujete svůj šifrovací klíč, nemůžete tuto funkci použít.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Vyžaduje se migrace šifrovacího klíče. Pro aktualizaci šifrovacího klíče se přihlaste přes webový trezor.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>Dozvědět se více</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@ Načtení proběhne automaticky.</value>
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Výchozí tmavý motiv</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Zvolte motiv, který se bude zobrazovat po aktivování tmavého motivu v systému.</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Kopírovat poznámku</value>
|
||||
</data>
|
||||
@@ -2362,7 +2353,7 @@ Načtení proběhne automaticky.</value>
|
||||
<value>Schválit žádosti o přihlášení</value>
|
||||
</data>
|
||||
<data name="UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices" xml:space="preserve">
|
||||
<value>Použijte toto zařízení pro schvalování žádostí o přihlášení z jiných zařízení.</value>
|
||||
<value>Použít toto zařízení pro schvalování žádostí o přihlášení z jiných zařízení</value>
|
||||
</data>
|
||||
<data name="AllowNotifications" xml:space="preserve">
|
||||
<value>Povolit oznámení</value>
|
||||
@@ -2650,10 +2641,6 @@ Chcete se přepnout na tento účet?</value>
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Přístupové klíče</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Vytvořeno {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Aplikace</value>
|
||||
</data>
|
||||
@@ -2760,4 +2747,116 @@ Chcete se přepnout na tento účet?</value>
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Přihlašování na</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Trezor</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Vzhled</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Zabezpečení účtu</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Centrum nápovědy Bitwarden</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Kontakt na podporu Bitwarden</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Kopírovat informace o aplikaci</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Synchronizovat nyní</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Volby odemknutí</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Časový limit relace</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Akce časového limitu relace</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Fráze otisku prstu účtu</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>Jedna hodina a jedna minuta</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>Jedna hodina a {0} minut</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} hodin a jedna minuta</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} hodin a {1} minut</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} hodin</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>Android Autofill Framework se používá k vyplnění přihlašovacích údajů do jiných aplikací na Vašem zařízení.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Pokud to Vaše vybraná klávesnice podporuje, použije se inline automatické vyplňování. V opačném případě se použije výchozí překrytí.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Další volby</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Pokračovat do webové aplikace?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Pokračovat do {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Pokračovat do centra nápovědy?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Pokračovat v kontaktování podpory?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Pokračovat do obchodu s aplikacemi?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Vytvořte svůj účet bezpečnějším nastavením dvoufázového přihlášení ve webové aplikaci Bitwarden.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>Hlavní heslo můžete změnit na webové aplikaci Bitwarden.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>Data můžete importovat do Vašeho trezoru na {0}.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Přečtěte si více o tom, jak používat Bitwarden v centru nápovědy.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Nenašli jste to, co jste hledali? Na bitwarden.com získejte podporu.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Prozkoumejte další funkce Vašeho účtu Bitwarden ve webové aplikaci.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden Vám umožňuje sdílet položky v trezoru s ostatními prostřednictvím organizace. Více informací naleznete na bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Pomozte ostatním zjistit, zda je Bitwarden pro ně správný. Navštivte obchod s aplikacemi a zanechte hodnocení.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Vybere tmavý motiv při použití tmavého režimu Vašeho zařízení.</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Vytvořeno {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>Příliš mnoho pokusů</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Účet byl odhlášen.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Visit our website</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>Visit our website to get help, news, email us, and/or learn more about how to use Bitwarden.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Gwefan</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Newid y prif gyfrinair</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>Cau</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>Two-step login</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>Datgloi â {0}</value>
|
||||
</data>
|
||||
@@ -916,7 +907,7 @@ Scanning will happen automatically.</value>
|
||||
<value>Copy TOTP</value>
|
||||
</data>
|
||||
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||
<value>If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login.</value>
|
||||
<value>If a login has an authenticator key, copy the TOTP verification code to your clipboard when you auto-fill the login.</value>
|
||||
</data>
|
||||
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||
<value>Copy TOTP automatically</value>
|
||||
@@ -954,6 +945,9 @@ Scanning will happen automatically.</value>
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>You cannot use this feature until you update your encryption key.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Encryption key migration required. Please login through the web vault to update your encryption key.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>Dysgu mwy</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@ Scanning will happen automatically.</value>
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Default dark theme</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Choose the dark theme to use when using Default (System) theme while your device's dark mode is in use.</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Copy note</value>
|
||||
</data>
|
||||
@@ -2364,7 +2355,7 @@ select Add TOTP to store the key safely</value>
|
||||
<value>Approve login requests</value>
|
||||
</data>
|
||||
<data name="UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices" xml:space="preserve">
|
||||
<value>Use this device to approve login requests made from other devices.</value>
|
||||
<value>Use this device to approve login requests made from other devices</value>
|
||||
</data>
|
||||
<data name="AllowNotifications" xml:space="preserve">
|
||||
<value>Caniatáu hysbysiadau</value>
|
||||
@@ -2652,10 +2643,6 @@ Do you want to switch to this account?</value>
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Passkeys</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Crëwyd {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Rhaglen</value>
|
||||
</data>
|
||||
@@ -2762,4 +2749,116 @@ Do you want to switch to this account?</value>
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Logging in on</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Vault</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Account security</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Bitwarden Help Center</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Contact Bitwarden support</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Copy app information</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Sync now</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Unlock options</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Session timeout</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Session timeout action</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Account fingerprint phrase</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>One hour and one minute</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>One hour and {0} minutes</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} hours and one minute</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} hours and {1} minutes</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} hours</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Additional options</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Continue to web app?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Continue to {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Continue to Help center?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Continue to contact support?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Continue to app store?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Make your account more secure by setting up two-step login in the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>You can change your master password on the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>You can import data to your vault on {0}.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Learn more about how to use Bitwarden on the Help center.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Explore more features of your Bitwarden account on the web app.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Help others find out if Bitwarden is right for them. Visit the app store and leave a rating now.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Choose the dark theme to use when your device’s dark mode is in use</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Created {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>Too many attempts</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Account logged out.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Besøg vores websted</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>Besøg vores hjemmeside for at få hjælp, nyheder, e-maile os og/eller få mere at vide om, hvordan du benytter Bitwarden.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Hjemmeside</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Skift hovedadgangskode</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>Du kan skifte din hovedadgangskode i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>Luk</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>To-trins login</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>To-trins login gør din konto mere sikker ved at kræve, at du verificerer dit login med en anden enhed, såsom en sikkerhedsnøgle, autentificeringsapp, SMS, telefonopkald eller e-mail. To-trins login kan aktiveres i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>Lås op med {0}</value>
|
||||
</data>
|
||||
@@ -954,6 +945,9 @@ Skanning vil ske automatisk.</value>
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>Du kan ikke bruge denne funktion, før du opdaterer din krypteringsnøgle.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Krypteringsnøglemigrering nødvendig. Log ind gennem web-boksen for at opdatere krypteringsnøglen.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>Få mere at vide</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@ Skanning vil ske automatisk.</value>
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Standard mørkt tema</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Vælg det mørke tema, der skal bruges, når Standard (System) temaet bruges, mens din enheds mørke tilstand er aktiveret.</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Kopiér notat</value>
|
||||
</data>
|
||||
@@ -2651,10 +2642,6 @@ Vil du skifte til denne konto?</value>
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Adgangsnøgler</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Oprettet {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Applikation</value>
|
||||
</data>
|
||||
@@ -2761,4 +2748,116 @@ Vil du skifte til denne konto?</value>
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Logger ind på</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Boks</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Udseende</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Kontosikkerhed</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Bitwarden Hjælpecenter</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Kontakt Bitwarden-support</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Kopiér app-information</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Synk nu</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Oplås muligheder</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Sessionstimeout</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Handling ved sessionstimeout</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Fingeraftrykssætning for konto</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>En time og et minut</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>En time og {0} minutter</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} timer og et minut</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} timer and {1} minutter</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} timer</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>Android Autofill Framework bruges til at hjælpe med at udfylde loginoplysninger i andre apps på enheden.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Brug inline-autofyld, hvis det valgte tastatur understøtter det, ellers brug standardoverlejring.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Yderligere indstillinger</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Fortsæt til web-app?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Fortsæt til {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Fortsæt til Hjælpecenter?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Fortsæt med at kontakte support?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Fortsæt til App Store?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Øg kontosikkerheden ved at oprette totrins-indlogning i Bitwarden web-appen.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>Hovedadgangskoden kan ændres fra Bitwarden web-appen.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>Data kan importere til din Boks på {0}.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Læs mere i Hjælpecenter om, hvordan man bruger Bitwarden.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Kan ikke finde det, der søges efter? Kontakt Bitwarden-supporten via bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Tjek flere funktioner ud i Bitwarden-kontoen på web-appen.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden muliggør deling af emner i din Boks med andre vha. en organisation. Læs mere på bitwarden.com-webstedet.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Hjælp andre med at finde ud af, om Bitwarden er det rigtige for dem. Besøg App Store og skriv en bedømmelse nu.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Vælg det mørke tema, der skal bruges, når enhedens mørke-tilstand er i brug</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Oprettet {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>For mange forsøg</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Konto logget ud.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -396,9 +396,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Besuche unsere Website</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>Besuche unsere Webseite um Hilfe zu erhalten, Neuigkeiten zu erfahren, Kontakt aufzunehmen und mehr über die Verwendung von Bitwarden zu lernen.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Webseite</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -451,9 +448,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Master-Password ändern</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>Du kannst dein Master-Passwort im Bitwarden.com Web-Tresor ändern. Möchtest du die Seite jetzt öffnen?</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>Schließen</value>
|
||||
</data>
|
||||
@@ -707,9 +701,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>Zwei-Faktor-Authentifizierung</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>Mit der Zwei-Faktor-Authentifizierung wird dein Konto zusätzlich abgesichert, da jede Anmeldung mit einem anderen Gerät wie einem Sicherheitsschlüssel, einer Authentifizierungs-App, einer SMS, einem Anruf oder einer E-Mail verifiziert werden muss. Die Zwei-Faktor-Authentifizierung kann im bitwarden.com Web-Tresor aktiviert werden. Möchtest du die Webseite jetzt öffnen?</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>Mit {0} entsperren</value>
|
||||
</data>
|
||||
@@ -954,6 +945,9 @@ Das Scannen erfolgt automatisch.</value>
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>Du kannst diese Funktion nicht nutzen, solange du deinen Verschlüsselungsschlüssel nicht aktualisiert hast.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Encryption key migration required. Please login through the web vault to update your encryption key.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>Mehr erfahren</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@ Das Scannen erfolgt automatisch.</value>
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Dunkles Standard-Design</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Wähle das zu verwendende dunkle Design aus, das bei der Auswahl vom Standard-(System)-Design verwendet werden soll, während der Dark Mode deines Geräts aktiviert ist.</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Notiz kopieren</value>
|
||||
</data>
|
||||
@@ -2650,10 +2641,6 @@ Möchtest du zu diesem Konto wechseln?</value>
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Passkeys</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Erstellt am {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Anwendung</value>
|
||||
</data>
|
||||
@@ -2676,7 +2663,7 @@ Möchtest du zu diesem Konto wechseln?</value>
|
||||
<value>Hilfe zum erneuten Abfragen des Master-Passworts</value>
|
||||
</data>
|
||||
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
|
||||
<value>Unlocking may fail due to insufficient memory. Decrease your KDF memory settings or set up biometric unlock to resolve.</value>
|
||||
<value>Das Entsperren kann aufgrund unzureichenden Arbeitsspeichers fehlschlagen. Verringere deine KDF-Speichereinstellungen oder richte die biometrische Entsperrung, um dies zu lösen.</value>
|
||||
</data>
|
||||
<data name="InvalidAPIKey" xml:space="preserve">
|
||||
<value>Ungültiger API-Schlüssel</value>
|
||||
@@ -2700,7 +2687,7 @@ Möchtest du zu diesem Konto wechseln?</value>
|
||||
<value>Anmelden als {0}</value>
|
||||
</data>
|
||||
<data name="VaultTimeoutActionChangedToLogOut" xml:space="preserve">
|
||||
<value>Vault timeout action changed to log out</value>
|
||||
<value>Tresor-Timeoutaktion auf "Abmelden" geändert</value>
|
||||
</data>
|
||||
<data name="ThisItemCannotBeSharedWithTheOrganizationBecauseThereIsOneAlreadyWithTheSamePasskey" xml:space="preserve">
|
||||
<value>Dieser Eintrag kann nicht mit der Organisation geteilt werden, da bereits einer mit dem gleichen Passkey existiert.</value>
|
||||
@@ -2760,4 +2747,116 @@ Möchtest du zu diesem Konto wechseln?</value>
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Anmelden bei</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Tresor</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Aussehen</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Kontosicherheit</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Bitwarden Hilfezentrum</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Bitwarden Support kontaktieren</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>App-Informationen kopieren</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Jetzt synchronisieren</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Entsperroptionen</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Sitzungs-Timeout</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Aktion bei Sitzungs-Timeout</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Fingerabdruck-Phrase des Kontos</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>Eine Stunde und eine Minute</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>Eine Stunde und {0} Minuten</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} Stunden und eine Minute</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} Stunden und {1} Minuten</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} Stunden</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>Das Android Autofill Framework wird verwendet, um Zugangsdaten in andere Apps auf deinem Gerät auszufüllen.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Inline Auto-Ausfüllen verwenden, falls deine ausgewählte Tastatur dies unterstützt. Andernfalls wird das Standard Auto-Ausfüllen-Overlay verwendet.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Weitere Optionen</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Weiter zur Web-App?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Weiter zu {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Weiter zum Hilfezentrum?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Weiter, um den Support zu kontaktieren?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Weiter zum App Store?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Mache dein Konto sicherer, indem du eine Zwei-Faktor-Authentifizierung in der Bitwarden Web-App einrichtest.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>Du kannst dein Master-Passwort in der Bitwarden Web-App ändern.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>Du kannst Daten in deinem Tresor auf {0} importieren.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Erfahre mehr über die Verwendung von Bitwarden im Hilfezentrum.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Du findest nicht, was du suchst? Kontaktiere den Bitwarden Support auf bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Entdecke mehr Funktionen deines Bitwarden-Kontos in der Web-App.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden erlaubt es dir, deine Tresor-Einträge mit anderen zu teilen, indem du eine Organisation verwendest. Erfahre mehr auf der bitwarden.com Website.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Hilf anderen herauszufinden, ob Bitwarden das Richtige für sie ist. Besuche den App Store und hinterlasse jetzt eine Bewertung.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Wähle das zu verwendende dunkle Design aus, während der Dark Mode deines Geräts aktiviert ist</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Erstellt am {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>Zu viele Versuche</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Konto abgemeldet.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -397,9 +397,6 @@
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Επισκεφθείτε την ιστοσελίδα μας</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsiteDescription" xml:space="preserve">
|
||||
<value>Επισκευθείτε την ιστοσελίδα μας για να λάβετε βοήθεια, νέα, να μας στείλετε email ή να μάθετε περισσότερα σχετικά με τον τρόπο χρήσης του Bitwarden.</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Ιστοσελίδα</value>
|
||||
<comment>Label for a website.</comment>
|
||||
@@ -452,9 +449,6 @@
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Αλλαγή Κύριου Κωδικού</value>
|
||||
</data>
|
||||
<data name="ChangePasswordConfirmation" xml:space="preserve">
|
||||
<value>Μπορείτε να αλλάξετε τον κύριο κωδικό στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>Κλείσιμο</value>
|
||||
</data>
|
||||
@@ -708,9 +702,6 @@
|
||||
<data name="TwoStepLogin" xml:space="preserve">
|
||||
<value>Σύνδεση σε δύο βήματα</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginConfirmation" xml:space="preserve">
|
||||
<value>Η σύνδεση σε δύο βήματα καθιστά πιο ασφαλή τον λογαριασμό σας, απαιτώντας να επαληθεύσετε τα στοιχεία σας με μια άλλη συσκευή, όπως κλειδί ασφαλείας, εφαρμογή επαλήθευσης, μήνυμα SMS, τηλεφωνική κλήση ή email. Μπορείτε να ενεργοποιήσετε τη σύνδεση σε δύο βήματα στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;</value>
|
||||
</data>
|
||||
<data name="UnlockWith" xml:space="preserve">
|
||||
<value>Ξεκλείδωμα με {0}</value>
|
||||
</data>
|
||||
@@ -954,6 +945,9 @@
|
||||
<data name="UpdateKey" xml:space="preserve">
|
||||
<value>Δεν μπορείτε να χρησιμοποιήσετε αυτήν τη δυνατότητα μέχρι να ενημερώσετε το κλειδί κρυπτογράφησης.</value>
|
||||
</data>
|
||||
<data name="EncryptionKeyMigrationRequiredDescriptionLong" xml:space="preserve">
|
||||
<value>Encryption key migration required. Please login through the web vault to update your encryption key.</value>
|
||||
</data>
|
||||
<data name="LearnMore" xml:space="preserve">
|
||||
<value>Μάθετε Περισσότερα</value>
|
||||
</data>
|
||||
@@ -1555,9 +1549,6 @@
|
||||
<data name="DefaultDarkTheme" xml:space="preserve">
|
||||
<value>Προεπιλεγμένο σκοτεινό θέμα</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescription" xml:space="preserve">
|
||||
<value>Επιλέξτε το σκούρο θέμα που θα χρησιμοποιηθεί όταν χρησιμοποιείτε το προεπιλεγμένο (System) θέμα ενώ η σκοτεινή λειτουργία της συσκευής σας είναι ενεργοποιημένη.</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Αντιγραφή Σημειώσεων</value>
|
||||
</data>
|
||||
@@ -2651,10 +2642,6 @@
|
||||
<data name="Passkeys" xml:space="preserve">
|
||||
<value>Συνθηματικά</value>
|
||||
</data>
|
||||
<data name="CreatedX" xml:space="preserve">
|
||||
<value>Δημιουργήθηκε {0}</value>
|
||||
<comment>To state the date in which the cipher was created: Created 03/21/2023</comment>
|
||||
</data>
|
||||
<data name="Application" xml:space="preserve">
|
||||
<value>Εφαρμογή</value>
|
||||
</data>
|
||||
@@ -2761,4 +2748,116 @@
|
||||
<data name="LoggingInOn" xml:space="preserve">
|
||||
<value>Logging in on</value>
|
||||
</data>
|
||||
<data name="Vault" xml:space="preserve">
|
||||
<value>Vault</value>
|
||||
</data>
|
||||
<data name="Appearance" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="AccountSecurity" xml:space="preserve">
|
||||
<value>Account security</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Bitwarden Help Center</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Contact Bitwarden support</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Copy app information</value>
|
||||
</data>
|
||||
<data name="SyncNow" xml:space="preserve">
|
||||
<value>Sync now</value>
|
||||
</data>
|
||||
<data name="UnlockOptions" xml:space="preserve">
|
||||
<value>Unlock options</value>
|
||||
</data>
|
||||
<data name="SessionTimeout" xml:space="preserve">
|
||||
<value>Session timeout</value>
|
||||
</data>
|
||||
<data name="SessionTimeoutAction" xml:space="preserve">
|
||||
<value>Session timeout action</value>
|
||||
</data>
|
||||
<data name="AccountFingerprintPhrase" xml:space="preserve">
|
||||
<value>Account fingerprint phrase</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="OneHourAndOneMinute" xml:space="preserve">
|
||||
<value>One hour and one minute</value>
|
||||
</data>
|
||||
<data name="OneHourAndXMinute" xml:space="preserve">
|
||||
<value>One hour and {0} minutes</value>
|
||||
</data>
|
||||
<data name="XHoursAndOneMinute" xml:space="preserve">
|
||||
<value>{0} hours and one minute</value>
|
||||
</data>
|
||||
<data name="XHoursAndYMinutes" xml:space="preserve">
|
||||
<value>{0} hours and {1} minutes</value>
|
||||
</data>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} hours</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
|
||||
</data>
|
||||
<data name="UseInlineAutofillExplanationLong" xml:space="preserve">
|
||||
<value>Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay.</value>
|
||||
</data>
|
||||
<data name="AdditionalOptions" xml:space="preserve">
|
||||
<value>Additional options</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Continue to web app?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Continue to {0}?</value>
|
||||
<comment>The parameter is an URL, like bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="ContinueToHelpCenter" xml:space="preserve">
|
||||
<value>Continue to Help center?</value>
|
||||
</data>
|
||||
<data name="ContinueToContactSupport" xml:space="preserve">
|
||||
<value>Continue to contact support?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Continue to app store?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Make your account more secure by setting up two-step login in the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>You can change your master password on the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>You can import data to your vault on {0}.</value>
|
||||
<comment>The parameter is an URL, like vault.bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="LearnMoreAboutHowToUseBitwardenOnTheHelpCenter" xml:space="preserve">
|
||||
<value>Learn more about how to use Bitwarden on the Help center.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Explore more features of your Bitwarden account on the web app.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Help others find out if Bitwarden is right for them. Visit the app store and leave a rating now.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Choose the dark theme to use when your device’s dark mode is in use</value>
|
||||
</data>
|
||||
<data name="CreatedXY" xml:space="preserve">
|
||||
<value>Created {0}, {1}</value>
|
||||
<comment>To state the date/time in which the cipher was created: Created 03/21/2023, 09:25 AM. First parameter is the date and the second parameter is the time.</comment>
|
||||
</data>
|
||||
<data name="TooManyAttempts" xml:space="preserve">
|
||||
<value>Too many attempts</value>
|
||||
</data>
|
||||
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
|
||||
<value>Account logged out.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user