From e97a37222a296386381c390fc93acca67bad160e Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Wed, 27 Sep 2023 16:26:12 -0300 Subject: [PATCH] [PM-2658] Settings Reorganization feature (#2702) * [PM-2658] Settings Reorganization Init (#2697) * PM-2658 Started settings reorganization (settings main + vault + about) * PM-2658 Added settings controls based on templates and implemented OtherSettingsPage * PM-2658 Fix format * [PM-3512] Settings Appearance (#2703) * PM-3512 Implemented new Appearance Settings * PM-3512 Fix format * [PM-3510] Implement Account Security Settings view (#2714) * PM-3510 Implemented Security settings view * PM-3510 Fix format * PM-3510 Added empty placeholder to pending login requests and also improved a11y on security settings view. * PM-3511 Implemented autofill settings view (#2735) * [PM-3695] Add Connect to Watch to Other settings (#2736) * PM-3511 Implemented autofill settings view * PM-3695 Add Connect to watch setting to other settings view * [PM-3693] Clear old Settings approach (#2737) * PM-3511 Implemented autofill settings view * PM-3693 Remove old Settings approach * PM-3845 Fix default dark theme description verbiage (#2759) * PM-3839 Fix allow screen capture and submit crash logs to init their state when the page appears (#2760) * PM-3834 Fix dialogs strings on settings (#2758) * [PM-3834] Fix import items link (#2782) * PM-3834 Fix import items link * PM-3834 Fix import items link, removed old link. * [PM-4092] Fix vault timeout policies on new Settings (#2796) * PM-4092 Fix vault timeout policy on settings for disabling controls and reset timeout when surpassing maximum * PM-4092 Removed testing hardcoding of policy data --- src/Android/Android.csproj | 8 + src/Android/MainActivity.cs | 2 +- src/Android/MainApplication.cs | 2 + .../drawable/empty_login_requests.xml | 41 + .../drawable/empty_login_requests_dark.xml | 41 + src/Android/Services/DeviceActionService.cs | 38 + src/App/Abstractions/IDeviceActionService.cs | 5 + src/App/App.csproj | 11 +- src/App/App.xaml.cs | 4 +- src/App/Controls/ExternalLinkItemView.xaml | 29 + src/App/Controls/ExternalLinkItemView.xaml.cs | 31 + .../Settings/BaseSettingControlView.cs | 25 + .../Settings/SettingChooserItemView.xaml | 19 + .../Settings/SettingChooserItemView.xaml.cs | 31 + src/App/Controls/Settings/SwitchItemView.xaml | 19 + .../Controls/Settings/SwitchItemView.xaml.cs | 45 + src/App/Pages/Accounts/HomePage.xaml.cs | 2 +- src/App/Pages/BaseModalContentPage.cs | 18 + src/App/Pages/BaseViewModel.cs | 24 +- src/App/Pages/Generator/GeneratorPage.xaml.cs | 3 +- src/App/Pages/PickerViewModel.cs | 124 +++ src/App/Pages/Settings/AboutSettingsPage.xaml | 85 ++ .../Pages/Settings/AboutSettingsPage.xaml.cs | 37 + .../Settings/AboutSettingsPageViewModel.cs | 159 ++++ .../Settings/AppearanceSettingsPage.xaml | 58 ++ .../Settings/AppearanceSettingsPage.xaml.cs | 44 + .../AppearanceSettingsPageViewModel.cs | 203 ++++ .../Pages/Settings/AutofillServicesPage.xaml | 131 --- .../Settings/AutofillServicesPage.xaml.cs | 66 -- .../Settings/AutofillServicesPageViewModel.cs | 231 ----- .../Pages/Settings/AutofillSettingsPage.xaml | 133 +++ .../Settings/AutofillSettingsPage.xaml.cs | 37 + .../AutofillSettingsPageViewModel.android.cs | 181 ++++ .../Settings/AutofillSettingsPageViewModel.cs | 111 +++ .../AutofillSettingsPageViewModel.ios.cs | 19 + .../LoginPasswordlessRequestsListPage.xaml | 46 +- .../LoginPasswordlessRequestsListPage.xaml.cs | 21 + .../LoginPasswordlessRequestsListViewModel.cs | 9 +- src/App/Pages/Settings/OptionsPage.xaml | 169 ---- src/App/Pages/Settings/OptionsPage.xaml.cs | 53 -- .../Pages/Settings/OptionsPageViewModel.cs | 300 ------ src/App/Pages/Settings/OtherSettingsPage.xaml | 70 ++ .../Pages/Settings/OtherSettingsPage.xaml.cs | 44 + .../Settings/OtherSettingsPageViewModel.cs | 245 +++++ .../Pages/Settings/SecuritySettingsPage.xaml | 178 ++++ .../Settings/SecuritySettingsPage.xaml.cs | 37 + .../Settings/SecuritySettingsPageViewModel.cs | 547 +++++++++++ .../SettingsPage/ISettingsPageListItem.cs | 6 - .../Settings/SettingsPage/SettingsPage.xaml | 127 +-- .../SettingsPage/SettingsPage.xaml.cs | 52 +- .../SettingsPageHeaderListItem.cs | 12 - .../SettingsPage/SettingsPageListGroup.cs | 29 - .../SettingsPage/SettingsPageListItem.cs | 44 +- .../SettingsPageListItemSelector.cs | 24 - .../SettingsPage/SettingsPageViewModel.cs | 898 +----------------- src/App/Pages/Settings/SyncPage.xaml | 51 - src/App/Pages/Settings/SyncPage.xaml.cs | 43 - src/App/Pages/Settings/SyncPageViewModel.cs | 117 --- src/App/Pages/Settings/VaultSettingsPage.xaml | 48 + .../Pages/Settings/VaultSettingsPage.xaml.cs | 12 + .../Settings/VaultSettingsPageViewModel.cs | 51 + src/App/Pages/TabsPage.cs | 7 +- src/App/Resources/AppResources.Designer.cs | 299 +++++- src/App/Resources/AppResources.resx | 108 ++- src/App/Services/BaseBiometricService.cs | 17 + src/App/Services/UserPinService.cs | 38 + src/App/Styles/Android.xaml | 14 + src/App/Styles/Base.xaml | 31 + src/App/Styles/ControlTemplates.xaml | 37 + src/App/Styles/ControlTemplates.xaml.cs | 13 + src/App/Styles/iOS.xaml | 14 + src/App/Utilities/A11yExtensions.cs | 43 + src/App/Utilities/AppHelpers.cs | 3 + src/App/Utilities/ThemeManager.cs | 5 +- src/Core/Abstractions/IBiometricService.cs | 1 + src/Core/Abstractions/IPolicyService.cs | 1 + src/Core/Abstractions/IUserPinService.cs | 9 + src/Core/ExternalLinksConstants.cs | 21 + src/Core/Services/PolicyService.cs | 5 + src/Core/Services/StateService.cs | 4 + src/Core/Services/VaultTimeoutService.cs | 3 - src/Core/Utilities/ServiceContainer.cs | 3 +- .../Renderers/CustomTabbedRenderer.cs | 3 +- src/iOS.Core/Services/AutofillHandler.cs | 6 +- src/iOS.Core/Services/DeviceActionService.cs | 7 + src/iOS.Core/Utilities/iOSCoreHelpers.cs | 2 + src/iOS/AppDelegate.cs | 6 +- .../Contents.json | 25 + .../empty_login_requests.pdf | Bin 0 -> 1821 bytes .../empty_login_requests_dark.pdf | Bin 0 -> 1814 bytes src/iOS/iOS.csproj | 3 + 91 files changed, 3621 insertions(+), 2357 deletions(-) create mode 100644 src/Android/Resources/drawable/empty_login_requests.xml create mode 100644 src/Android/Resources/drawable/empty_login_requests_dark.xml create mode 100644 src/App/Controls/ExternalLinkItemView.xaml create mode 100644 src/App/Controls/ExternalLinkItemView.xaml.cs create mode 100644 src/App/Controls/Settings/BaseSettingControlView.cs create mode 100644 src/App/Controls/Settings/SettingChooserItemView.xaml create mode 100644 src/App/Controls/Settings/SettingChooserItemView.xaml.cs create mode 100644 src/App/Controls/Settings/SwitchItemView.xaml create mode 100644 src/App/Controls/Settings/SwitchItemView.xaml.cs create mode 100644 src/App/Pages/BaseModalContentPage.cs create mode 100644 src/App/Pages/PickerViewModel.cs create mode 100644 src/App/Pages/Settings/AboutSettingsPage.xaml create mode 100644 src/App/Pages/Settings/AboutSettingsPage.xaml.cs create mode 100644 src/App/Pages/Settings/AboutSettingsPageViewModel.cs create mode 100644 src/App/Pages/Settings/AppearanceSettingsPage.xaml create mode 100644 src/App/Pages/Settings/AppearanceSettingsPage.xaml.cs create mode 100644 src/App/Pages/Settings/AppearanceSettingsPageViewModel.cs delete mode 100644 src/App/Pages/Settings/AutofillServicesPage.xaml delete mode 100644 src/App/Pages/Settings/AutofillServicesPage.xaml.cs delete mode 100644 src/App/Pages/Settings/AutofillServicesPageViewModel.cs create mode 100644 src/App/Pages/Settings/AutofillSettingsPage.xaml create mode 100644 src/App/Pages/Settings/AutofillSettingsPage.xaml.cs create mode 100644 src/App/Pages/Settings/AutofillSettingsPageViewModel.android.cs create mode 100644 src/App/Pages/Settings/AutofillSettingsPageViewModel.cs create mode 100644 src/App/Pages/Settings/AutofillSettingsPageViewModel.ios.cs delete mode 100644 src/App/Pages/Settings/OptionsPage.xaml delete mode 100644 src/App/Pages/Settings/OptionsPage.xaml.cs delete mode 100644 src/App/Pages/Settings/OptionsPageViewModel.cs create mode 100644 src/App/Pages/Settings/OtherSettingsPage.xaml create mode 100644 src/App/Pages/Settings/OtherSettingsPage.xaml.cs create mode 100644 src/App/Pages/Settings/OtherSettingsPageViewModel.cs create mode 100644 src/App/Pages/Settings/SecuritySettingsPage.xaml create mode 100644 src/App/Pages/Settings/SecuritySettingsPage.xaml.cs create mode 100644 src/App/Pages/Settings/SecuritySettingsPageViewModel.cs delete mode 100644 src/App/Pages/Settings/SettingsPage/ISettingsPageListItem.cs delete mode 100644 src/App/Pages/Settings/SettingsPage/SettingsPageHeaderListItem.cs delete mode 100644 src/App/Pages/Settings/SettingsPage/SettingsPageListGroup.cs delete mode 100644 src/App/Pages/Settings/SettingsPage/SettingsPageListItemSelector.cs delete mode 100644 src/App/Pages/Settings/SyncPage.xaml delete mode 100644 src/App/Pages/Settings/SyncPage.xaml.cs delete mode 100644 src/App/Pages/Settings/SyncPageViewModel.cs create mode 100644 src/App/Pages/Settings/VaultSettingsPage.xaml create mode 100644 src/App/Pages/Settings/VaultSettingsPage.xaml.cs create mode 100644 src/App/Pages/Settings/VaultSettingsPageViewModel.cs create mode 100644 src/App/Services/UserPinService.cs create mode 100644 src/App/Styles/ControlTemplates.xaml create mode 100644 src/App/Styles/ControlTemplates.xaml.cs create mode 100644 src/App/Utilities/A11yExtensions.cs create mode 100644 src/Core/Abstractions/IUserPinService.cs create mode 100644 src/Core/ExternalLinksConstants.cs create mode 100644 src/iOS/Resources/Assets.xcassets/empty_login_requests.imageset/Contents.json create mode 100644 src/iOS/Resources/Assets.xcassets/empty_login_requests.imageset/empty_login_requests.pdf create mode 100644 src/iOS/Resources/Assets.xcassets/empty_login_requests.imageset/empty_login_requests_dark.pdf diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 320a23a47..3997e036b 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -245,6 +245,14 @@ + + + + + + + + diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index f4fc640d0..6792f3567 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -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()); } diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 287df60ed..4407c1c31 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -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(preferencesStorage); @@ -182,6 +183,7 @@ namespace Bit.Droid ServiceContainer.Register("cryptoService", cryptoService); ServiceContainer.Register("passwordRepromptService", passwordRepromptService); ServiceContainer.Register("avatarImageSourcePool", new AvatarImageSourcePool()); + ServiceContainer.Register(userPinService); // Push #if FDROID diff --git a/src/Android/Resources/drawable/empty_login_requests.xml b/src/Android/Resources/drawable/empty_login_requests.xml new file mode 100644 index 000000000..4c5d399e7 --- /dev/null +++ b/src/Android/Resources/drawable/empty_login_requests.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Android/Resources/drawable/empty_login_requests_dark.xml b/src/Android/Resources/drawable/empty_login_requests_dark.xml new file mode 100644 index 000000000..b12d7af2b --- /dev/null +++ b/src/Android/Resources/drawable/empty_login_requests_dark.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index dd1b8f1b6..70962a5bd 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -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; diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs index 56bf5d17f..98de7ec21 100644 --- a/src/App/Abstractions/IDeviceActionService.cs +++ b/src/App/Abstractions/IDeviceActionService.cs @@ -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(); } } diff --git a/src/App/App.csproj b/src/App/App.csproj index dac6725c8..0bc1ad4ad 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -59,9 +59,6 @@ ExtensionPage.xaml - - AutofillServicesPage.xaml - FolderAddEditPage.xaml @@ -71,12 +68,6 @@ ExportVaultPage.xaml - - OptionsPage.xaml - - - SyncPage.xaml - AttachmentsPage.xaml @@ -147,6 +138,7 @@ + @@ -444,5 +436,6 @@ + diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index aa839370b..9ff35ec7a 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -91,7 +91,7 @@ namespace Bit.App _messagingService.Send("showDialogResolve", new Tuple(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); }); } diff --git a/src/App/Controls/ExternalLinkItemView.xaml b/src/App/Controls/ExternalLinkItemView.xaml new file mode 100644 index 000000000..416367774 --- /dev/null +++ b/src/App/Controls/ExternalLinkItemView.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/src/App/Controls/ExternalLinkItemView.xaml.cs b/src/App/Controls/ExternalLinkItemView.xaml.cs new file mode 100644 index 000000000..3ce73090e --- /dev/null +++ b/src/App/Controls/ExternalLinkItemView.xaml.cs @@ -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); + } + } +} diff --git a/src/App/Controls/Settings/BaseSettingControlView.cs b/src/App/Controls/Settings/BaseSettingControlView.cs new file mode 100644 index 000000000..714ebb143 --- /dev/null +++ b/src/App/Controls/Settings/BaseSettingControlView.cs @@ -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); } + } + } +} diff --git a/src/App/Controls/Settings/SettingChooserItemView.xaml b/src/App/Controls/Settings/SettingChooserItemView.xaml new file mode 100644 index 000000000..1cc1fe0f0 --- /dev/null +++ b/src/App/Controls/Settings/SettingChooserItemView.xaml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/src/App/Controls/Settings/SettingChooserItemView.xaml.cs b/src/App/Controls/Settings/SettingChooserItemView.xaml.cs new file mode 100644 index 000000000..3ab7dadf5 --- /dev/null +++ b/src/App/Controls/Settings/SettingChooserItemView.xaml.cs @@ -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); + } + } +} diff --git a/src/App/Controls/Settings/SwitchItemView.xaml b/src/App/Controls/Settings/SwitchItemView.xaml new file mode 100644 index 000000000..6249ca35e --- /dev/null +++ b/src/App/Controls/Settings/SwitchItemView.xaml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/src/App/Controls/Settings/SwitchItemView.xaml.cs b/src/App/Controls/Settings/SwitchItemView.xaml.cs new file mode 100644 index 000000000..dae79e3d6 --- /dev/null +++ b/src/App/Controls/Settings/SwitchItemView.xaml.cs @@ -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; + } + } +} diff --git a/src/App/Pages/Accounts/HomePage.xaml.cs b/src/App/Pages/Accounts/HomePage.xaml.cs index c374afe9f..ae06b5a9d 100644 --- a/src/App/Pages/Accounts/HomePage.xaml.cs +++ b/src/App/Pages/Accounts/HomePage.xaml.cs @@ -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(() => { diff --git a/src/App/Pages/BaseModalContentPage.cs b/src/App/Pages/BaseModalContentPage.cs new file mode 100644 index 000000000..5c69ff4e9 --- /dev/null +++ b/src/App/Pages/BaseModalContentPage.cs @@ -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(); + } + } + } +} diff --git a/src/App/Pages/BaseViewModel.cs b/src/App/Pages/BaseViewModel.cs index 57098f8d0..a6ce6c7a2 100644 --- a/src/App/Pages/BaseViewModel.cs +++ b/src/App/Pages/BaseViewModel.cs @@ -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 execute, Func canExecute = null) + { + return new AsyncCommand(execute, + canExecute, + ex => HandleException(ex), + allowsMultipleExecutions: false); + } + + protected async Task HasConnectivityAsync() + { + if (Connectivity.NetworkAccess == NetworkAccess.None) + { + await _platformUtilsService.Value.ShowDialogAsync( + AppResources.InternetConnectionRequiredMessage, + AppResources.InternetConnectionRequiredTitle); + return false; + } + return true; + } } } diff --git a/src/App/Pages/Generator/GeneratorPage.xaml.cs b/src/App/Pages/Generator/GeneratorPage.xaml.cs index b07365c1c..9a5df2990 100644 --- a/src/App/Pages/Generator/GeneratorPage.xaml.cs +++ b/src/App/Pages/Generator/GeneratorPage.xaml.cs @@ -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()); } diff --git a/src/App/Pages/PickerViewModel.cs b/src/App/Pages/PickerViewModel.cs new file mode 100644 index 000000000..7fb31b647 --- /dev/null +++ b/src/App/Pages/PickerViewModel.cs @@ -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 : ExtendedViewModel + { + const string SELECTED_CHARACTER = "✓"; + + private readonly IDeviceActionService _deviceActionService; + private readonly ILogger _logger; + private readonly Func> _onSelectionChangingAsync; + private readonly string _title; + + public Dictionary _items; + private TKey _selectedKey; + private TKey _defaultSelectedKeyIfFailsToFind; + private Func _afterSelectionChangedAsync; + + public PickerViewModel(IDeviceActionService deviceActionService, + ILogger logger, + Func> onSelectionChangingAsync, + string title, + Func canExecuteSelectOptionCommand = null, + Action 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 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.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.Default.Equals(optionKey, _selectedKey) + || + !await _onSelectionChangingAsync(optionKey)) + { + return; + } + + _selectedKey = optionKey; + TriggerPropertyChanged(nameof(SelectedValue)); + + if (_afterSelectionChangedAsync != null) + { + await _afterSelectionChangedAsync(_selectedKey); + } + } + + public void SetAfterSelectionChanged(Func afterSelectionChangedAsync) => _afterSelectionChangedAsync = afterSelectionChangedAsync; + + private string CreateSelectableOption(string option, bool selected) => selected ? ToSelectedOption(option) : option; + + private string ToSelectedOption(string option) => $"{SELECTED_CHARACTER} {option}"; + } +} diff --git a/src/App/Pages/Settings/AboutSettingsPage.xaml b/src/App/Pages/Settings/AboutSettingsPage.xaml new file mode 100644 index 000000000..8a9cd9646 --- /dev/null +++ b/src/App/Pages/Settings/AboutSettingsPage.xaml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Settings/AboutSettingsPage.xaml.cs b/src/App/Pages/Settings/AboutSettingsPage.xaml.cs new file mode 100644 index 000000000..83062f178 --- /dev/null +++ b/src/App/Pages/Settings/AboutSettingsPage.xaml.cs @@ -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().ShowToast(null, null, AppResources.AnErrorHasOccurred); + + Navigation.PopAsync().FireAndForget(); + } + } + } +} diff --git a/src/App/Pages/Settings/AboutSettingsPageViewModel.cs b/src/App/Pages/Settings/AboutSettingsPageViewModel.cs new file mode 100644 index 000000000..1866d368d --- /dev/null +++ b/src/App/Pages/Settings/AboutSettingsPageViewModel.cs @@ -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(); + _deviceActionService = ServiceContainer.Resolve(); + _logger = ServiceContainer.Resolve(); + + var environmentService = ServiceContainer.Resolve(); + var clipboardService = ServiceContainer.Resolve(); + + 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 + /// + /// Sets up app info plus debugging information for push notifications. + /// Useful when trying to solve problems regarding push notifications. + /// + /// + /// 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. + /// + // public async Task GetAppInfoForPushNotificationsDebugAsync() + // { + // var stateService = ServiceContainer.Resolve(); + + // var appInfo = string.Format("{0}: {1} ({2})", AppResources.Version, + // _platformUtilsService.GetApplicationVersion(), _deviceActionService.GetBuildNumber()); + + //#if DEBUG + // var pushNotificationsRegistered = ServiceContainer.Resolve("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; + // } + } +} diff --git a/src/App/Pages/Settings/AppearanceSettingsPage.xaml b/src/App/Pages/Settings/AppearanceSettingsPage.xaml new file mode 100644 index 000000000..09114f627 --- /dev/null +++ b/src/App/Pages/Settings/AppearanceSettingsPage.xaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Settings/AppearanceSettingsPage.xaml.cs b/src/App/Pages/Settings/AppearanceSettingsPage.xaml.cs new file mode 100644 index 000000000..bff35e445 --- /dev/null +++ b/src/App/Pages/Settings/AppearanceSettingsPage.xaml.cs @@ -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().ShowToast(null, null, AppResources.AnErrorHasOccurred); + + Navigation.PopModalAsync().FireAndForget(); + } + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + _vm.UnsubscribeEvents(); + } + } +} diff --git a/src/App/Pages/Settings/AppearanceSettingsPageViewModel.cs b/src/App/Pages/Settings/AppearanceSettingsPageViewModel.cs new file mode 100644 index 000000000..03a06e3ec --- /dev/null +++ b/src/App/Pages/Settings/AppearanceSettingsPageViewModel.cs @@ -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(); + _logger = ServiceContainer.Resolve(); + _i18nService = ServiceContainer.Resolve(); + _platformUtilsService = ServiceContainer.Resolve(); + _messagingService = ServiceContainer.Resolve(); + + var deviceActionService = ServiceContainer.Resolve(); + + LanguagePickerViewModel = new PickerViewModel( + deviceActionService, + _logger, + OnLanguageChangingAsync, + AppResources.Language, + _ => _inited, + ex => HandleException(ex)); + + ThemePickerViewModel = new PickerViewModel( + deviceActionService, + _logger, + key => OnThemeChangingAsync(key, DefaultDarkThemePickerViewModel.SelectedKey), + AppResources.Theme, + _ => _inited, + ex => HandleException(ex)); + ThemePickerViewModel.SetAfterSelectionChanged(_ => + MainThread.InvokeOnMainThreadAsync(() => + { + TriggerPropertyChanged(nameof(ShowDefaultDarkThemePicker)); + })); + + DefaultDarkThemePickerViewModel = new PickerViewModel( + deviceActionService, + _logger, + key => OnThemeChangingAsync(ThemePickerViewModel.SelectedKey, key), + AppResources.DefaultDarkTheme, + _ => _inited, + ex => HandleException(ex)); + + ToggleShowWebsiteIconsCommand = CreateDefaultAsyncCommnad(ToggleShowWebsiteIconsAsync, _ => _inited); + } + + public PickerViewModel LanguagePickerViewModel { get; } + public PickerViewModel ThemePickerViewModel { get; } + public PickerViewModel 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.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.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 + { + [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 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 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; + } + } +} diff --git a/src/App/Pages/Settings/AutofillServicesPage.xaml b/src/App/Pages/Settings/AutofillServicesPage.xaml deleted file mode 100644 index 38ad244f7..000000000 --- a/src/App/Pages/Settings/AutofillServicesPage.xaml +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/App/Pages/Settings/OtherSettingsPage.xaml.cs b/src/App/Pages/Settings/OtherSettingsPage.xaml.cs new file mode 100644 index 000000000..5e91aa560 --- /dev/null +++ b/src/App/Pages/Settings/OtherSettingsPage.xaml.cs @@ -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().ShowToast(null, null, AppResources.AnErrorHasOccurred); + + Navigation.PopAsync().FireAndForget(); + } + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + _vm.UnsubscribeEvents(); + } + } +} diff --git a/src/App/Pages/Settings/OtherSettingsPageViewModel.cs b/src/App/Pages/Settings/OtherSettingsPageViewModel.cs new file mode 100644 index 000000000..4d4dc2036 --- /dev/null +++ b/src/App/Pages/Settings/OtherSettingsPageViewModel.cs @@ -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(); + _platformUtilsService = ServiceContainer.Resolve(); + _stateService = ServiceContainer.Resolve(); + _syncService = ServiceContainer.Resolve(); + _localizeService = ServiceContainer.Resolve(); + _watchDeviceService = ServiceContainer.Resolve(); + _logger = ServiceContainer.Resolve(); + + SyncCommand = CreateDefaultAsyncCommnad(SyncAsync, _ => _inited); + ToggleIsScreenCaptureAllowedCommand = CreateDefaultAsyncCommnad(ToggleIsScreenCaptureAllowedAsync, _ => _inited); + ToggleShouldConnectToWatchCommand = CreateDefaultAsyncCommnad(ToggleShouldConnectToWatchAsync, _ => _inited); + + ClearClipboardPickerViewModel = new PickerViewModel( + _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 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 + { + [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 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; + } + } +} diff --git a/src/App/Pages/Settings/SecuritySettingsPage.xaml b/src/App/Pages/Settings/SecuritySettingsPage.xaml new file mode 100644 index 000000000..c3f16753e --- /dev/null +++ b/src/App/Pages/Settings/SecuritySettingsPage.xaml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Settings/SecuritySettingsPage.xaml.cs b/src/App/Pages/Settings/SecuritySettingsPage.xaml.cs new file mode 100644 index 000000000..6467b9ac6 --- /dev/null +++ b/src/App/Pages/Settings/SecuritySettingsPage.xaml.cs @@ -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().ShowToast(null, null, AppResources.AnErrorHasOccurred); + + Navigation.PopAsync().FireAndForget(); + } + } + } +} diff --git a/src/App/Pages/Settings/SecuritySettingsPageViewModel.cs b/src/App/Pages/Settings/SecuritySettingsPageViewModel.cs new file mode 100644 index 000000000..8e98aa1fe --- /dev/null +++ b/src/App/Pages/Settings/SecuritySettingsPageViewModel.cs @@ -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(); + _pushNotificationService = ServiceContainer.Resolve(); + _platformUtilsService = ServiceContainer.Resolve(); + _deviceActionService = ServiceContainer.Resolve(); + _vaultTimeoutService = ServiceContainer.Resolve(); + _biometricsService = ServiceContainer.Resolve(); + _userPinService = ServiceContainer.Resolve(); + _cryptoService = ServiceContainer.Resolve(); + _userVerificationService = ServiceContainer.Resolve(); + _policyService = ServiceContainer.Resolve(); + _messagingService = ServiceContainer.Resolve(); + _environmentService = ServiceContainer.Resolve(); + _logger = ServiceContainer.Resolve(); + + VaultTimeoutPickerViewModel = new PickerViewModel( + _deviceActionService, + _logger, + OnVaultTimeoutChangingAsync, + AppResources.SessionTimeout, + _ => _inited, + ex => HandleException(ex)); + VaultTimeoutPickerViewModel.SetAfterSelectionChanged(_ => MainThread.InvokeOnMainThreadAsync(TriggerUpdateCustomVaultTimeoutPicker)); + + VaultTimeoutActionPickerViewModel = new PickerViewModel( + _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 VaultTimeoutPickerViewModel { get; } + public PickerViewModel 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 + { + [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(); + 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 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 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 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 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); + } + } + } +} diff --git a/src/App/Pages/Settings/SettingsPage/ISettingsPageListItem.cs b/src/App/Pages/Settings/SettingsPage/ISettingsPageListItem.cs deleted file mode 100644 index e8edc5063..000000000 --- a/src/App/Pages/Settings/SettingsPage/ISettingsPageListItem.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bit.App.Pages -{ - public interface ISettingsPageListItem - { - } -} diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml index 4976573af..e1b519fb3 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml +++ b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml @@ -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"> - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - - + + + diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs index 47a8a22c4..b80c10b27 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs +++ b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs @@ -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); - } - } } } diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageHeaderListItem.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageHeaderListItem.cs deleted file mode 100644 index ec3aafe16..000000000 --- a/src/App/Pages/Settings/SettingsPage/SettingsPageHeaderListItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Bit.App.Pages -{ - public class SettingsPageHeaderListItem : ISettingsPageListItem - { - public SettingsPageHeaderListItem(string title) - { - Title = title; - } - - public string Title { get; } - } -} diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageListGroup.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageListGroup.cs deleted file mode 100644 index 319e3e773..000000000 --- a/src/App/Pages/Settings/SettingsPage/SettingsPageListGroup.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; - -namespace Bit.App.Pages -{ - public class SettingsPageListGroup : List - { - public SettingsPageListGroup(List 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; } - } -} diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageListItem.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageListItem.cs index e2b2b0295..75855112e 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPageListItem.cs +++ b/src/App/Pages/Settings/SettingsPage/SettingsPageListItem.cs @@ -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 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 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 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); } } } diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageListItemSelector.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageListItemSelector.cs deleted file mode 100644 index 74dae3f2d..000000000 --- a/src/App/Pages/Settings/SettingsPage/SettingsPageListItemSelector.cs +++ /dev/null @@ -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; - } - } -} diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs index 4019c43c0..c37c3efe1 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs +++ b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs @@ -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> VaultTimeoutOptions = - new List> - { - new KeyValuePair(AppResources.Immediately, 0), - new KeyValuePair(AppResources.OneMinute, 1), - new KeyValuePair(AppResources.FiveMinutes, 5), - new KeyValuePair(AppResources.FifteenMinutes, 15), - new KeyValuePair(AppResources.ThirtyMinutes, 30), - new KeyValuePair(AppResources.OneHour, 60), - new KeyValuePair(AppResources.FourHours, 240), - new KeyValuePair(AppResources.OnRestart, -1), - new KeyValuePair(AppResources.Never, null), - new KeyValuePair(AppResources.Custom, CustomVaultTimeoutValue), - }; - private readonly static List> VaultTimeoutActionOptions = - new List> - { - new KeyValuePair(AppResources.Lock, VaultTimeoutAction.Lock), - new KeyValuePair(AppResources.LogOut, VaultTimeoutAction.Logout), - }; - - private Policy _vaultTimeoutPolicy; - private int? _vaultTimeout; - private List> _vaultTimeoutOptions = VaultTimeoutOptions; - private List> _vaultTimeoutActionOptions = VaultTimeoutActionOptions; - public SettingsPageViewModel() { - _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - _cryptoService = ServiceContainer.Resolve("cryptoService"); - _stateService = ServiceContainer.Resolve("stateService"); - _deviceActionService = ServiceContainer.Resolve("deviceActionService"); - _autofillHandler = ServiceContainer.Resolve(); - _environmentService = ServiceContainer.Resolve("environmentService"); - _messagingService = ServiceContainer.Resolve("messagingService"); - _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); - _syncService = ServiceContainer.Resolve("syncService"); - _biometricService = ServiceContainer.Resolve("biometricService"); - _policyService = ServiceContainer.Resolve("policyService"); - _localizeService = ServiceContainer.Resolve("localizeService"); - _userVerificationService = ServiceContainer.Resolve(); - _clipboardService = ServiceContainer.Resolve("clipboardService"); - _loggerService = ServiceContainer.Resolve("logger"); - _pushNotificationService = ServiceContainer.Resolve(); - _authService = ServiceContainer.Resolve(); - _watchDeviceService = ServiceContainer.Resolve(); - GroupedItems = new ObservableRangeCollection(); - PageTitle = AppResources.Settings; + ExecuteSettingItemCommand = new AsyncCommand(item => item.ExecuteAsync(), + onException: ex => HandleException(ex), + allowsMultipleExecutions: false); - ExecuteSettingItemCommand = new AsyncCommand(item => item.ExecuteAsync(), onException: _loggerService.Exception, allowsMultipleExecutions: false); + SettingsItems = new List + { + 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 GroupedItems { get; set; } + public List SettingsItems { get; } public IAsyncCommand 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("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 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(); - 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 - { - 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 - { - 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(); - 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 - { - 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 - { - 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() - { - 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(); - 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(); - 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 { 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); } } } diff --git a/src/App/Pages/Settings/SyncPage.xaml b/src/App/Pages/Settings/SyncPage.xaml deleted file mode 100644 index 7ad3a4fd6..000000000 --- a/src/App/Pages/Settings/SyncPage.xaml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/App/Pages/Settings/SyncPage.xaml.cs b/src/App/Pages/Settings/SyncPage.xaml.cs deleted file mode 100644 index a6b837f6b..000000000 --- a/src/App/Pages/Settings/SyncPage.xaml.cs +++ /dev/null @@ -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(); - } - } - } -} diff --git a/src/App/Pages/Settings/SyncPageViewModel.cs b/src/App/Pages/Settings/SyncPageViewModel.cs deleted file mode 100644 index a98e12617..000000000 --- a/src/App/Pages/Settings/SyncPageViewModel.cs +++ /dev/null @@ -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("deviceActionService"); - _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - _stateService = ServiceContainer.Resolve("stateService"); - _syncService = ServiceContainer.Resolve("syncService"); - _localizeService = ServiceContainer.Resolve("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); - } - } - } - } -} diff --git a/src/App/Pages/Settings/VaultSettingsPage.xaml b/src/App/Pages/Settings/VaultSettingsPage.xaml new file mode 100644 index 000000000..64f59f041 --- /dev/null +++ b/src/App/Pages/Settings/VaultSettingsPage.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Settings/VaultSettingsPage.xaml.cs b/src/App/Pages/Settings/VaultSettingsPage.xaml.cs new file mode 100644 index 000000000..9e9d7b76f --- /dev/null +++ b/src/App/Pages/Settings/VaultSettingsPage.xaml.cs @@ -0,0 +1,12 @@ +namespace Bit.App.Pages +{ + public partial class VaultSettingsPage : BaseContentPage + { + public VaultSettingsPage() + { + InitializeComponent(); + var vm = BindingContext as VaultSettingsPageViewModel; + vm.Page = this; + } + } +} diff --git a/src/App/Pages/Settings/VaultSettingsPageViewModel.cs b/src/App/Pages/Settings/VaultSettingsPageViewModel.cs new file mode 100644 index 000000000..6969a8133 --- /dev/null +++ b/src/App/Pages/Settings/VaultSettingsPageViewModel.cs @@ -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(); + _environmentService = ServiceContainer.Resolve(); + + 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); + } + } + } +} diff --git a/src/App/Pages/TabsPage.cs b/src/App/Pages/TabsPage.cs index 88c638101..9df5cbcbf 100644 --- a/src/App/Pages/TabsPage.cs +++ b/src/App/Pages/TabsPage.cs @@ -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(); - } } } diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index c09f5a81a..d50bec7ea 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -229,6 +229,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Account fingerprint phrase. + /// + public static string AccountFingerprintPhrase { + get { + return ResourceManager.GetString("AccountFingerprintPhrase", resourceCulture); + } + } + /// /// Looks up a localized string similar to Locked. /// @@ -283,6 +292,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Account security. + /// + public static string AccountSecurity { + get { + return ResourceManager.GetString("AccountSecurity", resourceCulture); + } + } + /// /// Looks up a localized string similar to Switched to next available account. /// @@ -355,6 +373,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Additional options. + /// + public static string AdditionalOptions { + get { + return ResourceManager.GetString("AdditionalOptions", resourceCulture); + } + } + /// /// Looks up a localized string similar to Add new attachment. /// @@ -562,6 +589,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Appearance. + /// + public static string Appearance { + get { + return ResourceManager.GetString("Appearance", resourceCulture); + } + } + /// /// Looks up a localized string similar to App extension. /// @@ -886,6 +922,15 @@ namespace Bit.App.Resources { } } + /// + /// 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.. + /// + public static string AutofillServicesExplanationLong { + get { + return ResourceManager.GetString("AutofillServicesExplanationLong", resourceCulture); + } + } + /// /// Looks up a localized string similar to Your logins are now easily accessible right from your keyboard while logging into apps and websites.. /// @@ -1273,6 +1318,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Bitwarden Help Center. + /// + public static string BitwardenHelpCenter { + get { + return ResourceManager.GetString("BitwardenHelpCenter", resourceCulture); + } + } + /// /// Looks up a localized string similar to Black. /// @@ -1444,6 +1498,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to You can change your master password on the Bitwarden web app.. + /// + public static string ChangeMasterPasswordDescriptionLong { + get { + return ResourceManager.GetString("ChangeMasterPasswordDescriptionLong", resourceCulture); + } + } + /// /// 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?. /// @@ -1606,6 +1669,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Contact Bitwarden support. + /// + public static string ContactBitwardenSupport { + get { + return ResourceManager.GetString("ContactBitwardenSupport", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com.. + /// + public static string ContactSupportDescriptionLong { + get { + return ResourceManager.GetString("ContactSupportDescriptionLong", resourceCulture); + } + } + /// /// Looks up a localized string similar to Continue. /// @@ -1615,6 +1696,51 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Continue to app store?. + /// + public static string ContinueToAppStore { + get { + return ResourceManager.GetString("ContinueToAppStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Continue to contact support?. + /// + public static string ContinueToContactSupport { + get { + return ResourceManager.GetString("ContinueToContactSupport", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Continue to Help center?. + /// + public static string ContinueToHelpCenter { + get { + return ResourceManager.GetString("ContinueToHelpCenter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Continue to web app?. + /// + public static string ContinueToWebApp { + get { + return ResourceManager.GetString("ContinueToWebApp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Continue to {0}?. + /// + public static string ContinueToX { + get { + return ResourceManager.GetString("ContinueToX", resourceCulture); + } + } + /// /// Looks up a localized string similar to Copy. /// @@ -1624,6 +1750,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Copy app information. + /// + public static string CopyAppInformation { + get { + return ResourceManager.GetString("CopyAppInformation", resourceCulture); + } + } + /// /// Looks up a localized string similar to Copy application. /// @@ -1930,6 +2065,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Choose the dark theme to use when your device’s dark mode is in use. + /// + public static string DefaultDarkThemeDescriptionLong { + get { + return ResourceManager.GetString("DefaultDarkThemeDescriptionLong", resourceCulture); + } + } + /// /// Looks up a localized string similar to Default (System). /// @@ -2569,6 +2713,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Explore more features of your Bitwarden account on the web app.. + /// + public static string ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp { + get { + return ResourceManager.GetString("ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp", resourceCulture); + } + } + /// /// Looks up a localized string similar to Export vault. /// @@ -3649,6 +3802,15 @@ namespace Bit.App.Resources { } } + /// + /// 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.. + /// + public static string LearnAboutOrganizationsDescriptionLong { + get { + return ResourceManager.GetString("LearnAboutOrganizationsDescriptionLong", resourceCulture); + } + } + /// /// Looks up a localized string similar to Learn more. /// @@ -3658,6 +3820,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Learn more about how to use Bitwarden on the Help center.. + /// + public static string LearnMoreAboutHowToUseBitwardenOnTheHelpCenter { + get { + return ResourceManager.GetString("LearnMoreAboutHowToUseBitwardenOnTheHelpCenter", resourceCulture); + } + } + /// /// Looks up a localized string similar to Learn about organizations. /// @@ -4813,6 +4984,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to One hour and one minute. + /// + public static string OneHourAndOneMinute { + get { + return ResourceManager.GetString("OneHourAndOneMinute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to One hour and {0} minutes. + /// + public static string OneHourAndXMinute { + get { + return ResourceManager.GetString("OneHourAndXMinute", resourceCulture); + } + } + /// /// Looks up a localized string similar to 1 minute. /// @@ -5372,6 +5561,15 @@ namespace Bit.App.Resources { } } + /// + /// 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.. + /// + public static string RateAppDescriptionLong { + get { + return ResourceManager.GetString("RateAppDescriptionLong", resourceCulture); + } + } + /// /// Looks up a localized string similar to Rate the app. /// @@ -5975,6 +6173,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Session timeout. + /// + public static string SessionTimeout { + get { + return ResourceManager.GetString("SessionTimeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Session timeout action. + /// + public static string SessionTimeoutAction { + get { + return ResourceManager.GetString("SessionTimeoutAction", resourceCulture); + } + } + /// /// Looks up a localized string similar to Set master password. /// @@ -6281,6 +6497,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Sync now. + /// + public static string SyncNow { + get { + return ResourceManager.GetString("SyncNow", resourceCulture); + } + } + /// /// Looks up a localized string similar to Sync vault now. /// @@ -6642,6 +6867,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Make your account more secure by setting up two-step login in the Bitwarden web app.. + /// + public static string TwoStepLoginDescriptionLong { + get { + return ResourceManager.GetString("TwoStepLoginDescriptionLong", resourceCulture); + } + } + /// /// Looks up a localized string similar to Two-step login options. /// @@ -6795,6 +7029,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Unlock options. + /// + public static string UnlockOptions { + get { + return ResourceManager.GetString("UnlockOptions", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unlock vault. /// @@ -6993,6 +7236,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay.. + /// + public static string UseInlineAutofillExplanationLong { + get { + return ResourceManager.GetString("UseInlineAutofillExplanationLong", resourceCulture); + } + } + /// /// Looks up a localized string similar to Username. /// @@ -7012,7 +7264,7 @@ namespace Bit.App.Resources { } /// - /// 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. /// public static string UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices { get { @@ -7056,6 +7308,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Vault. + /// + public static string Vault { + get { + return ResourceManager.GetString("Vault", resourceCulture); + } + } + /// /// Looks up a localized string similar to Vault: {0}. /// @@ -7452,6 +7713,33 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to {0} hours. + /// + public static string XHours { + get { + return ResourceManager.GetString("XHours", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} hours and one minute. + /// + public static string XHoursAndOneMinute { + get { + return ResourceManager.GetString("XHoursAndOneMinute", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} hours and {1} minutes. + /// + public static string XHoursAndYMinutes { + get { + return ResourceManager.GetString("XHoursAndYMinutes", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} minutes ago. /// @@ -7479,6 +7767,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to You can import data to your vault on {0}.. + /// + public static string YouCanImportDataToYourVaultOnX { + get { + return ResourceManager.GetString("YouCanImportDataToYourVaultOnX", resourceCulture); + } + } + /// /// Looks up a localized string similar to You cannot edit passkey application because it would invalidate the passkey. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 066ad4500..6e48e42c1 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -398,6 +398,7 @@ Visit our website to get help, news, email us, and/or learn more about how to use Bitwarden. + DEPRECATED Website @@ -453,6 +454,7 @@ You can change your master password on the bitwarden.com web vault. Do you want to visit the website now? + DEPRECATED Close @@ -709,6 +711,7 @@ 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? + DEPRECATED Unlock with {0} @@ -1560,6 +1563,7 @@ Scanning will happen automatically. Choose the dark theme to use when using Default (System) theme while your device's dark mode is in use. + DEPRECATED Copy note @@ -2367,7 +2371,7 @@ select Add TOTP to store the key safely Approve login requests - Use this device to approve login requests made from other devices. + Use this device to approve login requests made from other devices Allow notifications @@ -2765,6 +2769,108 @@ Do you want to switch to this account? Logging in on + + Vault + + + Appearance + + + Account security + + + Bitwarden Help Center + + + Contact Bitwarden support + + + Copy app information + + + Sync now + + + Unlock options + + + Session timeout + + + Session timeout action + + + Account fingerprint phrase + 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. + + + One hour and one minute + + + One hour and {0} minutes + + + {0} hours and one minute + + + {0} hours and {1} minutes + + + {0} hours + + + The Android Autofill Framework is used to assist in filling login information into other apps on your device. + + + Use inline autofill if your selected keyboard supports it. Otherwise, use the default overlay. + + + Additional options + + + Continue to web app? + + + Continue to {0}? + The parameter is an URL, like bitwarden.com. + + + Continue to Help center? + + + Continue to contact support? + + + Continue to app store? + + + Make your account more secure by setting up two-step login in the Bitwarden web app. + + + You can change your master password on the Bitwarden web app. + + + You can import data to your vault on {0}. + The parameter is an URL, like vault.bitwarden.com. + + + Learn more about how to use Bitwarden on the Help center. + + + Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com. + + + Explore more features of your Bitwarden account on the web app. + + + Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website. + + + Help others find out if Bitwarden is right for them. Visit the app store and leave a rating now. + + + Choose the dark theme to use when your device’s dark mode is in use + Created {0}, {1} 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. diff --git a/src/App/Services/BaseBiometricService.cs b/src/App/Services/BaseBiometricService.cs index 0c3c8e34a..2153c6e1c 100644 --- a/src/App/Services/BaseBiometricService.cs +++ b/src/App/Services/BaseBiometricService.cs @@ -16,7 +16,24 @@ namespace Bit.App.Services public async Task CanUseBiometricsUnlockAsync() { +#pragma warning disable CS0618 // Type or member is obsolete return await _cryptoService.GetBiometricUnlockKeyAsync() != null || await _stateService.GetKeyEncryptedAsync() != null; +#pragma warning restore CS0618 // Type or member is obsolete + } + + public async Task SetCanUnlockWithBiometricsAsync(bool canUnlockWithBiometrics) + { + if (canUnlockWithBiometrics) + { + await SetupBiometricAsync(); + await _stateService.SetBiometricUnlockAsync(true); + } + else + { + await _stateService.SetBiometricUnlockAsync(null); + } + await _stateService.SetBiometricLockedAsync(false); + await _cryptoService.RefreshKeysAsync(); } public abstract Task IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null); diff --git a/src/App/Services/UserPinService.cs b/src/App/Services/UserPinService.cs new file mode 100644 index 000000000..b4440a313 --- /dev/null +++ b/src/App/Services/UserPinService.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using Bit.Core.Abstractions; + +namespace Bit.App.Services +{ + public class UserPinService : IUserPinService + { + private readonly IStateService _stateService; + private readonly ICryptoService _cryptoService; + + public UserPinService(IStateService stateService, ICryptoService cryptoService) + { + _stateService = stateService; + _cryptoService = cryptoService; + } + + public async Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart) + { + 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 (requireMasterPasswordOnRestart) + { + await _stateService.SetPinKeyEncryptedUserKeyEphemeralAsync(protectedPinKey); + } + else + { + await _stateService.SetPinKeyEncryptedUserKeyAsync(protectedPinKey); + } + } + } +} diff --git a/src/App/Styles/Android.xaml b/src/App/Styles/Android.xaml index 51c2e37aa..fa5a8c829 100644 --- a/src/App/Styles/Android.xaml +++ b/src/App/Styles/Android.xaml @@ -367,4 +367,18 @@ + + diff --git a/src/App/Styles/Base.xaml b/src/App/Styles/Base.xaml index 2dac11bf7..c637aa5ff 100644 --- a/src/App/Styles/Base.xaml +++ b/src/App/Styles/Base.xaml @@ -508,6 +508,7 @@ Value="{DynamicResource MutedColor}" /> + + + + + + + + + diff --git a/src/App/Styles/ControlTemplates.xaml b/src/App/Styles/ControlTemplates.xaml new file mode 100644 index 000000000..e61b80eda --- /dev/null +++ b/src/App/Styles/ControlTemplates.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/src/App/Styles/ControlTemplates.xaml.cs b/src/App/Styles/ControlTemplates.xaml.cs new file mode 100644 index 000000000..db09fc2e9 --- /dev/null +++ b/src/App/Styles/ControlTemplates.xaml.cs @@ -0,0 +1,13 @@ +using Xamarin.Forms; + +namespace Bit.App.Styles +{ + public partial class ControlTemplates : ResourceDictionary + { + public ControlTemplates() + { + InitializeComponent(); + } + } +} + diff --git a/src/App/Styles/iOS.xaml b/src/App/Styles/iOS.xaml index b6041bcce..ef9aef5a7 100644 --- a/src/App/Styles/iOS.xaml +++ b/src/App/Styles/iOS.xaml @@ -391,4 +391,18 @@ + + diff --git a/src/App/Utilities/A11yExtensions.cs b/src/App/Utilities/A11yExtensions.cs new file mode 100644 index 000000000..5fbe8bd2c --- /dev/null +++ b/src/App/Utilities/A11yExtensions.cs @@ -0,0 +1,43 @@ +using System; +using Bit.App.Resources; + +namespace Bit.App.Utilities +{ + public static class A11yExtensions + { + public enum TimeSpanVerbalizationMode + { + HoursAndMinutes, + Hours + } + + public static string Verbalize(this TimeSpan timeSpan, TimeSpanVerbalizationMode mode) + { + if (mode == TimeSpanVerbalizationMode.Hours) + { + if (timeSpan.TotalHours == 1) + { + return AppResources.OneHour; + } + + return string.Format(AppResources.XHours, timeSpan.TotalHours); + } + + if (timeSpan.Hours == 1) + { + if (timeSpan.Minutes == 1) + { + return AppResources.OneHourAndOneMinute; + } + return string.Format(AppResources.OneHourAndXMinute, timeSpan.Minutes); + } + + if (timeSpan.Minutes == 1) + { + return string.Format(AppResources.XHoursAndOneMinute, timeSpan.Hours); + } + + return string.Format(AppResources.XHoursAndYMinutes, timeSpan.Hours, timeSpan.Minutes); + } + } +} diff --git a/src/App/Utilities/AppHelpers.cs b/src/App/Utilities/AppHelpers.cs index eac563800..3e0a70bda 100644 --- a/src/App/Utilities/AppHelpers.cs +++ b/src/App/Utilities/AppHelpers.cs @@ -25,6 +25,9 @@ namespace Bit.App.Utilities { public static class AppHelpers { + public const string VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND = "vaultTimeoutActionChanged"; + public const string RESUMED_MESSAGE_COMMAND = "resumed"; + public static async Task CipherListOptions(ContentPage page, CipherView cipher, IPasswordRepromptService passwordRepromptService) { var platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); diff --git a/src/App/Utilities/ThemeManager.cs b/src/App/Utilities/ThemeManager.cs index 65d2d2cfe..ead18f8a1 100644 --- a/src/App/Utilities/ThemeManager.cs +++ b/src/App/Utilities/ThemeManager.cs @@ -12,6 +12,8 @@ namespace Bit.App.Utilities { public static class ThemeManager { + public const string UPDATED_THEME_MESSAGE_KEY = "updatedTheme"; + public static bool UsingLightTheme = true; public static Func Resources = () => null; @@ -58,11 +60,12 @@ namespace Bit.App.Utilities // Base styles resources.MergedDictionaries.Add(new Base()); + resources.MergedDictionaries.Add(new ControlTemplates()); // Platform styles if (Device.RuntimePlatform == Device.Android) { - resources.MergedDictionaries.Add(new Android()); + resources.MergedDictionaries.Add(new Styles.Android()); } else if (Device.RuntimePlatform == Device.iOS) { diff --git a/src/Core/Abstractions/IBiometricService.cs b/src/Core/Abstractions/IBiometricService.cs index 3ce26c058..e0e239922 100644 --- a/src/Core/Abstractions/IBiometricService.cs +++ b/src/Core/Abstractions/IBiometricService.cs @@ -7,5 +7,6 @@ namespace Bit.Core.Abstractions Task CanUseBiometricsUnlockAsync(); Task SetupBiometricAsync(string bioIntegritySrcKey = null); Task IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null); + Task SetCanUnlockWithBiometricsAsync(bool canUnlockWithBiometrics); } } diff --git a/src/Core/Abstractions/IPolicyService.cs b/src/Core/Abstractions/IPolicyService.cs index 75ca6168e..5a0e7f6f2 100644 --- a/src/Core/Abstractions/IPolicyService.cs +++ b/src/Core/Abstractions/IPolicyService.cs @@ -11,6 +11,7 @@ namespace Bit.Core.Abstractions { void ClearCache(); Task> GetAll(PolicyType? type, string userId = null); + Task FirstOrDefault(PolicyType? type, string userId = null); Task Replace(Dictionary policies, string userId = null); Task ClearAsync(string userId); Task GetMasterPasswordPolicyOptions(IEnumerable policies = null, string userId = null); diff --git a/src/Core/Abstractions/IUserPinService.cs b/src/Core/Abstractions/IUserPinService.cs new file mode 100644 index 000000000..1a8b69d6e --- /dev/null +++ b/src/Core/Abstractions/IUserPinService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Bit.Core.Abstractions +{ + public interface IUserPinService + { + Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart); + } +} diff --git a/src/Core/ExternalLinksConstants.cs b/src/Core/ExternalLinksConstants.cs new file mode 100644 index 000000000..fbb70030a --- /dev/null +++ b/src/Core/ExternalLinksConstants.cs @@ -0,0 +1,21 @@ +namespace Bit.Core +{ + public static class ExternalLinksConstants + { + public const string HELP_CENTER = "https://bitwarden.com/help/"; + public const string HELP_ABOUT_ORGANIZATIONS = "https://bitwarden.com/help/about-organizations/"; + public const string HELP_FINGERPRINT_PHRASE = "https://bitwarden.com/help/fingerprint-phrase/"; + + public const string CONTACT_SUPPORT = "https://bitwarden.com/contact/"; + + /// + /// Link to go to settings website. Requires to pass website URL as parameter. + /// + public const string WEB_VAULT_SETTINGS_FORMAT = "{0}/#/settings"; + + /// + /// General website, not in the full format of a URL given that this is used as parameter of string resources to be shown to the user. + /// + public const string BITWARDEN_WEBSITE = "bitwarden.com"; + } +} diff --git a/src/Core/Services/PolicyService.cs b/src/Core/Services/PolicyService.cs index 9301763ac..4e5f00aab 100644 --- a/src/Core/Services/PolicyService.cs +++ b/src/Core/Services/PolicyService.cs @@ -50,6 +50,11 @@ namespace Bit.Core.Services return _policyCache; } + public async Task FirstOrDefault(PolicyType? type, string userId = null) + { + return (await GetAll(type, userId)).FirstOrDefault(); + } + public async Task Replace(Dictionary policies, string userId = null) { await _stateService.SetEncryptedPoliciesAsync(policies, userId); diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs index 443a94b15..6f2289a43 100644 --- a/src/Core/Services/StateService.cs +++ b/src/Core/Services/StateService.cs @@ -695,16 +695,19 @@ namespace Bit.Core.Services await SetValueAsync(Constants.LastBuildKey, value, await GetDefaultStorageOptionsAsync()); } + // TODO: [PS-961] Fix negative function names public async Task GetDisableFaviconAsync() { return await GetValueAsync(Constants.DisableFaviconKey, await GetDefaultStorageOptionsAsync()); } + // TODO: [PS-961] Fix negative function names public async Task SetDisableFaviconAsync(bool? value) { await SetValueAsync(Constants.DisableFaviconKey, value, await GetDefaultStorageOptionsAsync()); } + // TODO: [PS-961] Fix negative function names public async Task GetDisableAutoTotpCopyAsync(string userId = null) { var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, @@ -713,6 +716,7 @@ namespace Bit.Core.Services reconciledOptions); } + // TODO: [PS-961] Fix negative function names public async Task SetDisableAutoTotpCopyAsync(bool? value, string userId = null) { var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId }, diff --git a/src/Core/Services/VaultTimeoutService.cs b/src/Core/Services/VaultTimeoutService.cs index 51b29ac37..50b3a411b 100644 --- a/src/Core/Services/VaultTimeoutService.cs +++ b/src/Core/Services/VaultTimeoutService.cs @@ -23,7 +23,6 @@ namespace Bit.Core.Services private readonly ICipherService _cipherService; private readonly ICollectionService _collectionService; private readonly ISearchService _searchService; - private readonly IMessagingService _messagingService; private readonly ITokenService _tokenService; private readonly IUserVerificationService _userVerificationService; private readonly Func, Task> _lockedCallback; @@ -37,7 +36,6 @@ namespace Bit.Core.Services ICipherService cipherService, ICollectionService collectionService, ISearchService searchService, - IMessagingService messagingService, ITokenService tokenService, IUserVerificationService userVerificationService, Func, Task> lockedCallback, @@ -50,7 +48,6 @@ namespace Bit.Core.Services _cipherService = cipherService; _collectionService = collectionService; _searchService = searchService; - _messagingService = messagingService; _tokenService = tokenService; _userVerificationService = userVerificationService; _lockedCallback = lockedCallback; diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs index 4a85ab354..f2eada5be 100644 --- a/src/Core/Utilities/ServiceContainer.cs +++ b/src/Core/Utilities/ServiceContainer.cs @@ -58,8 +58,7 @@ namespace Bit.Core.Utilities var userVerificationService = new UserVerificationService(apiService, platformUtilsService, i18nService, cryptoService, stateService, keyConnectorService); var vaultTimeoutService = new VaultTimeoutService(cryptoService, stateService, platformUtilsService, - folderService, cipherService, collectionService, searchService, messagingService, tokenService, - userVerificationService, + folderService, cipherService, collectionService, searchService, tokenService, userVerificationService, (extras) => { messagingService.Send("locked", extras); diff --git a/src/iOS.Core/Renderers/CustomTabbedRenderer.cs b/src/iOS.Core/Renderers/CustomTabbedRenderer.cs index 60e59ba23..c5716058d 100644 --- a/src/iOS.Core/Renderers/CustomTabbedRenderer.cs +++ b/src/iOS.Core/Renderers/CustomTabbedRenderer.cs @@ -1,6 +1,7 @@ using System; using Bit.App.Abstractions; using Bit.App.Pages; +using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Utilities; using Bit.iOS.Core.Renderers; @@ -23,7 +24,7 @@ namespace Bit.iOS.Core.Renderers _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _broadcasterService.Subscribe(nameof(CustomTabbedRenderer), (message) => { - if (message.Command == "updatedTheme") + if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY) { Device.BeginInvokeOnMainThread(() => { diff --git a/src/iOS.Core/Services/AutofillHandler.cs b/src/iOS.Core/Services/AutofillHandler.cs index ba12bed5e..78e677493 100644 --- a/src/iOS.Core/Services/AutofillHandler.cs +++ b/src/iOS.Core/Services/AutofillHandler.cs @@ -12,9 +12,9 @@ namespace Bit.iOS.Core.Services public bool SupportsAutofillService() => false; public bool AutofillServiceEnabled() => false; public void Autofill(CipherView cipher) => throw new NotImplementedException(); - public bool AutofillAccessibilityOverlayPermitted() => throw new NotImplementedException(); - public bool AutofillAccessibilityServiceRunning() => throw new NotImplementedException(); - public bool AutofillServicesEnabled() => throw new NotImplementedException(); + public bool AutofillAccessibilityOverlayPermitted() => false; + public bool AutofillAccessibilityServiceRunning() => false; + public bool AutofillServicesEnabled() => false; public void CloseAutofill() => throw new NotImplementedException(); public void DisableAutofillService() => throw new NotImplementedException(); } diff --git a/src/iOS.Core/Services/DeviceActionService.cs b/src/iOS.Core/Services/DeviceActionService.cs index 23640f95a..569fd1491 100644 --- a/src/iOS.Core/Services/DeviceActionService.cs +++ b/src/iOS.Core/Services/DeviceActionService.cs @@ -339,6 +339,10 @@ namespace Bit.iOS.Core.Services return false; } + public bool SupportsAutofillServices() => UIDevice.CurrentDevice.CheckSystemVersion(12, 0); + public bool SupportsInlineAutofill() => false; + public bool SupportsDrawOver() => false; + private UIViewController GetPresentedViewController() { var window = UIApplication.SharedApplication.KeyWindow; @@ -390,5 +394,8 @@ namespace Bit.iOS.Core.Services { GetPresentedViewController().DismissViewController(true, null); } + + public string GetAutofillAccessibilityDescription() => null; + public string GetAutofillDrawOverDescription() => null; } } diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index 3b43a370c..2296bdfaf 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -115,6 +115,7 @@ namespace Bit.iOS.Core.Utilities 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(preferencesStorage); @@ -138,6 +139,7 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Register("cryptoService", cryptoService); ServiceContainer.Register("passwordRepromptService", passwordRepromptService); ServiceContainer.Register("avatarImageSourcePool", new AvatarImageSourcePool()); + ServiceContainer.Register(userPinService); } public static void RegisterFinallyBeforeBootstrap() diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 284f47299..7ce15f48b 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -75,7 +75,7 @@ namespace Bit.iOS { var task = StopEventTimerAsync(); } - else if (message.Command == "updatedTheme") + else if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY) { Device.BeginInvokeOnMainThread(() => { @@ -164,7 +164,7 @@ namespace Bit.iOS { await ASHelpers.ReplaceAllIdentities(); } - else if (message.Command == "vaultTimeoutActionChanged") + else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND) { var timeoutAction = await _stateService.GetVaultTimeoutActionAsync(); if (timeoutAction == VaultTimeoutAction.Logout) @@ -229,7 +229,7 @@ namespace Bit.iOS public override void WillEnterForeground(UIApplication uiApplication) { - _messagingService?.Send("resumed"); + _messagingService?.Send(AppHelpers.RESUMED_MESSAGE_COMMAND); base.WillEnterForeground(uiApplication); } diff --git a/src/iOS/Resources/Assets.xcassets/empty_login_requests.imageset/Contents.json b/src/iOS/Resources/Assets.xcassets/empty_login_requests.imageset/Contents.json new file mode 100644 index 000000000..920c75d19 --- /dev/null +++ b/src/iOS/Resources/Assets.xcassets/empty_login_requests.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "empty_login_requests.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "empty_login_requests_dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/src/iOS/Resources/Assets.xcassets/empty_login_requests.imageset/empty_login_requests.pdf b/src/iOS/Resources/Assets.xcassets/empty_login_requests.imageset/empty_login_requests.pdf new file mode 100644 index 0000000000000000000000000000000000000000..38f66ca7868e097db84a388a2d108b21afd99a00 GIT binary patch literal 1821 zcmah~c~BEq7^jvJnnG(kA_K$mEHDB!$!_j&jofGeV+0i_$dYUz8nfwU(?UFoojMAl zTopVKL1VF^7Cdn(inXGZL8u^FuwG~okb%+}5oG!{95VEezR9=WzTbP__4fO{c*(*d zSSVY-^g7#ra+Jvj2q4ndG6MntCmPqLQyD-2AvqHOz==?6D4c{^gHd=Gt{{{+Gbo6u zr$`)I&&)hvSSj7*fZBDwn3v%3=@ao^3Z3=cIjWZ0)C=|B_me|z*UAe`$z5Mv{Bk5P zH(@-ec&su&t{ybn*N4RJj8wChg629;^VXz2yjQ7fqEg1VOrq$dY6-3&XaW9#?9V)&^w9oc0DQrLSv1CI{xb;hb-s*hwK)S)r zIB?oq45^3p?p#fx-beCd;FYn|5~JT*tVB| zMY%50b7RvSM;gxL*jE%d*j&zE^oZpY-Tc*!@Aa_LQs3exXP9()C!cLr?O90@JqP zAfxBrn4;@(Yd4ObJakMK{bt}rQM^qIYuMR!_Zekn$=dv!z%-lkZ(FNK&xek&)yu6+ z+(X{NedgA~o|&pqE1SHaJ;zy?uX5eN#*~mbSGtF?m8I10wpYr(na?=M*xqH}*SAPh z1?%Ga?4lj-{L*68u$VD0bT`c(k6O9a&#s&)o##IsENpL`yYIT&j!%mG?%9ohw=}Dw zBkKOCoo>4lc>}4ZuQGCrcUMQ9>q?7wWhN7vSc$@RS&NfxSqkDRdrzn7ckg)MINv4D z+V5OzZS`26KW~86>+zeqQ{AKA2J4An zsj>J{lSHQe=;^kF+s}$(<6nv0SN~IE6|UpwFU;TZ?4fOq@WXlgyB&^>_r#n!&>(J_ zdnCbBtt@#_)4j^p_w;Ywj=y@3N;fN?Wj|fMCO68t+pg^IJF7Df&Kck8?sWVrCzsV1 z`PzBhD)z|vQYNldPI3ai(QKkAL~BSQkw`?Z(=*;Q*DUM`&!i$(P>@+5Ab5yZ~zPE5k@!OF`2q8pq&DH5Ni- zf+Pqb02M$%L~O1ABwGwZ2`vRJ^nkt$CYT>d5(eFjye7g}(5X~cz~CgN)$8bRimWMk z37|*=J~a!4yTb6V)e2l58A5{-<#6PA`FKTf*>Lu)U=@R9f4YaAY7<$6M}S1}PFX2O&w2M`JbJONOFNeo5B zuxaQ4v{M)&5yFo;3lmG=hn^F zq#)vk1&S>Ns#Yy7)SRNWf|ROt7p)5%d$eG|g(_C-(l=p|+8=!nD8k zO}t-ke>ALGYQ{HKt}1ChwZ}WMw7)FDrmuo*#CL`MQg4{CE?{vv)^=p%e7#+8?9RP^ z%{j0lsej$9w%ejVw$&$|3eOER3p4e8!{uMQSetwH-@9#2yt~;A`&<`ju9@5FKggXs zvEp*Lxww#jQRCv-ipYx|A6x!5&y=d#l-yWPySS#5B`)X+_By-RXXVI)D%*2A0u~EW z>`vB}S6@*bK~|@hh;*gv?t2RIqHHfPK|chn>HUJeA==|ZNSJGbGlh5VCCHiF*zU|{ z*O~b(Pt486$I+lF?t{FhNK@r(FC7Rj%(rg1mSxw~blSc_lYFH>Rj^goI54lh(jy|R zT8D0)Q(V+@CSLK*iziI?!jdNEg7p=VE0;S*AMEPzKyA(j4J`AYd0)_cS!5Mo6Fb{A znjMInsoY;=WuuSiYgdT7ZCt6a%>5Z~&C=VSPOa>;i>S-8Ee`%lxzx}&*rRp~ z|E4AvxzcCT+5K~Z*EJ*RnadvDeaG3?HLIoHffp;@Hm$`8UZ2voXYwfTd03 z_$(Ce3L|s$T2d1p!hxgdh&3cbnP@G^0In#SNy%|xkKaVElViBE;wZh2fk|-rYbIhh zak)-1Ae&?$j;3@bExfx#T0&1#0QEy3b+S~TskHY z<7`NR;Tp8fiR}OEbH3p|IkYWxSi(T1Tk_@j!;CTJHP7;BGL4S_1s{eyHh>JrB{)a}V;G9cp=E9W<2Z)+ z@Od*3`_um+bN&k!C7^L#}K9(l*29mb?d&TPuNEk9l7Y#+R ifJ=Oo(-For3UKVO(2ZdUnzdwz;&O~H6sls?{C@!7`)4!& literal 0 HcmV?d00001 diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index 6b4c07f0e..4a21e24df 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -183,6 +183,9 @@ + + +