mirror of
https://github.com/bitwarden/mobile
synced 2026-01-08 11:33:31 +00:00
PM-3349 PM-3350 MAUI Migration Initial
This commit is contained in:
85
src/Core/Pages/Settings/AboutSettingsPage.xaml
Normal file
85
src/Core/Pages/Settings/AboutSettingsPage.xaml
Normal file
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:AboutSettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:core="clr-namespace:Bit.Core"
|
||||
x:Class="Bit.App.Pages.AboutSettingsPage"
|
||||
Title="{u:I18n About}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:AboutSettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<StackLayout>
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n SubmitCrashLogs}"
|
||||
IsToggled="{Binding ShouldSubmitCrashLogs, Mode=TwoWay}"
|
||||
AutomationId="SubmitCrashLogsSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n BitwardenHelpCenter}"
|
||||
GoToLinkCommand="{Binding GoToHelpCenterCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n ContactBitwardenSupport}"
|
||||
GoToLinkCommand="{Binding ContactBitwardenSupportCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n WebVault}"
|
||||
GoToLinkCommand="{Binding GoToWebVaultCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n LearnOrg}"
|
||||
GoToLinkCommand="{Binding GoToLearnAboutOrgsCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n RateTheApp}"
|
||||
GoToLinkCommand="{Binding RateTheAppCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<StackLayout
|
||||
Padding="16,12"
|
||||
Orientation="Horizontal">
|
||||
<controls:CustomLabel
|
||||
Text="{Binding AppInfo}"
|
||||
MaxLines="10"
|
||||
StyleClass="box-footer-label"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
LineBreakMode="TailTruncation" />
|
||||
|
||||
<controls:IconLabel
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
TextColor="Black"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="Center"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n CopyAppInformation}">
|
||||
<controls:IconLabel.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding CopyAppInfoCommand}" />
|
||||
</controls:IconLabel.GestureRecognizers>
|
||||
</controls:IconLabel>
|
||||
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
||||
37
src/Core/Pages/Settings/AboutSettingsPage.xaml.cs
Normal file
37
src/Core/Pages/Settings/AboutSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AboutSettingsPage : BaseContentPage
|
||||
{
|
||||
private readonly AboutSettingsPageViewModel _vm;
|
||||
|
||||
public AboutSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as AboutSettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
|
||||
|
||||
Navigation.PopAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/Core/Pages/Settings/AboutSettingsPageViewModel.cs
Normal file
159
src/Core/Pages/Settings/AboutSettingsPageViewModel.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class AboutSettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private bool _inited;
|
||||
private bool _shouldSubmitCrashLogs;
|
||||
|
||||
public AboutSettingsPageViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
|
||||
var environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||
var clipboardService = ServiceContainer.Resolve<IClipboardService>();
|
||||
|
||||
ToggleSubmitCrashLogsCommand = CreateDefaultAsyncCommnad(ToggleSubmitCrashLogsAsync);
|
||||
|
||||
GoToHelpCenterCommand = CreateDefaultAsyncCommnad(
|
||||
() => LaunchUriAsync(AppResources.LearnMoreAboutHowToUseBitwardenOnTheHelpCenter,
|
||||
AppResources.ContinueToHelpCenter,
|
||||
ExternalLinksConstants.HELP_CENTER));
|
||||
|
||||
ContactBitwardenSupportCommand = CreateDefaultAsyncCommnad(
|
||||
() => LaunchUriAsync(AppResources.ContactSupportDescriptionLong,
|
||||
AppResources.ContinueToContactSupport,
|
||||
ExternalLinksConstants.CONTACT_SUPPORT));
|
||||
|
||||
GoToWebVaultCommand = CreateDefaultAsyncCommnad(
|
||||
() => LaunchUriAsync(AppResources.ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp,
|
||||
AppResources.ContinueToWebApp,
|
||||
environmentService.GetWebVaultUrl()));
|
||||
|
||||
GoToLearnAboutOrgsCommand = CreateDefaultAsyncCommnad(
|
||||
() => LaunchUriAsync(AppResources.LearnAboutOrganizationsDescriptionLong,
|
||||
string.Format(AppResources.ContinueToX, ExternalLinksConstants.BITWARDEN_WEBSITE),
|
||||
ExternalLinksConstants.HELP_ABOUT_ORGANIZATIONS));
|
||||
|
||||
RateTheAppCommand = CreateDefaultAsyncCommnad(RateAppAsync);
|
||||
|
||||
CopyAppInfoCommand = CreateDefaultAsyncCommnad(
|
||||
() => clipboardService.CopyTextAsync(AppInfo));
|
||||
}
|
||||
|
||||
public bool ShouldSubmitCrashLogs
|
||||
{
|
||||
get => _shouldSubmitCrashLogs;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _shouldSubmitCrashLogs, value);
|
||||
((ICommand)ToggleSubmitCrashLogsCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
|
||||
public string AppInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
var appInfo = string.Format("{0}: {1} ({2})",
|
||||
AppResources.Version,
|
||||
_platformUtilsService.GetApplicationVersion(),
|
||||
_deviceActionService.GetBuildNumber());
|
||||
|
||||
return $"© Bitwarden Inc. 2015-{DateTime.Now.Year}\n\n{appInfo}";
|
||||
}
|
||||
}
|
||||
|
||||
public AsyncCommand ToggleSubmitCrashLogsCommand { get; }
|
||||
public ICommand GoToHelpCenterCommand { get; }
|
||||
public ICommand ContactBitwardenSupportCommand { get; }
|
||||
public ICommand GoToWebVaultCommand { get; }
|
||||
public ICommand GoToLearnAboutOrgsCommand { get; }
|
||||
public ICommand RateTheAppCommand { get; }
|
||||
public ICommand CopyAppInfoCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_shouldSubmitCrashLogs = await _logger.IsEnabled();
|
||||
|
||||
_inited = true;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(ShouldSubmitCrashLogs));
|
||||
ToggleSubmitCrashLogsCommand.RaiseCanExecuteChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ToggleSubmitCrashLogsAsync()
|
||||
{
|
||||
await _logger.SetEnabled(ShouldSubmitCrashLogs);
|
||||
_shouldSubmitCrashLogs = await _logger.IsEnabled();
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(ShouldSubmitCrashLogs)));
|
||||
}
|
||||
|
||||
private async Task LaunchUriAsync(string dialogText, string dialogTitle, string uri)
|
||||
{
|
||||
if (await _platformUtilsService.ShowDialogAsync(dialogText, dialogTitle, AppResources.Continue, AppResources.Cancel))
|
||||
{
|
||||
_platformUtilsService.LaunchUri(uri);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RateAppAsync()
|
||||
{
|
||||
if (await _platformUtilsService.ShowDialogAsync(AppResources.RateAppDescriptionLong, AppResources.ContinueToAppStore, AppResources.Continue, AppResources.Cancel))
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(_deviceActionService.RateApp);
|
||||
}
|
||||
}
|
||||
|
||||
/// INFO: Left here in case we need to debug push notifications
|
||||
/// <summary>
|
||||
/// Sets up app info plus debugging information for push notifications.
|
||||
/// Useful when trying to solve problems regarding push notifications.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// Add an IniAsync() method to be called on view appearing, change the AppInfo to be a normal property with setter
|
||||
/// and set the result of this method in the main thread to that property to show that in the UI.
|
||||
/// </example>
|
||||
// public async Task<string> GetAppInfoForPushNotificationsDebugAsync()
|
||||
// {
|
||||
// var stateService = ServiceContainer.Resolve<IStateService>();
|
||||
|
||||
// var appInfo = string.Format("{0}: {1} ({2})", AppResources.Version,
|
||||
// _platformUtilsService.GetApplicationVersion(), _deviceActionService.GetBuildNumber());
|
||||
|
||||
//#if DEBUG
|
||||
// var pushNotificationsRegistered = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService").IsRegisteredForPush;
|
||||
// var pnServerRegDate = await stateService.GetPushLastRegistrationDateAsync();
|
||||
// var pnServerError = await stateService.GetPushInstallationRegistrationErrorAsync();
|
||||
|
||||
// var pnServerRegDateMessage = default(DateTime) == pnServerRegDate ? "-" : $"{pnServerRegDate.GetValueOrDefault().ToShortDateString()}-{pnServerRegDate.GetValueOrDefault().ToShortTimeString()} UTC";
|
||||
// var errorMessage = string.IsNullOrEmpty(pnServerError) ? string.Empty : $"Push Notifications Server Registration error: {pnServerError}";
|
||||
|
||||
// var text = string.Format("© Bitwarden Inc. 2015-{0}\n\n{1}\nPush Notifications registered:{2}\nPush Notifications Server Last Date :{3}\n{4}", DateTime.Now.Year, appInfo, pushNotificationsRegistered, pnServerRegDateMessage, errorMessage);
|
||||
//#else
|
||||
// var text = string.Format("© Bitwarden Inc. 2015-{0}\n\n{1}", DateTime.Now.Year, appInfo);
|
||||
//#endif
|
||||
// return text;
|
||||
// }
|
||||
}
|
||||
}
|
||||
58
src/Core/Pages/Settings/AppearanceSettingsPage.xaml
Normal file
58
src/Core/Pages/Settings/AppearanceSettingsPage.xaml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:AppearanceSettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:Class="Bit.App.Pages.AppearanceSettingsPage"
|
||||
Title="{u:I18n Appearance}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:AppearanceSettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="5">
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n Language}"
|
||||
Subtitle="{u:I18n LanguageChangeRequiresAppRestart}"
|
||||
DisplayValue="{Binding LanguagePickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding LanguagePickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="LanguageChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n Theme}"
|
||||
Subtitle="{u:I18n ThemeDescription}"
|
||||
DisplayValue="{Binding ThemePickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding ThemePickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="ThemeChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n DefaultDarkTheme}"
|
||||
Subtitle="{u:I18n DefaultDarkThemeDescriptionLong}"
|
||||
DisplayValue="{Binding DefaultDarkThemePickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding DefaultDarkThemePickerViewModel.SelectOptionCommand}"
|
||||
IsVisible="{Binding ShowDefaultDarkThemePicker}"
|
||||
AutomationId="DefaultDarkThemeChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n ShowWebsiteIcons}"
|
||||
Subtitle="{u:I18n ShowWebsiteIconsDescription}"
|
||||
IsToggled="{Binding ShowWebsiteIcons}"
|
||||
IsEnabled="{Binding IsShowWebsiteIconsEnabled}"
|
||||
AutomationId="ShowWebsiteIconsSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</pages:BaseContentPage>
|
||||
44
src/Core/Pages/Settings/AppearanceSettingsPage.xaml.cs
Normal file
44
src/Core/Pages/Settings/AppearanceSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AppearanceSettingsPage : BaseContentPage
|
||||
{
|
||||
private readonly AppearanceSettingsPageViewModel _vm;
|
||||
|
||||
public AppearanceSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as AppearanceSettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
_vm.SubscribeEvents();
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
|
||||
|
||||
Navigation.PopModalAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_vm.UnsubscribeEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
203
src/Core/Pages/Settings/AppearanceSettingsPageViewModel.cs
Normal file
203
src/Core/Pages/Settings/AppearanceSettingsPageViewModel.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class AppearanceSettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
|
||||
private bool _inited;
|
||||
private bool _showWebsiteIcons;
|
||||
|
||||
public AppearanceSettingsPageViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>();
|
||||
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
|
||||
LanguagePickerViewModel = new PickerViewModel<string>(
|
||||
deviceActionService,
|
||||
_logger,
|
||||
OnLanguageChangingAsync,
|
||||
AppResources.Language,
|
||||
() => _inited,
|
||||
ex => HandleException(ex));
|
||||
|
||||
ThemePickerViewModel = new PickerViewModel<string>(
|
||||
deviceActionService,
|
||||
_logger,
|
||||
key => OnThemeChangingAsync(key, DefaultDarkThemePickerViewModel.SelectedKey),
|
||||
AppResources.Theme,
|
||||
() => _inited,
|
||||
ex => HandleException(ex));
|
||||
ThemePickerViewModel.SetAfterSelectionChanged(_ =>
|
||||
MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(ShowDefaultDarkThemePicker));
|
||||
}));
|
||||
|
||||
DefaultDarkThemePickerViewModel = new PickerViewModel<string>(
|
||||
deviceActionService,
|
||||
_logger,
|
||||
key => OnThemeChangingAsync(ThemePickerViewModel.SelectedKey, key),
|
||||
AppResources.DefaultDarkTheme,
|
||||
() => _inited,
|
||||
ex => HandleException(ex));
|
||||
|
||||
ToggleShowWebsiteIconsCommand = CreateDefaultAsyncCommnad(ToggleShowWebsiteIconsAsync, () => _inited);
|
||||
}
|
||||
|
||||
public PickerViewModel<string> LanguagePickerViewModel { get; }
|
||||
public PickerViewModel<string> ThemePickerViewModel { get; }
|
||||
public PickerViewModel<string> DefaultDarkThemePickerViewModel { get; }
|
||||
|
||||
public bool ShowDefaultDarkThemePicker => ThemePickerViewModel.SelectedKey == string.Empty;
|
||||
|
||||
public bool ShowWebsiteIcons
|
||||
{
|
||||
get => _showWebsiteIcons;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _showWebsiteIcons, value))
|
||||
{
|
||||
((ICommand)ToggleShowWebsiteIconsCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsShowWebsiteIconsEnabled => ToggleShowWebsiteIconsCommand.CanExecute(null);
|
||||
|
||||
public AsyncCommand ToggleShowWebsiteIconsCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_showWebsiteIcons = !(await _stateService.GetDisableFaviconAsync() ?? false);
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(ShowWebsiteIcons)));
|
||||
|
||||
InitLanguagePicker();
|
||||
await InitThemePickerAsync();
|
||||
await InitDefaultDarkThemePickerAsync();
|
||||
|
||||
_inited = true;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
ToggleShowWebsiteIconsCommand.RaiseCanExecuteChanged();
|
||||
LanguagePickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
ThemePickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
DefaultDarkThemePickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private void InitLanguagePicker()
|
||||
{
|
||||
var options = new Dictionary<string, string>
|
||||
{
|
||||
[string.Empty] = AppResources.DefaultSystem
|
||||
};
|
||||
_i18nService.LocaleNames
|
||||
.ToList()
|
||||
.ForEach(pair => options[pair.Key] = pair.Value);
|
||||
|
||||
var selectedKey = _stateService.GetLocale() ?? string.Empty;
|
||||
|
||||
LanguagePickerViewModel.Init(options, selectedKey, string.Empty);
|
||||
}
|
||||
|
||||
private async Task InitThemePickerAsync()
|
||||
{
|
||||
var options = new Dictionary<string, string>
|
||||
{
|
||||
[string.Empty] = AppResources.ThemeDefault,
|
||||
[ThemeManager.Light] = AppResources.Light,
|
||||
[ThemeManager.Dark] = AppResources.Dark,
|
||||
[ThemeManager.Black] = AppResources.Black,
|
||||
[ThemeManager.Nord] = AppResources.Nord,
|
||||
[ThemeManager.SolarizedDark] = AppResources.SolarizedDark
|
||||
};
|
||||
|
||||
var selectedKey = await _stateService.GetThemeAsync() ?? string.Empty;
|
||||
|
||||
ThemePickerViewModel.Init(options, selectedKey, string.Empty);
|
||||
|
||||
TriggerPropertyChanged(nameof(ShowDefaultDarkThemePicker));
|
||||
}
|
||||
|
||||
private async Task InitDefaultDarkThemePickerAsync()
|
||||
{
|
||||
var options = new Dictionary<string, string>
|
||||
{
|
||||
[ThemeManager.Dark] = AppResources.Dark,
|
||||
[ThemeManager.Black] = AppResources.Black,
|
||||
[ThemeManager.Nord] = AppResources.Nord,
|
||||
[ThemeManager.SolarizedDark] = AppResources.SolarizedDark
|
||||
};
|
||||
|
||||
var selectedKey = await _stateService.GetAutoDarkThemeAsync() ?? ThemeManager.Dark;
|
||||
|
||||
DefaultDarkThemePickerViewModel.Init(options, selectedKey, ThemeManager.Dark);
|
||||
}
|
||||
|
||||
private async Task<bool> OnLanguageChangingAsync(string selectedLanguage)
|
||||
{
|
||||
_stateService.SetLocale(selectedLanguage == string.Empty ? (string)null : selectedLanguage);
|
||||
|
||||
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.LanguageChangeXDescription, LanguagePickerViewModel.SelectedValue), AppResources.Language, AppResources.Ok);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> OnThemeChangingAsync(string selectedTheme, string selectedDefaultDarkTheme)
|
||||
{
|
||||
await _stateService.SetThemeAsync(selectedTheme == string.Empty ? (string)null : selectedTheme);
|
||||
await _stateService.SetAutoDarkThemeAsync(selectedDefaultDarkTheme == string.Empty ? (string)null : selectedDefaultDarkTheme);
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
ThemeManager.SetTheme(Application.Current.Resources);
|
||||
_messagingService.Send(ThemeManager.UPDATED_THEME_MESSAGE_KEY);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ToggleShowWebsiteIconsAsync()
|
||||
{
|
||||
// TODO: [PS-961] Fix negative function names
|
||||
await _stateService.SetDisableFaviconAsync(!ShowWebsiteIcons);
|
||||
}
|
||||
|
||||
private void ToggleShowWebsiteIconsCommand_CanExecuteChanged(object sender, EventArgs e)
|
||||
{
|
||||
TriggerPropertyChanged(nameof(IsShowWebsiteIconsEnabled));
|
||||
}
|
||||
|
||||
internal void SubscribeEvents()
|
||||
{
|
||||
ToggleShowWebsiteIconsCommand.CanExecuteChanged += ToggleShowWebsiteIconsCommand_CanExecuteChanged;
|
||||
}
|
||||
|
||||
internal void UnsubscribeEvents()
|
||||
{
|
||||
ToggleShowWebsiteIconsCommand.CanExecuteChanged -= ToggleShowWebsiteIconsCommand_CanExecuteChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/Core/Pages/Settings/AutofillPage.xaml
Normal file
48
src/Core/Pages/Settings/AutofillPage.xaml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.AutofillPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
Title="{u:I18n PasswordAutofill}">
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="5"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand">
|
||||
<Label Text="{u:I18n ExtensionInstantAccess}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap"
|
||||
StyleClass="text-lg"
|
||||
Margin="0, 0, 0, 15" />
|
||||
<Label Text="{u:I18n AutofillTurnOn}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap"
|
||||
Margin="0, 0, 0, 15" />
|
||||
<Label Text="{u:I18n AutofillTurnOn1}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn2}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn3}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn4}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn5}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="autofill-kb.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, 10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
20
src/Core/Pages/Settings/AutofillPage.xaml.cs
Normal file
20
src/Core/Pages/Settings/AutofillPage.xaml.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillPage : BaseContentPage
|
||||
{
|
||||
public AutofillPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/Core/Pages/Settings/AutofillSettingsPage.xaml
Normal file
133
src/Core/Pages/Settings/AutofillSettingsPage.xaml
Normal file
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:AutofillSettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:Class="Bit.App.Pages.AutofillSettingsPage"
|
||||
Title="{u:I18n Autofill}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:AutofillSettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="5">
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n Autofill}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n AutofillServices}"
|
||||
Subtitle="{u:I18n AutofillServicesExplanationLong}"
|
||||
IsVisible="{Binding SupportsAndroidAutofillServices}"
|
||||
IsToggled="{Binding UseAutofillServices}"
|
||||
AutomationId="AutofillServicesSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n InlineAutofill}"
|
||||
Subtitle="{u:I18n UseInlineAutofillExplanationLong}"
|
||||
IsToggled="{Binding UseInlineAutofill}"
|
||||
IsVisible="{Binding ShowUseInlineAutofillToggle}"
|
||||
IsEnabled="{Binding UseAutofillServices}"
|
||||
AutomationId="InlineAutofillSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n Accessibility}"
|
||||
Subtitle="{Binding UseAccessibilityDescription}"
|
||||
IsToggled="{Binding UseAccessibility}"
|
||||
IsVisible="{Binding ShowUseAccessibilityToggle}"
|
||||
AutomationId="AccessibilitySwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n DrawOver}"
|
||||
Subtitle="{Binding UseDrawOverDescription}"
|
||||
IsToggled="{Binding UseDrawOver}"
|
||||
IsVisible="{Binding ShowUseDrawOverToggle}"
|
||||
IsEnabled="{Binding UseAccessibility}"
|
||||
AutomationId="DrawOverSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n PasswordAutofill}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
IsVisible="{Binding SupportsiOSAutofill}"
|
||||
AutomationId="PasswordAutofillLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToPasswordAutofillCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n AppExtension}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
IsVisible="{OnPlatform iOS=True, Android=False}"
|
||||
AutomationId="AppExtensionLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToAppExtensionCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<BoxView StyleClass="settings-box-row-separator" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n AdditionalOptions}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n CopyTotpAutomatically}"
|
||||
Subtitle="{u:I18n CopyTotpAutomaticallyDescription}"
|
||||
IsToggled="{Binding CopyTotpAutomatically}"
|
||||
AutomationId="CopyTotpAutomaticallySwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n AskToAddLogin}"
|
||||
Subtitle="{u:I18n AskToAddLoginDescription}"
|
||||
IsToggled="{Binding AskToAddLogin}"
|
||||
IsVisible="{Binding SupportsAndroidAutofillServices}"
|
||||
AutomationId="AskToAddLoginSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n DefaultUriMatchDetection}"
|
||||
Subtitle="{u:I18n DefaultUriMatchDetectionDescription}"
|
||||
DisplayValue="{Binding DefaultUriMatchDetectionPickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding DefaultUriMatchDetectionPickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="DefaultUriMatchDetectionChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<StackLayout
|
||||
Spacing="0"
|
||||
Padding="16,12"
|
||||
IsVisible="{Binding SupportsAndroidAutofillServices}"
|
||||
AutomationId="BlockAutoFillView">
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToBlockAutofillUrisCommand}" />
|
||||
</StackLayout.GestureRecognizers>
|
||||
<controls:CustomLabel
|
||||
MaxLines="2"
|
||||
Text="{u:I18n BlockAutoFill}"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
LineBreakMode="TailTruncation" />
|
||||
<Label
|
||||
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch"
|
||||
Margin="0,0,0,0"/>
|
||||
</StackLayout>
|
||||
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</pages:BaseContentPage>
|
||||
37
src/Core/Pages/Settings/AutofillSettingsPage.xaml.cs
Normal file
37
src/Core/Pages/Settings/AutofillSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillSettingsPage : BaseContentPage
|
||||
{
|
||||
AutofillSettingsPageViewModel _vm;
|
||||
|
||||
public AutofillSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as AutofillSettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
|
||||
|
||||
Navigation.PopAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
184
src/Core/Pages/Settings/AutofillSettingsPageViewModel.android.cs
Normal file
184
src/Core/Pages/Settings/AutofillSettingsPageViewModel.android.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillSettingsPageViewModel
|
||||
{
|
||||
private bool _useAutofillServices;
|
||||
private bool _useInlineAutofill;
|
||||
private bool _useAccessibility;
|
||||
private bool _useDrawOver;
|
||||
private bool _askToAddLogin;
|
||||
|
||||
public bool SupportsAndroidAutofillServices => // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
Device.RuntimePlatform == Device.Android && _deviceActionService.SupportsAutofillServices();
|
||||
|
||||
public bool UseAutofillServices
|
||||
{
|
||||
get => _useAutofillServices;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useAutofillServices, value))
|
||||
{
|
||||
((ICommand)ToggleUseAutofillServicesCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowUseInlineAutofillToggle => _deviceActionService.SupportsInlineAutofill();
|
||||
|
||||
public bool UseInlineAutofill
|
||||
{
|
||||
get => _useInlineAutofill;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useInlineAutofill, value))
|
||||
{
|
||||
((ICommand)ToggleUseInlineAutofillCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowUseAccessibilityToggle => // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
Device.RuntimePlatform == Device.Android;
|
||||
|
||||
public string UseAccessibilityDescription => _deviceActionService.GetAutofillAccessibilityDescription();
|
||||
|
||||
public bool UseAccessibility
|
||||
{
|
||||
get => _useAccessibility;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useAccessibility, value))
|
||||
{
|
||||
((ICommand)ToggleUseAccessibilityCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowUseDrawOverToggle => _deviceActionService.SupportsDrawOver();
|
||||
|
||||
public bool UseDrawOver
|
||||
{
|
||||
get => _useDrawOver;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useDrawOver, value))
|
||||
{
|
||||
((ICommand)ToggleUseDrawOverCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string UseDrawOverDescription => _deviceActionService.GetAutofillDrawOverDescription();
|
||||
|
||||
public bool AskToAddLogin
|
||||
{
|
||||
get => _askToAddLogin;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _askToAddLogin, value))
|
||||
{
|
||||
((ICommand)ToggleAskToAddLoginCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AsyncCommand ToggleUseAutofillServicesCommand { get; private set; }
|
||||
public AsyncCommand ToggleUseInlineAutofillCommand { get; private set; }
|
||||
public AsyncCommand ToggleUseAccessibilityCommand { get; private set; }
|
||||
public AsyncCommand ToggleUseDrawOverCommand { get; private set; }
|
||||
public AsyncCommand ToggleAskToAddLoginCommand { get; private set; }
|
||||
public ICommand GoToBlockAutofillUrisCommand { get; private set; }
|
||||
|
||||
private void InitAndroidCommands()
|
||||
{
|
||||
ToggleUseAutofillServicesCommand = CreateDefaultAsyncCommnad(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseAutofillServices()), () => _inited);
|
||||
ToggleUseInlineAutofillCommand = CreateDefaultAsyncCommnad(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseInlineAutofillEnabledAsync()), () => _inited);
|
||||
ToggleUseAccessibilityCommand = CreateDefaultAsyncCommnad(ToggleUseAccessibilityAsync, () => _inited);
|
||||
ToggleUseDrawOverCommand = CreateDefaultAsyncCommnad(() => MainThread.InvokeOnMainThreadAsync(() => ToggleDrawOver()), () => _inited);
|
||||
ToggleAskToAddLoginCommand = CreateDefaultAsyncCommnad(ToggleAskToAddLoginAsync, () => _inited);
|
||||
GoToBlockAutofillUrisCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushAsync(new BlockAutofillUrisPage()));
|
||||
}
|
||||
|
||||
private async Task InitAndroidAutofillSettingsAsync()
|
||||
{
|
||||
_useInlineAutofill = await _stateService.GetInlineAutofillEnabledAsync() ?? true;
|
||||
|
||||
await UpdateAndroidAutofillSettingsAsync();
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(UseInlineAutofill));
|
||||
});
|
||||
}
|
||||
|
||||
private async Task UpdateAndroidAutofillSettingsAsync()
|
||||
{
|
||||
_useAutofillServices =
|
||||
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
|
||||
_useAccessibility = _autofillHandler.AutofillAccessibilityServiceRunning();
|
||||
_useDrawOver = _autofillHandler.AutofillAccessibilityOverlayPermitted();
|
||||
_askToAddLogin = await _stateService.GetAutofillDisableSavePromptAsync() != true;
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(UseAutofillServices));
|
||||
TriggerPropertyChanged(nameof(UseAccessibility));
|
||||
TriggerPropertyChanged(nameof(UseDrawOver));
|
||||
TriggerPropertyChanged(nameof(AskToAddLogin));
|
||||
});
|
||||
}
|
||||
|
||||
private void ToggleUseAutofillServices()
|
||||
{
|
||||
if (UseAutofillServices)
|
||||
{
|
||||
_deviceActionService.OpenAutofillSettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
_autofillHandler.DisableAutofillService();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleUseInlineAutofillEnabledAsync()
|
||||
{
|
||||
await _stateService.SetInlineAutofillEnabledAsync(UseInlineAutofill);
|
||||
}
|
||||
|
||||
private async Task ToggleUseAccessibilityAsync()
|
||||
{
|
||||
if (!_autofillHandler.AutofillAccessibilityServiceRunning()
|
||||
&&
|
||||
!await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText, AppResources.AccessibilityServiceDisclosure,
|
||||
AppResources.Accept, AppResources.Decline))
|
||||
{
|
||||
_useAccessibility = false;
|
||||
await MainThread.InvokeOnMainThreadAsync(() => TriggerPropertyChanged(nameof(UseAccessibility)));
|
||||
return;
|
||||
}
|
||||
_deviceActionService.OpenAccessibilitySettings();
|
||||
}
|
||||
|
||||
private void ToggleDrawOver()
|
||||
{
|
||||
if (!UseAccessibility)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_deviceActionService.OpenAccessibilityOverlayPermissionSettings();
|
||||
}
|
||||
|
||||
private async Task ToggleAskToAddLoginAsync()
|
||||
{
|
||||
await _stateService.SetAutofillDisableSavePromptAsync(!AskToAddLogin);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/Core/Pages/Settings/AutofillSettingsPageViewModel.cs
Normal file
111
src/Core/Pages/Settings/AutofillSettingsPageViewModel.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillSettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAutofillHandler _autofillHandler;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
|
||||
private bool _inited;
|
||||
private bool _copyTotpAutomatically;
|
||||
|
||||
public AutofillSettingsPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
|
||||
DefaultUriMatchDetectionPickerViewModel = new PickerViewModel<UriMatchType>(
|
||||
_deviceActionService,
|
||||
ServiceContainer.Resolve<ILogger>(),
|
||||
DefaultUriMatchDetectionChangingAsync,
|
||||
AppResources.DefaultUriMatchDetection,
|
||||
() => _inited,
|
||||
ex => HandleException(ex));
|
||||
|
||||
ToggleCopyTotpAutomaticallyCommand = CreateDefaultAsyncCommnad(ToggleCopyTotpAutomaticallyAsync, () => _inited);
|
||||
|
||||
InitAndroidCommands();
|
||||
InitIOSCommands();
|
||||
}
|
||||
|
||||
public bool CopyTotpAutomatically
|
||||
{
|
||||
get => _copyTotpAutomatically;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _copyTotpAutomatically, value))
|
||||
{
|
||||
((ICommand)ToggleCopyTotpAutomaticallyCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PickerViewModel<UriMatchType> DefaultUriMatchDetectionPickerViewModel { get; }
|
||||
|
||||
public AsyncCommand ToggleCopyTotpAutomaticallyCommand { get; private set; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await InitAndroidAutofillSettingsAsync();
|
||||
|
||||
_copyTotpAutomatically = await _stateService.GetDisableAutoTotpCopyAsync() != true;
|
||||
|
||||
await InitDefaultUriMatchDetectionPickerViewModelAsync();
|
||||
|
||||
_inited = true;
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(CopyTotpAutomatically));
|
||||
|
||||
ToggleUseAutofillServicesCommand.RaiseCanExecuteChanged();
|
||||
ToggleUseInlineAutofillCommand.RaiseCanExecuteChanged();
|
||||
ToggleUseAccessibilityCommand.RaiseCanExecuteChanged();
|
||||
ToggleUseDrawOverCommand.RaiseCanExecuteChanged();
|
||||
DefaultUriMatchDetectionPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task InitDefaultUriMatchDetectionPickerViewModelAsync()
|
||||
{
|
||||
var options = new Dictionary<UriMatchType, string>
|
||||
{
|
||||
[UriMatchType.Domain] = AppResources.BaseDomain,
|
||||
[UriMatchType.Host] = AppResources.Host,
|
||||
[UriMatchType.StartsWith] = AppResources.StartsWith,
|
||||
[UriMatchType.RegularExpression] = AppResources.RegEx,
|
||||
[UriMatchType.Exact] = AppResources.Exact,
|
||||
[UriMatchType.Never] = AppResources.Never
|
||||
};
|
||||
|
||||
var defaultUriMatchDetection = ((UriMatchType?)await _stateService.GetDefaultUriMatchAsync()) ?? UriMatchType.Domain;
|
||||
|
||||
DefaultUriMatchDetectionPickerViewModel.Init(options, defaultUriMatchDetection, UriMatchType.Domain);
|
||||
}
|
||||
|
||||
private async Task ToggleCopyTotpAutomaticallyAsync()
|
||||
{
|
||||
await _stateService.SetDisableAutoTotpCopyAsync(!CopyTotpAutomatically);
|
||||
}
|
||||
|
||||
private async Task<bool> DefaultUriMatchDetectionChangingAsync(UriMatchType type)
|
||||
{
|
||||
await _stateService.SetDefaultUriMatchAsync((int?)type);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/Core/Pages/Settings/AutofillSettingsPageViewModel.ios.cs
Normal file
21
src/Core/Pages/Settings/AutofillSettingsPageViewModel.ios.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Windows.Input;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillSettingsPageViewModel
|
||||
{
|
||||
public bool SupportsiOSAutofill => // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
Device.RuntimePlatform == Device.iOS && _deviceActionService.SupportsAutofillServices();
|
||||
|
||||
public ICommand GoToPasswordAutofillCommand { get; private set; }
|
||||
public ICommand GoToAppExtensionCommand { get; private set; }
|
||||
|
||||
private void InitIOSCommands()
|
||||
{
|
||||
GoToPasswordAutofillCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillPage())));
|
||||
GoToAppExtensionCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new ExtensionPage())));
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/Core/Pages/Settings/BlockAutofillUrisPage.xaml
Normal file
86
src/Core/Pages/Settings/BlockAutofillUrisPage.xaml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.BlockAutofillUrisPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:core="clr-namespace:Bit.Core"
|
||||
x:DataType="pages:BlockAutofillUrisPageViewModel"
|
||||
NavigationPage.HasBackButton="False"
|
||||
Title="{u:I18n BlockAutoFill}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:BlockAutofillUrisPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
<StackLayout Orientation="Vertical">
|
||||
<Image
|
||||
x:Name="_emptyUrisPlaceholder"
|
||||
HorizontalOptions="Center"
|
||||
WidthRequest="120"
|
||||
HeightRequest="120"
|
||||
Margin="0,100,0,0"
|
||||
IsVisible="{Binding ShowList, Converter={StaticResource inverseBool}}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ThereAreNoBlockedURIs}" />
|
||||
<controls:CustomLabel
|
||||
StyleClass="box-label-regular"
|
||||
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
|
||||
FontWeight="500"
|
||||
HorizontalTextAlignment="Center"
|
||||
Margin="14,10,14,0"/>
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding BlockedUris}"
|
||||
IsVisible="{Binding ShowList}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
Margin="0,5,0,0"
|
||||
SelectionMode="None"
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Blocked Autofill Uris"
|
||||
AutomationId="BlockedUrisCellList">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="pages:BlockAutofillUriItemViewModel">
|
||||
<StackLayout
|
||||
Orientation="Vertical"
|
||||
AutomationId="BlockedUriCell">
|
||||
<StackLayout
|
||||
Orientation="Horizontal">
|
||||
<controls:CustomLabel
|
||||
VerticalOptions="Center"
|
||||
StyleClass="box-label-regular"
|
||||
Text="{Binding Uri}"
|
||||
MaxLines="2"
|
||||
LineBreakMode="TailTruncation"
|
||||
FontWeight="500"
|
||||
Margin="15,0,0,0"
|
||||
HorizontalOptions="StartAndExpand"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button-muted, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.PencilSquare}}"
|
||||
Command="{Binding EditUriCommand}"
|
||||
Margin="5,0,15,0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n EditURI}"
|
||||
AutomationId="EditUriButton" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
<Button
|
||||
Text="{u:I18n NewBlockedURI}"
|
||||
Command="{Binding AddUriCommand}"
|
||||
VerticalOptions="End"
|
||||
HeightRequest="40"
|
||||
Opacity="0.8"
|
||||
Margin="14,5,14,10"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n NewBlockedURI}"
|
||||
AutomationId="NewBlockedUriButton" />
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
||||
45
src/Core/Pages/Settings/BlockAutofillUrisPage.xaml.cs
Normal file
45
src/Core/Pages/Settings/BlockAutofillUrisPage.xaml.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Styles;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class BlockAutofillUrisPage : BaseContentPage, IThemeDirtablePage
|
||||
{
|
||||
private readonly BlockAutofillUrisPageViewModel _vm;
|
||||
|
||||
public BlockAutofillUrisPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_vm = BindingContext as BlockAutofillUrisPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
_vm.InitAsync().FireAndForget(_ => Navigation.PopAsync());
|
||||
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
public override async Task UpdateOnThemeChanged()
|
||||
{
|
||||
await base.UpdateOnThemeChanged();
|
||||
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
private void UpdatePlaceholder()
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
_emptyUrisPlaceholder.Source = ImageSource.FromFile(ThemeManager.UsingLightTheme ? "empty_uris_placeholder" : "empty_uris_placeholder_dark"));
|
||||
}
|
||||
}
|
||||
}
|
||||
188
src/Core/Pages/Settings/BlockAutofillUrisPageViewModel.cs
Normal file
188
src/Core/Pages/Settings/BlockAutofillUrisPageViewModel.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class BlockAutofillUrisPageViewModel : BaseViewModel
|
||||
{
|
||||
private const char URI_SEPARARTOR = ',';
|
||||
private const string URI_FORMAT = "https://domain.com";
|
||||
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
|
||||
public BlockAutofillUrisPageViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
|
||||
AddUriCommand = new AsyncCommand(AddUriAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
EditUriCommand = new AsyncCommand<BlockAutofillUriItemViewModel>(EditUriAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public ObservableRangeCollection<BlockAutofillUriItemViewModel> BlockedUris { get; set; } = new ObservableRangeCollection<BlockAutofillUriItemViewModel>();
|
||||
|
||||
public bool ShowList => BlockedUris.Any();
|
||||
|
||||
public ICommand AddUriCommand { get; }
|
||||
|
||||
public ICommand EditUriCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
if (blockedUrisList?.Any() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
BlockedUris.AddRange(blockedUrisList.OrderBy(uri => uri).Select(u => new BlockAutofillUriItemViewModel(u, EditUriCommand)).ToList());
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
}
|
||||
|
||||
private async Task AddUriAsync()
|
||||
{
|
||||
var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
|
||||
{
|
||||
Title = AppResources.NewUri,
|
||||
Subtitle = AppResources.EnterURI,
|
||||
ValueSubInfo = string.Format(AppResources.FormatXSeparateMultipleURIsWithAComma, URI_FORMAT),
|
||||
OkButtonText = AppResources.Save,
|
||||
ValidateText = text => ValidateUris(text, true)
|
||||
});
|
||||
if (response?.Text is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
foreach (var uri in response.Value.Text.Split(URI_SEPARARTOR).Where(s => !string.IsNullOrEmpty(s)))
|
||||
{
|
||||
var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
|
||||
BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
|
||||
}
|
||||
|
||||
BlockedUris = new ObservableRangeCollection<BlockAutofillUriItemViewModel>(BlockedUris.OrderBy(b => b.Uri));
|
||||
TriggerPropertyChanged(nameof(BlockedUris));
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
await UpdateAutofillBlacklistedUrisAsync();
|
||||
_deviceActionService.Toast(AppResources.URISaved);
|
||||
}
|
||||
|
||||
private async Task EditUriAsync(BlockAutofillUriItemViewModel uriItemViewModel)
|
||||
{
|
||||
var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
|
||||
{
|
||||
Title = AppResources.EditURI,
|
||||
Subtitle = AppResources.EnterURI,
|
||||
Text = uriItemViewModel.Uri,
|
||||
ValueSubInfo = string.Format(AppResources.FormatX, URI_FORMAT),
|
||||
OkButtonText = AppResources.Save,
|
||||
ThirdButtonText = AppResources.Remove,
|
||||
ValidateText = text => ValidateUris(text, false)
|
||||
});
|
||||
if (response is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Value.ExecuteThirdAction)
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
BlockedUris.Remove(uriItemViewModel);
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
await UpdateAutofillBlacklistedUrisAsync();
|
||||
_deviceActionService.Toast(AppResources.URIRemoved);
|
||||
return;
|
||||
}
|
||||
|
||||
var cleanedUri = response.Value.Text.Replace(Environment.NewLine, string.Empty).Trim();
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
BlockedUris.Remove(uriItemViewModel);
|
||||
BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
|
||||
BlockedUris = new ObservableRangeCollection<BlockAutofillUriItemViewModel>(BlockedUris.OrderBy(b => b.Uri));
|
||||
TriggerPropertyChanged(nameof(BlockedUris));
|
||||
TriggerPropertyChanged(nameof(ShowList));
|
||||
});
|
||||
await UpdateAutofillBlacklistedUrisAsync();
|
||||
_deviceActionService.Toast(AppResources.URISaved);
|
||||
}
|
||||
|
||||
private string ValidateUris(string uris, bool allowMultipleUris)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uris))
|
||||
{
|
||||
return string.Format(AppResources.FormatX, URI_FORMAT);
|
||||
}
|
||||
|
||||
if (!allowMultipleUris && uris.Contains(URI_SEPARARTOR))
|
||||
{
|
||||
return AppResources.CannotEditMultipleURIsAtOnce;
|
||||
}
|
||||
|
||||
foreach (var uri in uris.Split(URI_SEPARARTOR).Where(u => !string.IsNullOrWhiteSpace(u)))
|
||||
{
|
||||
var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
|
||||
if (!cleanedUri.StartsWith("http://") && !cleanedUri.StartsWith("https://") &&
|
||||
!cleanedUri.StartsWith(Constants.AndroidAppProtocol))
|
||||
{
|
||||
return AppResources.InvalidFormatUseHttpsHttpOrAndroidApp;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(cleanedUri, UriKind.Absolute, out var _))
|
||||
{
|
||||
return AppResources.InvalidURI;
|
||||
}
|
||||
|
||||
if (BlockedUris.Any(uriItem => uriItem.Uri == cleanedUri))
|
||||
{
|
||||
return string.Format(AppResources.TheURIXIsAlreadyBlocked, cleanedUri);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task UpdateAutofillBlacklistedUrisAsync()
|
||||
{
|
||||
await _stateService.SetAutofillBlacklistedUrisAsync(BlockedUris.Any() ? BlockedUris.Select(bu => bu.Uri).ToList() : null);
|
||||
}
|
||||
}
|
||||
|
||||
public class BlockAutofillUriItemViewModel : ExtendedViewModel
|
||||
{
|
||||
public BlockAutofillUriItemViewModel(string uri, ICommand editUriCommand)
|
||||
{
|
||||
Uri = uri;
|
||||
EditUriCommand = new Command(() => editUriCommand.Execute(this));
|
||||
}
|
||||
|
||||
public string Uri { get; }
|
||||
|
||||
public ICommand EditUriCommand { get; }
|
||||
}
|
||||
}
|
||||
142
src/Core/Pages/Settings/ExportVaultPage.xaml
Normal file
142
src/Core/Pages/Settings/ExportVaultPage.xaml
Normal file
@@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.ExportVaultPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:ExportVaultPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:ExportVaultPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:UpperCaseConverter x:Key="toUpper" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout
|
||||
StyleClass="box"
|
||||
Spacing="20">
|
||||
<Frame
|
||||
IsVisible="{Binding DisablePrivateVaultPolicyEnabled}"
|
||||
Padding="10"
|
||||
Margin="0, 12, 0, 0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{u:I18n DisablePersonalVaultExportPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="DisablePrivateVaultPolicyLabel" />
|
||||
</Frame>
|
||||
<Grid
|
||||
RowSpacing="10"
|
||||
RowDefinitions="70, Auto, Auto">
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Grid.Row="0">
|
||||
<Label
|
||||
Text="{u:I18n FileFormat}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_fileFormatPicker"
|
||||
ItemsSource="{Binding FileFormatOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding FileFormatSelectedIndex}"
|
||||
SelectedIndexChanged="FileFormat_Changed"
|
||||
StyleClass="box-value"
|
||||
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="FileFormatPicker" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Grid.Row="1"
|
||||
IsVisible="{Binding UseOTPVerification}">
|
||||
<Label
|
||||
Text="{u:I18n SendVerificationCodeToEmail}"
|
||||
StyleClass="box-label"
|
||||
LineBreakMode="WordWrap"
|
||||
Margin="0,0,0,10" />
|
||||
<Button x:Name="_requestOTP"
|
||||
Text="{u:I18n SendCode}"
|
||||
Clicked="RequestOTP_Clicked"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="End"
|
||||
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
|
||||
Margin="0,0,0,10"
|
||||
AutomationId="SendTOTPCodeButton" />
|
||||
</StackLayout>
|
||||
<Grid
|
||||
StyleClass="box-row"
|
||||
Grid.Row="2"
|
||||
RowDefinitions="Auto,Auto,Auto"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="{Binding SecretName}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.ColumnSpan="2" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_secret"
|
||||
Text="{Binding Secret}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding ExportVaultCommand}"
|
||||
AutomationId="MasterPasswordEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
IsVisible="{Binding UseOTPVerification, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="TogglePasswordVisibilityButton" />
|
||||
<Label
|
||||
Text="{u:I18n ConfirmYourIdentity}"
|
||||
StyleClass="box-footer-label"
|
||||
LineBreakMode="WordWrap"
|
||||
Grid.Row="2"
|
||||
Margin="0,10,0,0"
|
||||
IsVisible="{Binding UseOTPVerification}" />
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
<StackLayout
|
||||
StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n ExportVaultMasterPasswordDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch"
|
||||
IsVisible="{Binding UseOTPVerification, Converter={StaticResource inverseBool}}"/>
|
||||
<Button
|
||||
Text="{u:I18n ExportVault}"
|
||||
Clicked="ExportVault_Clicked"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="End"
|
||||
IsEnabled="{Binding DisablePrivateVaultPolicyEnabled, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="ExportVaultButton" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
84
src/Core/Pages/Settings/ExportVaultPage.xaml.cs
Normal file
84
src/Core/Pages/Settings/ExportVaultPage.xaml.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class ExportVaultPage : BaseContentPage
|
||||
{
|
||||
private readonly ExportVaultPageViewModel _vm;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
|
||||
public ExportVaultPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_vm = BindingContext as ExportVaultPageViewModel;
|
||||
_vm.Page = this;
|
||||
_fileFormatPicker.ItemDisplayBinding = new Binding("Value");
|
||||
SecretEntry = _secret;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
_broadcasterService.Subscribe(nameof(ExportVaultPage), (message) =>
|
||||
{
|
||||
if (message.Command == "selectSaveFileResult")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
var data = message.Data as Tuple<string, string>;
|
||||
if (data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_vm.SaveFileSelected(data.Item1, data.Item2);
|
||||
});
|
||||
}
|
||||
});
|
||||
RequestFocus(_secret);
|
||||
}
|
||||
|
||||
protected async override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_broadcasterService.Unsubscribe(nameof(ExportVaultPage));
|
||||
}
|
||||
|
||||
public Entry SecretEntry { get; set; }
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExportVault_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.ExportVaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void RequestOTP_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.RequestOTP();
|
||||
_requestOTP.IsEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
void FileFormat_Changed(object sender, EventArgs e)
|
||||
{
|
||||
_vm?.UpdateWarning();
|
||||
}
|
||||
}
|
||||
}
|
||||
261
src/Core/Pages/Settings/ExportVaultPageViewModel.cs
Normal file
261
src/Core/Pages/Settings/ExportVaultPageViewModel.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class ExportVaultPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IExportService _exportService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private int _fileFormatSelectedIndex;
|
||||
private string _exportWarningMessage;
|
||||
private bool _showPassword;
|
||||
private string _secret;
|
||||
private byte[] _exportResult;
|
||||
private string _defaultFilename;
|
||||
private bool _initialized = false;
|
||||
private bool _useOTPVerification = false;
|
||||
private string _secretName;
|
||||
private string _instructionText;
|
||||
|
||||
public ExportVaultPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_exportService = ServiceContainer.Resolve<IExportService>("exportService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
PageTitle = AppResources.ExportVault;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ExportVaultCommand = new Command(async () => await ExportVaultAsync());
|
||||
|
||||
FileFormatOptions = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("json", ".json"),
|
||||
new KeyValuePair<string, string>("csv", ".csv"),
|
||||
new KeyValuePair<string, string>("encrypted_json", ".json (Encrypted)")
|
||||
};
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_initialized = true;
|
||||
FileFormatSelectedIndex = FileFormatOptions.FindIndex(k => k.Key == "json");
|
||||
DisablePrivateVaultPolicyEnabled = await _policyService.PolicyAppliesToUser(PolicyType.DisablePersonalVaultExport);
|
||||
UseOTPVerification = !await _userVerificationService.HasMasterPasswordAsync(true);
|
||||
|
||||
if (UseOTPVerification)
|
||||
{
|
||||
InstructionText = _i18nService.T("ExportVaultOTPDescription");
|
||||
SecretName = _i18nService.T("VerificationCode");
|
||||
}
|
||||
else
|
||||
{
|
||||
InstructionText = _i18nService.T("ExportVaultMasterPasswordDescription");
|
||||
SecretName = _i18nService.T("MasterPassword");
|
||||
}
|
||||
|
||||
UpdateWarning();
|
||||
}
|
||||
|
||||
public List<KeyValuePair<string, string>> FileFormatOptions { get; set; }
|
||||
private bool _disabledPrivateVaultPolicyEnabled = false;
|
||||
|
||||
public bool DisablePrivateVaultPolicyEnabled
|
||||
{
|
||||
get => _disabledPrivateVaultPolicyEnabled;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _disabledPrivateVaultPolicyEnabled, value);
|
||||
}
|
||||
}
|
||||
|
||||
public int FileFormatSelectedIndex
|
||||
{
|
||||
get => _fileFormatSelectedIndex;
|
||||
set { SetProperty(ref _fileFormatSelectedIndex, value); }
|
||||
}
|
||||
|
||||
public string ExportWarningMessage
|
||||
{
|
||||
get => _exportWarningMessage;
|
||||
set { SetProperty(ref _exportWarningMessage, value); }
|
||||
}
|
||||
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText),
|
||||
});
|
||||
}
|
||||
|
||||
public bool UseOTPVerification
|
||||
{
|
||||
get => _useOTPVerification;
|
||||
set => SetProperty(ref _useOTPVerification, value);
|
||||
}
|
||||
|
||||
public string Secret
|
||||
{
|
||||
get => _secret;
|
||||
set => SetProperty(ref _secret, value);
|
||||
}
|
||||
|
||||
public string SecretName
|
||||
{
|
||||
get => _secretName;
|
||||
set => SetProperty(ref _secretName, value);
|
||||
}
|
||||
|
||||
public string InstructionText
|
||||
{
|
||||
get => _instructionText;
|
||||
set => SetProperty(ref _instructionText, value);
|
||||
}
|
||||
|
||||
public Command TogglePasswordCommand { get; }
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as ExportVaultPage).SecretEntry.Focus();
|
||||
}
|
||||
|
||||
public Command ExportVaultCommand { get; }
|
||||
|
||||
public async Task ExportVaultAsync()
|
||||
{
|
||||
bool userConfirmedExport = await _platformUtilsService.ShowDialogAsync(ExportWarningMessage,
|
||||
_i18nService.T("ExportVaultConfirmationTitle"), _i18nService.T("ExportVault"), _i18nService.T("Cancel"));
|
||||
|
||||
if (!userConfirmedExport)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var verificationType = await _userVerificationService.HasMasterPasswordAsync(true)
|
||||
? VerificationType.MasterPassword
|
||||
: VerificationType.OTP;
|
||||
if (!await _userVerificationService.VerifyUser(Secret, verificationType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Secret = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var data = await _exportService.GetExport(FileFormatOptions[FileFormatSelectedIndex].Key);
|
||||
var fileFormat = FileFormatOptions[FileFormatSelectedIndex].Key;
|
||||
fileFormat = fileFormat == "encrypted_json" ? "json" : fileFormat;
|
||||
|
||||
_defaultFilename = _exportService.GetFileName(null, fileFormat);
|
||||
_exportResult = Encoding.UTF8.GetBytes(data);
|
||||
|
||||
if (!_fileService.SaveFile(_exportResult, null, _defaultFilename, null))
|
||||
{
|
||||
ClearResult();
|
||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ClearResult();
|
||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RequestOTP()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Sending);
|
||||
await _apiService.PostAccountRequestOTP();
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.CodeSent);
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async void SaveFileSelected(string contentUri, string filename)
|
||||
{
|
||||
if (_fileService.SaveFile(_exportResult, null, filename ?? _defaultFilename, contentUri))
|
||||
{
|
||||
ClearResult();
|
||||
_platformUtilsService.ShowToast("success", null, _i18nService.T("ExportVaultSuccess"));
|
||||
return;
|
||||
}
|
||||
|
||||
ClearResult();
|
||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
|
||||
}
|
||||
|
||||
public void UpdateWarning()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (FileFormatOptions[FileFormatSelectedIndex].Key)
|
||||
{
|
||||
case "encrypted_json":
|
||||
ExportWarningMessage = _i18nService.T("EncExportKeyWarning") +
|
||||
"\n\n" +
|
||||
_i18nService.T("EncExportAccountWarning");
|
||||
break;
|
||||
default:
|
||||
ExportWarningMessage = _i18nService.T("ExportVaultWarning");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearResult()
|
||||
{
|
||||
_defaultFilename = null;
|
||||
_exportResult = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/Core/Pages/Settings/ExtensionPage.xaml
Normal file
95
src/Core/Pages/Settings/ExtensionPage.xaml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.ExtensionPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:ExtensionPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:ExtensionPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Padding="0" Spacing="0" VerticalOptions="FillAndExpand">
|
||||
<StackLayout Spacing="20"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand"
|
||||
IsVisible="{Binding NotStarted}">
|
||||
<Label Text="{u:I18n ExtensionInstantAccess}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n ExtensionTurnOn}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="ext-more.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, -10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
<Button Text="{u:I18n ExtensionEnable}"
|
||||
Clicked="Show_Clicked"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Fill" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="20"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand"
|
||||
IsVisible="{Binding StartedAndNotActivated}">
|
||||
<Label Text="{u:I18n ExtensionAlmostDone}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n ExtensionTapIcon}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="ext-act.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, -10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
<Button Text="{u:I18n ExtensionEnable}"
|
||||
Clicked="Show_Clicked"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Fill" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="20"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand"
|
||||
IsVisible="{Binding StartedAndActivated}">
|
||||
<Label Text="{u:I18n ExtensionReady}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n ExtensionInSafari}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="ext-use.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, -10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
<Button Text="{u:I18n ExntesionReenable}"
|
||||
Clicked="Show_Clicked"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Fill" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
38
src/Core/Pages/Settings/ExtensionPage.xaml.cs
Normal file
38
src/Core/Pages/Settings/ExtensionPage.xaml.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class ExtensionPage : BaseContentPage
|
||||
{
|
||||
private readonly ExtensionPageViewModel _vm;
|
||||
|
||||
public ExtensionPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as ExtensionPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
|
||||
private void Show_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.ShowExtension();
|
||||
}
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/Core/Pages/Settings/ExtensionPageViewModel.cs
Normal file
66
src/Core/Pages/Settings/ExtensionPageViewModel.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class ExtensionPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IMessagingService _messagingService;
|
||||
|
||||
private bool _started;
|
||||
private bool _activated;
|
||||
|
||||
public ExtensionPageViewModel()
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
PageTitle = AppResources.AppExtension;
|
||||
}
|
||||
|
||||
public bool Started
|
||||
{
|
||||
get => _started;
|
||||
set => SetProperty(ref _started, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(NotStarted),
|
||||
nameof(StartedAndNotActivated),
|
||||
nameof(StartedAndActivated)
|
||||
});
|
||||
}
|
||||
|
||||
public bool Activated
|
||||
{
|
||||
get => _activated;
|
||||
set => SetProperty(ref _activated, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(StartedAndNotActivated),
|
||||
nameof(StartedAndActivated)
|
||||
});
|
||||
}
|
||||
|
||||
public bool NotStarted => !Started;
|
||||
public bool StartedAndNotActivated => Started && !Activated;
|
||||
public bool StartedAndActivated => Started && Activated;
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
Started = false;
|
||||
Activated = false;
|
||||
}
|
||||
|
||||
public void ShowExtension()
|
||||
{
|
||||
_messagingService.Send("showAppExtension", this);
|
||||
}
|
||||
|
||||
public void EnabledExtension(bool enabled)
|
||||
{
|
||||
Started = true;
|
||||
if (!Activated && enabled)
|
||||
{
|
||||
Activated = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/Core/Pages/Settings/FolderAddEditPage.xaml
Normal file
58
src/Core/Pages/Settings/FolderAddEditPage.xaml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.FolderAddEditPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:FolderAddEditPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:FolderAddEditPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}"
|
||||
Clicked="Close_Clicked"
|
||||
Order="Primary"
|
||||
Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Save}"
|
||||
Clicked="Save_Clicked"
|
||||
Order="Primary" />
|
||||
<ToolbarItem Text="{u:I18n Delete}"
|
||||
Clicked="Delete_Clicked"
|
||||
Order="Secondary"
|
||||
IsDestructive="True"
|
||||
x:Name="_deleteItem" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<ToolbarItem IconImageSource="more_vert.png" Clicked="More_Clicked" Order="Primary" x:Name="_moreItem"
|
||||
x:Key="moreItem"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Options}" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ScrollView x:Name="_scrollView">
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
Text="{u:I18n Name}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
Text="{Binding Folder.Name}"
|
||||
StyleClass="box-value"
|
||||
x:Name="_nameEntry"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="FolderNameEntry" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
90
src/Core/Pages/Settings/FolderAddEditPage.xaml.cs
Normal file
90
src/Core/Pages/Settings/FolderAddEditPage.xaml.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class FolderAddEditPage : BaseContentPage
|
||||
{
|
||||
private FolderAddEditPageViewModel _vm;
|
||||
|
||||
public FolderAddEditPage(
|
||||
string folderId = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as FolderAddEditPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.FolderId = folderId;
|
||||
_vm.Init();
|
||||
SetActivityIndicator();
|
||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
if (!_vm.EditMode || Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ToolbarItems.Remove(_deleteItem);
|
||||
}
|
||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
if (_vm.EditMode && Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await LoadOnAppearedAsync(_scrollView, true, async () =>
|
||||
{
|
||||
await _vm.LoadAsync();
|
||||
if (!_vm.EditMode)
|
||||
{
|
||||
RequestFocus(_nameEntry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async void Save_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Delete_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.DeleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var options = new List<string> { };
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
_vm.EditMode ? AppResources.Delete : null, options.ToArray());
|
||||
if (selection == AppResources.Delete)
|
||||
{
|
||||
await _vm.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
145
src/Core/Pages/Settings/FolderAddEditPageViewModel.cs
Normal file
145
src/Core/Pages/Settings/FolderAddEditPageViewModel.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class FolderAddEditPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private FolderView _folder;
|
||||
|
||||
public FolderAddEditPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
}
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public string FolderId { get; set; }
|
||||
public FolderView Folder
|
||||
{
|
||||
get => _folder;
|
||||
set => SetProperty(ref _folder, value);
|
||||
}
|
||||
public bool EditMode => !string.IsNullOrWhiteSpace(FolderId);
|
||||
|
||||
public void Init()
|
||||
{
|
||||
PageTitle = EditMode ? AppResources.EditFolder : AppResources.AddFolder;
|
||||
}
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
if (Folder == null)
|
||||
{
|
||||
if (EditMode)
|
||||
{
|
||||
var folder = await _folderService.GetAsync(FolderId);
|
||||
if (folder != null)
|
||||
{
|
||||
Folder = await folder.DecryptAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Folder = new FolderView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SubmitAsync()
|
||||
{
|
||||
if (Folder == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Folder.Name))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.Name),
|
||||
AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
|
||||
var folder = await _folderService.EncryptAsync(Folder);
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||
await _folderService.SaveWithServerAsync(folder);
|
||||
Folder.Id = folder.Id;
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null,
|
||||
EditMode ? AppResources.FolderUpdated : AppResources.FolderCreated);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
return true;
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync()
|
||||
{
|
||||
if (Folder == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return false;
|
||||
}
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete,
|
||||
null, AppResources.Yes, AppResources.No);
|
||||
if (!confirmed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Deleting);
|
||||
await _folderService.DeleteWithServerAsync(Folder.Id);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.FolderDeleted);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
return true;
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/Core/Pages/Settings/FoldersPage.xaml
Normal file
86
src/Core/Pages/Settings/FoldersPage.xaml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.FoldersPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:views="clr-namespace:Bit.Core.Models.View"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
x:DataType="pages:FoldersPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:FoldersPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<ToolbarItem x:Name="_closeItem" x:Key="closeItem" Text="{u:I18n Close}"
|
||||
Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem x:Name="_addItem" x:Key="addItem" IconImageSource="plus.png"
|
||||
Clicked="AddButton_Clicked" Order="Primary"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n AddItem}" />
|
||||
<StackLayout x:Name="_mainLayout" x:Key="mainLayout">
|
||||
<Label IsVisible="{Binding ShowNoData}"
|
||||
Text="{u:I18n NoFoldersToList}"
|
||||
Margin="20, 0"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoFoldersLabel"></Label>
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding Folders}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Folders Page">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:FolderView">
|
||||
<controls:ExtendedStackLayout
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10"
|
||||
AutomationId="FolderCell">
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Name, Mode=OneWay}"
|
||||
AutomationId="FolderName" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<AbsoluteLayout
|
||||
x:Name="_absLayout"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HorizontalOptions="FillAndExpand">
|
||||
<ContentView
|
||||
x:Name="_mainContent"
|
||||
AbsoluteLayout.LayoutFlags="All"
|
||||
AbsoluteLayout.LayoutBounds="0, 0, 1, 1">
|
||||
</ContentView>
|
||||
<Button
|
||||
x:Name="_fab"
|
||||
ImageSource="plus.png"
|
||||
Clicked="AddButton_Clicked"
|
||||
Style="{StaticResource btn-fab}"
|
||||
AbsoluteLayout.LayoutFlags="PositionProportional"
|
||||
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n AddFolder}">
|
||||
<Button.Effects>
|
||||
<effects:FabShadowEffect />
|
||||
</Button.Effects>
|
||||
</Button>
|
||||
</AbsoluteLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
71
src/Core/Pages/Settings/FoldersPage.xaml.cs
Normal file
71
src/Core/Pages/Settings/FoldersPage.xaml.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Models.View;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class FoldersPage : BaseContentPage
|
||||
{
|
||||
private FoldersPageViewModel _vm;
|
||||
|
||||
public FoldersPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
SetActivityIndicator(_mainContent);
|
||||
_vm = BindingContext as FoldersPageViewModel;
|
||||
_vm.Page = this;
|
||||
|
||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
_absLayout.Children.Remove(_fab);
|
||||
ToolbarItems.Add(_closeItem);
|
||||
ToolbarItems.Add(_addItem);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await LoadOnAppearedAsync(_mainLayout, true, async () =>
|
||||
{
|
||||
await _vm.InitAsync();
|
||||
}, _mainContent);
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is FolderView folder))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var page = new FolderAddEditPage(folder.Id);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async void AddButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var page = new FolderAddEditPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/Core/Pages/Settings/FoldersPageViewModel.cs
Normal file
45
src/Core/Pages/Settings/FoldersPageViewModel.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class FoldersPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IFolderService _folderService;
|
||||
|
||||
private bool _showNoData;
|
||||
|
||||
public FoldersPageViewModel()
|
||||
{
|
||||
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
||||
|
||||
PageTitle = AppResources.Folders;
|
||||
Folders = new ExtendedObservableCollection<FolderView>();
|
||||
}
|
||||
|
||||
public ExtendedObservableCollection<FolderView> Folders { get; set; }
|
||||
|
||||
public bool ShowNoData
|
||||
{
|
||||
get => _showNoData;
|
||||
set => SetProperty(ref _showNoData, value);
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var folders = await _folderService.GetAllDecryptedAsync();
|
||||
// Remove "No Folder"
|
||||
if (folders?.Any() ?? false)
|
||||
{
|
||||
folders = folders.GetRange(0, folders.Count - 1);
|
||||
}
|
||||
Folders.ResetWithRange(folders ?? new List<FolderView>());
|
||||
ShowNoData = Folders.Count == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/Core/Pages/Settings/LoginPasswordlessRequestsListPage.xaml
Normal file
133
src/Core/Pages/Settings/LoginPasswordlessRequestsListPage.xaml
Normal file
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:xct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
||||
x:Class="Bit.App.Pages.LoginPasswordlessRequestsListPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:LoginPasswordlessRequestsListViewModel"
|
||||
xmlns:models="clr-namespace:Bit.Core.Models.Response"
|
||||
xmlns:core="clr-namespace:Bit.Core"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginPasswordlessRequestsListViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:DateTimeConverter x:Key="dateTime" />
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<controls:SelectionChangedEventArgsConverter x:Key="SelectionChangedEventArgsConverter" />
|
||||
<DataTemplate
|
||||
x:Key="loginRequestTemplate"
|
||||
x:DataType="models:PasswordlessLoginResponse">
|
||||
<Grid
|
||||
Padding="10, 0"
|
||||
RowSpacing="0"
|
||||
RowDefinitions="*, Auto, *, 10"
|
||||
ColumnDefinitions="*, *"
|
||||
AutomationId="LoginRequestCell">
|
||||
<Label
|
||||
Text="{u:I18n FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
Padding="0, 10, 0 ,0"
|
||||
FontAttributes="Bold"/>
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding FingerprintPhrase}"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
FontSize="Small"
|
||||
Padding="0, 5, 0, 10"
|
||||
VerticalTextAlignment="Center"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"
|
||||
AutomationId="FingerprintPhraseLabel" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
HorizontalOptions="Start"
|
||||
HorizontalTextAlignment="Start"
|
||||
Text="{Binding RequestDeviceType}"
|
||||
StyleClass="list-header-sub"
|
||||
AutomationId="RequestDeviceLabel" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
HorizontalOptions="End"
|
||||
HorizontalTextAlignment="End"
|
||||
Text="{Binding CreationDate, Converter={StaticResource dateTime}}"
|
||||
StyleClass="list-header-sub"
|
||||
AutomationId="RequestDateLabel" />
|
||||
<BoxView
|
||||
StyleClass="list-section-separator-top, list-section-separator-top-platform"
|
||||
VerticalOptions="End"
|
||||
Grid.Row="3"
|
||||
Grid.ColumnSpan="2"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<StackLayout
|
||||
x:Key="mainLayout"
|
||||
x:Name="_mainLayout"
|
||||
Padding="0, 10">
|
||||
<RefreshView
|
||||
IsRefreshing="{Binding IsRefreshing}"
|
||||
Command="{Binding RefreshCommand}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
BackgroundColor="{DynamicResource BackgroundColor}">
|
||||
<StackLayout>
|
||||
<Image
|
||||
x:Name="_emptyPlaceholder"
|
||||
Source="empty_login_requests"
|
||||
HorizontalOptions="Center"
|
||||
WidthRequest="160"
|
||||
HeightRequest="160"
|
||||
Margin="0,70,0,0"
|
||||
IsVisible="{Binding HasLoginRequests, Converter={StaticResource inverseBool}}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n NoPendingRequests}" />
|
||||
<controls:CustomLabel
|
||||
StyleClass="box-label-regular"
|
||||
Text="{u:I18n NoPendingRequests}"
|
||||
FontAttributes="{OnPlatform iOS=Bold}"
|
||||
FontWeight="500"
|
||||
HorizontalTextAlignment="Center"
|
||||
Margin="14,10,14,0"/>
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding LoginRequests}"
|
||||
ItemTemplate="{StaticResource loginRequestTemplate}"
|
||||
SelectionMode="Single"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
ExtraDataForLogging="Login requests page" >
|
||||
<controls:ExtendedCollectionView.Behaviors>
|
||||
<xct:EventToCommandBehavior
|
||||
EventName="SelectionChanged"
|
||||
Command="{Binding AnswerRequestCommand}"
|
||||
EventArgsConverter="{StaticResource SelectionChangedEventArgsConverter}" />
|
||||
</controls:ExtendedCollectionView.Behaviors>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
</RefreshView>
|
||||
<controls:IconLabelButton
|
||||
VerticalOptions="End"
|
||||
Margin="10,0"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||
Label="{u:I18n DeclineAllRequests}"
|
||||
ButtonCommand="{Binding DeclineAllRequestsCommand}"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
AutomationId="DeleteAllRequestsButton" />
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentView
|
||||
x:Name="_mainContent">
|
||||
</ContentView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginPasswordlessRequestsListPage : BaseContentPage
|
||||
{
|
||||
private LoginPasswordlessRequestsListViewModel _vm;
|
||||
|
||||
public LoginPasswordlessRequestsListPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
SetActivityIndicator(_mainContent);
|
||||
_vm = BindingContext as LoginPasswordlessRequestsListViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await LoadOnAppearedAsync(_mainLayout, false, _vm.RefreshAsync, _mainContent);
|
||||
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task UpdateOnThemeChanged()
|
||||
{
|
||||
await base.UpdateOnThemeChanged();
|
||||
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
private void UpdatePlaceholder()
|
||||
{
|
||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
_emptyPlaceholder.Source = ImageSource.FromFile(ThemeManager.UsingLightTheme ? "empty_login_requests" : "empty_login_requests_dark"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginPasswordlessRequestsListViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private bool _isRefreshing;
|
||||
|
||||
public LoginPasswordlessRequestsListViewModel()
|
||||
{
|
||||
_authService = ServiceContainer.Resolve<IAuthService>();
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
|
||||
PageTitle = AppResources.PendingLogInRequests;
|
||||
LoginRequests = new ObservableRangeCollection<PasswordlessLoginResponse>();
|
||||
|
||||
AnswerRequestCommand = new AsyncCommand<PasswordlessLoginResponse>(PasswordlessLoginAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
DeclineAllRequestsCommand = new AsyncCommand(DeclineAllRequestsAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
RefreshCommand = new Command(async () => await RefreshAsync());
|
||||
}
|
||||
|
||||
public ICommand RefreshCommand { get; }
|
||||
|
||||
public AsyncCommand<PasswordlessLoginResponse> AnswerRequestCommand { get; }
|
||||
|
||||
public AsyncCommand DeclineAllRequestsCommand { get; }
|
||||
|
||||
public ObservableRangeCollection<PasswordlessLoginResponse> LoginRequests { get; }
|
||||
|
||||
public bool IsRefreshing
|
||||
{
|
||||
get => _isRefreshing;
|
||||
set => SetProperty(ref _isRefreshing, value);
|
||||
}
|
||||
|
||||
public bool HasLoginRequests => LoginRequests.Any();
|
||||
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsRefreshing = true;
|
||||
LoginRequests.ReplaceRange(await _authService.GetActivePasswordlessLoginRequestsAsync());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRefreshing = false;
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(HasLoginRequests)));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PasswordlessLoginAsync(PasswordlessLoginResponse request)
|
||||
{
|
||||
if (request.IsExpired)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(request.Id);
|
||||
if (loginRequestData.IsAnswered)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.ThisRequestIsNoLongerValid);
|
||||
return;
|
||||
}
|
||||
|
||||
var page = new LoginPasswordlessPage(new LoginPasswordlessDetails()
|
||||
{
|
||||
PubKey = loginRequestData.PublicKey,
|
||||
Id = loginRequestData.Id,
|
||||
IpAddress = loginRequestData.RequestIpAddress,
|
||||
Email = await _stateService.GetEmailAsync(),
|
||||
FingerprintPhrase = loginRequestData.FingerprintPhrase,
|
||||
RequestDate = loginRequestData.CreationDate,
|
||||
DeviceType = loginRequestData.RequestDeviceType,
|
||||
Origin = loginRequestData.Origin
|
||||
});
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
|
||||
}
|
||||
|
||||
private async Task DeclineAllRequestsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await _platformUtilsService.ShowDialogAsync(AppResources.AreYouSureYouWantToDeclineAllPendingLogInRequests, null, AppResources.Yes, AppResources.No))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
var taskList = new List<Task>();
|
||||
foreach (var request in LoginRequests)
|
||||
{
|
||||
taskList.Add(_authService.PasswordlessLoginAsync(request.Id, request.PublicKey, false));
|
||||
}
|
||||
await Task.WhenAll(taskList);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await RefreshAsync();
|
||||
_platformUtilsService.ShowToast("info", null, AppResources.RequestsDeclined);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
RefreshAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/Core/Pages/Settings/OtherSettingsPage.xaml
Normal file
70
src/Core/Pages/Settings/OtherSettingsPage.xaml
Normal file
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:OtherSettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:Class="Bit.App.Pages.OtherSettingsPage"
|
||||
Title="{u:I18n Other}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:OtherSettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<u:InverseBoolConverter x:Key="inverseBoolConverter" />
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="5">
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n EnableSyncOnRefresh}"
|
||||
IsToggled="{Binding EnableSyncOnRefresh}"
|
||||
Subtitle="{u:I18n EnableSyncOnRefreshDescription}"
|
||||
AutomationId="SubmitCrashLogsSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<StackLayout StyleClass="box" Margin="0,12,0,0">
|
||||
<Button
|
||||
Text="{u:I18n SyncNow}"
|
||||
Command="{Binding SyncCommand}"></Button>
|
||||
<Label
|
||||
Text="{Binding LastSyncDisplay}"
|
||||
StyleClass="text-muted, text-sm"
|
||||
HorizontalTextAlignment="Start"
|
||||
Margin="0,10" />
|
||||
</StackLayout>
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n ClearClipboard}"
|
||||
Subtitle="{u:I18n ClearClipboardDescription}"
|
||||
DisplayValue="{Binding ClearClipboardPickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding ClearClipboardPickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="ClearClipboardChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n AllowScreenCapture}"
|
||||
IsToggled="{Binding IsScreenCaptureAllowed}"
|
||||
IsEnabled="{Binding CanToggleeScreenCaptureAllowed}"
|
||||
AutomationId="SubmitCrashLogsSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n ConnectToWatch}"
|
||||
IsToggled="{Binding ShouldConnectToWatch}"
|
||||
IsEnabled="{Binding CanToggleShouldConnectToWatch}"
|
||||
IsVisible="{OnPlatform iOS=True, Android=False}"
|
||||
AutomationId="ConnectToWatchSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
44
src/Core/Pages/Settings/OtherSettingsPage.xaml.cs
Normal file
44
src/Core/Pages/Settings/OtherSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class OtherSettingsPage : BaseContentPage
|
||||
{
|
||||
private OtherSettingsPageViewModel _vm;
|
||||
|
||||
public OtherSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as OtherSettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
_vm.SubscribeEvents();
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
|
||||
|
||||
Navigation.PopAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_vm.UnsubscribeEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
248
src/Core/Pages/Settings/OtherSettingsPageViewModel.cs
Normal file
248
src/Core/Pages/Settings/OtherSettingsPageViewModel.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class OtherSettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private const int CLEAR_CLIPBOARD_NEVER_OPTION = -1;
|
||||
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly ILocalizeService _localizeService;
|
||||
private readonly IWatchDeviceService _watchDeviceService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private string _lastSyncDisplay = "--";
|
||||
private bool _inited;
|
||||
private bool _syncOnRefresh;
|
||||
private bool _isScreenCaptureAllowed;
|
||||
private bool _shouldConnectToWatch;
|
||||
|
||||
public OtherSettingsPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||
_localizeService = ServiceContainer.Resolve<ILocalizeService>();
|
||||
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
|
||||
SyncCommand = CreateDefaultAsyncCommnad(SyncAsync, () => _inited);
|
||||
ToggleIsScreenCaptureAllowedCommand = CreateDefaultAsyncCommnad(ToggleIsScreenCaptureAllowedAsync, () => _inited);
|
||||
ToggleShouldConnectToWatchCommand = CreateDefaultAsyncCommnad(ToggleShouldConnectToWatchAsync, () => _inited);
|
||||
|
||||
ClearClipboardPickerViewModel = new PickerViewModel<int>(
|
||||
_deviceActionService,
|
||||
_logger,
|
||||
OnClearClipboardChangingAsync,
|
||||
AppResources.ClearClipboard,
|
||||
() => _inited,
|
||||
ex => HandleException(ex));
|
||||
}
|
||||
|
||||
public bool EnableSyncOnRefresh
|
||||
{
|
||||
get => _syncOnRefresh;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _syncOnRefresh, value))
|
||||
{
|
||||
UpdateSyncOnRefreshAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string LastSyncDisplay
|
||||
{
|
||||
get => $"{AppResources.LastSync} {_lastSyncDisplay}";
|
||||
set => SetProperty(ref _lastSyncDisplay, value);
|
||||
}
|
||||
|
||||
public PickerViewModel<int> ClearClipboardPickerViewModel { get; }
|
||||
|
||||
public bool IsScreenCaptureAllowed
|
||||
{
|
||||
get => _isScreenCaptureAllowed;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isScreenCaptureAllowed, value))
|
||||
{
|
||||
((ICommand)ToggleIsScreenCaptureAllowedCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanToggleeScreenCaptureAllowed => ToggleIsScreenCaptureAllowedCommand.CanExecute(null);
|
||||
|
||||
public bool ShouldConnectToWatch
|
||||
{
|
||||
get => _shouldConnectToWatch;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _shouldConnectToWatch, value))
|
||||
{
|
||||
((ICommand)ToggleShouldConnectToWatchCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanToggleShouldConnectToWatch => ToggleShouldConnectToWatchCommand.CanExecute(null);
|
||||
|
||||
public AsyncCommand SyncCommand { get; }
|
||||
public AsyncCommand ToggleIsScreenCaptureAllowedCommand { get; }
|
||||
public AsyncCommand ToggleShouldConnectToWatchCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await SetLastSyncAsync();
|
||||
|
||||
EnableSyncOnRefresh = await _stateService.GetSyncOnRefreshAsync();
|
||||
|
||||
await InitClearClipboardAsync();
|
||||
|
||||
_isScreenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
|
||||
_shouldConnectToWatch = await _stateService.GetShouldConnectToWatchAsync();
|
||||
|
||||
_inited = true;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(IsScreenCaptureAllowed));
|
||||
TriggerPropertyChanged(nameof(ShouldConnectToWatch));
|
||||
SyncCommand.RaiseCanExecuteChanged();
|
||||
ClearClipboardPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
ToggleIsScreenCaptureAllowedCommand.RaiseCanExecuteChanged();
|
||||
ToggleShouldConnectToWatchCommand.RaiseCanExecuteChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task InitClearClipboardAsync()
|
||||
{
|
||||
var clearClipboardOptions = new Dictionary<int, string>
|
||||
{
|
||||
[CLEAR_CLIPBOARD_NEVER_OPTION] = AppResources.Never,
|
||||
[10] = AppResources.TenSeconds,
|
||||
[20] = AppResources.TwentySeconds,
|
||||
[30] = AppResources.ThirtySeconds,
|
||||
[60] = AppResources.OneMinute
|
||||
};
|
||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
if (Device.RuntimePlatform != Device.iOS)
|
||||
{
|
||||
clearClipboardOptions.Add(120, AppResources.TwoMinutes);
|
||||
clearClipboardOptions.Add(300, AppResources.FiveMinutes);
|
||||
}
|
||||
|
||||
var clearClipboard = await _stateService.GetClearClipboardAsync() ?? CLEAR_CLIPBOARD_NEVER_OPTION;
|
||||
|
||||
ClearClipboardPickerViewModel.Init(clearClipboardOptions, clearClipboard, CLEAR_CLIPBOARD_NEVER_OPTION);
|
||||
}
|
||||
|
||||
public async Task UpdateSyncOnRefreshAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
await _stateService.SetSyncOnRefreshAsync(_syncOnRefresh);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetLastSyncAsync()
|
||||
{
|
||||
var last = await _syncService.GetLastSyncAsync();
|
||||
if (last is null)
|
||||
{
|
||||
LastSyncDisplay = AppResources.Never;
|
||||
return;
|
||||
}
|
||||
|
||||
var localDate = last.Value.ToLocalTime();
|
||||
LastSyncDisplay = string.Format("{0} {1}",
|
||||
_localizeService.GetLocaleShortDate(localDate),
|
||||
_localizeService.GetLocaleShortTime(localDate));
|
||||
}
|
||||
|
||||
public async Task SyncAsync()
|
||||
{
|
||||
if (!await HasConnectivityAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
|
||||
await _syncService.SyncPasswordlessLoginRequestsAsync();
|
||||
var success = await _syncService.FullSyncAsync(true);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (!success)
|
||||
{
|
||||
await Page.DisplayAlert(null, AppResources.SyncingFailed, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
await SetLastSyncAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete);
|
||||
}
|
||||
|
||||
private async Task<bool> OnClearClipboardChangingAsync(int optionKey)
|
||||
{
|
||||
await _stateService.SetClearClipboardAsync(optionKey == CLEAR_CLIPBOARD_NEVER_OPTION ? (int?)null : optionKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ToggleIsScreenCaptureAllowedAsync()
|
||||
{
|
||||
if (IsScreenCaptureAllowed
|
||||
&&
|
||||
!await Page.DisplayAlert(AppResources.AllowScreenCapture, AppResources.AreYouSureYouWantToEnableScreenCapture, AppResources.Yes, AppResources.No))
|
||||
{
|
||||
_isScreenCaptureAllowed = !IsScreenCaptureAllowed;
|
||||
TriggerPropertyChanged(nameof(IsScreenCaptureAllowed));
|
||||
return;
|
||||
}
|
||||
|
||||
await _stateService.SetScreenCaptureAllowedAsync(IsScreenCaptureAllowed);
|
||||
await _deviceActionService.SetScreenCaptureAllowedAsync();
|
||||
}
|
||||
|
||||
private async Task ToggleShouldConnectToWatchAsync()
|
||||
{
|
||||
await _watchDeviceService.SetShouldConnectToWatchAsync(ShouldConnectToWatch);
|
||||
}
|
||||
|
||||
private void ToggleIsScreenCaptureAllowedCommand_CanExecuteChanged(object sender, EventArgs e)
|
||||
{
|
||||
TriggerPropertyChanged(nameof(CanToggleeScreenCaptureAllowed));
|
||||
}
|
||||
|
||||
private void ToggleShouldConnectToWatchCommand_CanExecuteChanged(object sender, EventArgs e)
|
||||
{
|
||||
TriggerPropertyChanged(nameof(CanToggleShouldConnectToWatch));
|
||||
}
|
||||
|
||||
internal void SubscribeEvents()
|
||||
{
|
||||
ToggleIsScreenCaptureAllowedCommand.CanExecuteChanged += ToggleIsScreenCaptureAllowedCommand_CanExecuteChanged;
|
||||
ToggleShouldConnectToWatchCommand.CanExecuteChanged += ToggleShouldConnectToWatchCommand_CanExecuteChanged;
|
||||
}
|
||||
|
||||
internal void UnsubscribeEvents()
|
||||
{
|
||||
ToggleIsScreenCaptureAllowedCommand.CanExecuteChanged -= ToggleIsScreenCaptureAllowedCommand_CanExecuteChanged;
|
||||
ToggleShouldConnectToWatchCommand.CanExecuteChanged -= ToggleShouldConnectToWatchCommand_CanExecuteChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
178
src/Core/Pages/Settings/SecuritySettingsPage.xaml
Normal file
178
src/Core/Pages/Settings/SecuritySettingsPage.xaml
Normal file
@@ -0,0 +1,178 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:SecuritySettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:Class="Bit.App.Pages.SecuritySettingsPage"
|
||||
Title="{u:I18n AccountSecurity}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SecuritySettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<u:IsNotNullConverter x:Key="isNotNullConverter" />
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="5">
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n ApproveLoginRequests}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices}"
|
||||
IsToggled="{Binding UseThisDeviceToApproveLoginRequests}"
|
||||
AutomationId="ApproveLoginRequestsMadeFromOtherDevicesSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n PendingLogInRequests}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
IsVisible="{Binding UseThisDeviceToApproveLoginRequests}"
|
||||
AutomationId="PendingLogInRequestsLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToPendingLogInRequestsCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<BoxView StyleClass="settings-box-row-separator" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n UnlockOptions}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{Binding UnlockWithBiometricsTitle}"
|
||||
IsToggled="{Binding CanUnlockWithBiometrics}"
|
||||
IsVisible="{Binding UnlockWithBiometricsTitle, Converter={StaticResource isNotNullConverter}}"
|
||||
AutomationId="CanUnlockWithBiometricsSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n UnlockWithPIN}"
|
||||
IsToggled="{Binding CanUnlockWithPin}"
|
||||
AutomationId="CanUnlockWithPinSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<BoxView StyleClass="settings-box-row-separator" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n SessionTimeout}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<Frame
|
||||
IsVisible="{Binding ShowVaultTimeoutPolicyInfo}"
|
||||
Padding="10"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}"
|
||||
AutomationId="VaultTimeoutPolicyLabel"
|
||||
Margin="16,5">
|
||||
<Label
|
||||
Text="{Binding VaultTimeoutPolicyDescription}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n SessionTimeout}"
|
||||
DisplayValue="{Binding VaultTimeoutPickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding VaultTimeoutPickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="VaultTimeoutChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<controls:BaseSettingItemView
|
||||
Title="{u:I18n Custom}"
|
||||
IsVisible="{Binding ShowCustomVaultTimeoutPicker}"
|
||||
AutomationId="CustomVaultTimeoutChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
ControlTemplate="{StaticResource SettingControlTemplate}">
|
||||
<TimePicker Time="{Binding CustomVaultTimeoutTime}" Format="HH:mm"
|
||||
FontSize="Small"
|
||||
TextColor="{DynamicResource MutedColor}"
|
||||
StyleClass="list-sub" Margin="-5"
|
||||
HorizontalOptions="End"
|
||||
ios:TimePicker.UpdateMode="WhenFinished"
|
||||
AutomationId="SettingCustomVaultTimeoutPicker"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{Binding CustomVaultTimeoutTimeVerbalized}"/>
|
||||
</controls:BaseSettingItemView>
|
||||
|
||||
<controls:SettingChooserItemView
|
||||
Title="{u:I18n SessionTimeoutAction}"
|
||||
DisplayValue="{Binding VaultTimeoutActionPickerViewModel.SelectedValue}"
|
||||
ChooseCommand="{Binding VaultTimeoutActionPickerViewModel.SelectOptionCommand}"
|
||||
AutomationId="VaultTimeoutActionChooser"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand"/>
|
||||
|
||||
<BoxView StyleClass="settings-box-row-separator" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n Other}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n AccountFingerprintPhrase}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="AccountFingerprintPhraseLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding ShowAccountFingerprintPhraseCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n TwoStepLogin}"
|
||||
GoToLinkCommand="{Binding GoToTwoStepLoginCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="TwoStepLoginLinkItemView" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n ChangeMasterPassword}"
|
||||
GoToLinkCommand="{Binding GoToChangeMasterPasswordCommand}"
|
||||
IsVisible="{Binding ShowChangeMasterPassword}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="ChangeMasterPasswordLinkItemView" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n LockNow}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="LockNowLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding LockCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n LogOut}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="LogOutLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding LogOutCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n DeleteAccount}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="DeleteAccountLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding DeleteAccountCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</pages:BaseContentPage>
|
||||
37
src/Core/Pages/Settings/SecuritySettingsPage.xaml.cs
Normal file
37
src/Core/Pages/Settings/SecuritySettingsPage.xaml.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SecuritySettingsPage : BaseContentPage
|
||||
{
|
||||
private SecuritySettingsPageViewModel _vm;
|
||||
|
||||
public SecuritySettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SecuritySettingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>().ShowToast(null, null, AppResources.AnErrorHasOccurred);
|
||||
|
||||
Navigation.PopAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
551
src/Core/Pages/Settings/SecuritySettingsPageViewModel.cs
Normal file
551
src/Core/Pages/Settings/SecuritySettingsPageViewModel.cs
Normal file
@@ -0,0 +1,551 @@
|
||||
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.Core.Resources.Localization;
|
||||
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 Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SecuritySettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private const int NEVER_SESSION_TIMEOUT_VALUE = -2;
|
||||
private const int CUSTOM_VAULT_TIMEOUT_VALUE = -100;
|
||||
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly IBiometricService _biometricsService;
|
||||
private readonly IUserPinService _userPinService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private bool _inited;
|
||||
private bool _useThisDeviceToApproveLoginRequests;
|
||||
private bool _supportsBiometric, _canUnlockWithBiometrics;
|
||||
private bool _canUnlockWithPin;
|
||||
private bool _hasMasterPassword;
|
||||
private int? _maximumVaultTimeoutPolicy;
|
||||
private string _vaultTimeoutActionPolicy;
|
||||
private TimeSpan? _customVaultTimeoutTime;
|
||||
|
||||
public SecuritySettingsPageViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||
_biometricsService = ServiceContainer.Resolve<IBiometricService>();
|
||||
_userPinService = ServiceContainer.Resolve<IUserPinService>();
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>();
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>();
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
|
||||
VaultTimeoutPickerViewModel = new PickerViewModel<int>(
|
||||
_deviceActionService,
|
||||
_logger,
|
||||
OnVaultTimeoutChangingAsync,
|
||||
AppResources.SessionTimeout,
|
||||
() => _inited,
|
||||
ex => HandleException(ex));
|
||||
VaultTimeoutPickerViewModel.SetAfterSelectionChanged(_ => MainThread.InvokeOnMainThreadAsync(TriggerUpdateCustomVaultTimeoutPicker));
|
||||
|
||||
VaultTimeoutActionPickerViewModel = new PickerViewModel<VaultTimeoutAction>(
|
||||
_deviceActionService,
|
||||
_logger,
|
||||
OnVaultTimeoutActionChangingAsync,
|
||||
AppResources.SessionTimeoutAction,
|
||||
() => _inited && !HasVaultTimeoutActionPolicy,
|
||||
ex => HandleException(ex));
|
||||
|
||||
ToggleUseThisDeviceToApproveLoginRequestsCommand = CreateDefaultAsyncCommnad(ToggleUseThisDeviceToApproveLoginRequestsAsync, () => _inited);
|
||||
GoToPendingLogInRequestsCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new LoginPasswordlessRequestsListPage())));
|
||||
ToggleCanUnlockWithBiometricsCommand = CreateDefaultAsyncCommnad(ToggleCanUnlockWithBiometricsAsync, () => _inited);
|
||||
ToggleCanUnlockWithPinCommand = CreateDefaultAsyncCommnad(ToggleCanUnlockWithPinAsync, () => _inited);
|
||||
ShowAccountFingerprintPhraseCommand = CreateDefaultAsyncCommnad(ShowAccountFingerprintPhraseAsync);
|
||||
GoToTwoStepLoginCommand = CreateDefaultAsyncCommnad(() => GoToWebVaultSettingsAsync(AppResources.TwoStepLoginDescriptionLong, AppResources.ContinueToWebApp));
|
||||
GoToChangeMasterPasswordCommand = CreateDefaultAsyncCommnad(() => GoToWebVaultSettingsAsync(AppResources.ChangeMasterPasswordDescriptionLong, AppResources.ContinueToWebApp));
|
||||
LockCommand = CreateDefaultAsyncCommnad(() => _vaultTimeoutService.LockAsync(true, true));
|
||||
LogOutCommand = CreateDefaultAsyncCommnad(LogOutAsync);
|
||||
DeleteAccountCommand = CreateDefaultAsyncCommnad(() => Page.Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage())));
|
||||
}
|
||||
|
||||
public bool UseThisDeviceToApproveLoginRequests
|
||||
{
|
||||
get => _useThisDeviceToApproveLoginRequests;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useThisDeviceToApproveLoginRequests, value))
|
||||
{
|
||||
((ICommand)ToggleUseThisDeviceToApproveLoginRequestsCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string UnlockWithBiometricsTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_supportsBiometric)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var biometricName = AppResources.Biometrics;
|
||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
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 => // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
Device.RuntimePlatform != Device.iOS;
|
||||
|
||||
private bool HasVaultTimeoutActionPolicy => !string.IsNullOrEmpty(_vaultTimeoutActionPolicy);
|
||||
|
||||
public PickerViewModel<int> VaultTimeoutPickerViewModel { get; }
|
||||
public PickerViewModel<VaultTimeoutAction> VaultTimeoutActionPickerViewModel { get; }
|
||||
|
||||
public AsyncCommand ToggleUseThisDeviceToApproveLoginRequestsCommand { get; }
|
||||
public ICommand GoToPendingLogInRequestsCommand { get; }
|
||||
public AsyncCommand ToggleCanUnlockWithBiometricsCommand { get; }
|
||||
public AsyncCommand ToggleCanUnlockWithPinCommand { get; }
|
||||
public ICommand ShowAccountFingerprintPhraseCommand { get; }
|
||||
public ICommand GoToTwoStepLoginCommand { get; }
|
||||
public ICommand GoToChangeMasterPasswordCommand { get; }
|
||||
public ICommand LockCommand { get; }
|
||||
public ICommand LogOutCommand { get; }
|
||||
public ICommand DeleteAccountCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var decryptionOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
// set default true for backwards compatibility
|
||||
_hasMasterPassword = decryptionOptions?.HasMasterPassword ?? true;
|
||||
_useThisDeviceToApproveLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
|
||||
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
|
||||
_canUnlockWithBiometrics = await _vaultTimeoutService.IsBiometricLockSetAsync();
|
||||
_canUnlockWithPin = await _vaultTimeoutService.GetPinLockTypeAsync() != Core.Services.PinLockType.Disabled;
|
||||
|
||||
await LoadPoliciesAsync();
|
||||
await InitVaultTimeoutPickerAsync();
|
||||
await InitVaultTimeoutActionPickerAsync();
|
||||
|
||||
ShowChangeMasterPassword = IncludeLinksWithSubscriptionInfo && await _userVerificationService.HasMasterPasswordAsync();
|
||||
|
||||
_inited = true;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(UseThisDeviceToApproveLoginRequests));
|
||||
TriggerPropertyChanged(nameof(UnlockWithBiometricsTitle));
|
||||
TriggerPropertyChanged(nameof(CanUnlockWithBiometrics));
|
||||
TriggerPropertyChanged(nameof(CanUnlockWithPin));
|
||||
TriggerPropertyChanged(nameof(ShowVaultTimeoutPolicyInfo));
|
||||
TriggerPropertyChanged(nameof(VaultTimeoutPolicyDescription));
|
||||
TriggerPropertyChanged(nameof(ShowChangeMasterPassword));
|
||||
TriggerUpdateCustomVaultTimeoutPicker();
|
||||
ToggleUseThisDeviceToApproveLoginRequestsCommand.RaiseCanExecuteChanged();
|
||||
ToggleCanUnlockWithBiometricsCommand.RaiseCanExecuteChanged();
|
||||
ToggleCanUnlockWithPinCommand.RaiseCanExecuteChanged();
|
||||
VaultTimeoutPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
VaultTimeoutActionPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LoadPoliciesAsync()
|
||||
{
|
||||
if (!await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var maximumVaultTimeoutPolicy = await _policyService.FirstOrDefault(PolicyType.MaximumVaultTimeout);
|
||||
_maximumVaultTimeoutPolicy = maximumVaultTimeoutPolicy?.GetInt(Policy.MINUTES_KEY);
|
||||
_vaultTimeoutActionPolicy = maximumVaultTimeoutPolicy?.GetString(Policy.ACTION_KEY);
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(VaultTimeoutActionPickerViewModel.SelectOptionCommand.RaiseCanExecuteChanged);
|
||||
}
|
||||
|
||||
private async Task InitVaultTimeoutPickerAsync()
|
||||
{
|
||||
var options = new Dictionary<int, string>
|
||||
{
|
||||
[0] = AppResources.Immediately,
|
||||
[1] = AppResources.OneMinute,
|
||||
[5] = AppResources.FiveMinutes,
|
||||
[15] = AppResources.FifteenMinutes,
|
||||
[30] = AppResources.ThirtyMinutes,
|
||||
[60] = AppResources.OneHour,
|
||||
[240] = AppResources.FourHours,
|
||||
[-1] = AppResources.OnRestart,
|
||||
[NEVER_SESSION_TIMEOUT_VALUE] = AppResources.Never
|
||||
};
|
||||
|
||||
if (_maximumVaultTimeoutPolicy.HasValue)
|
||||
{
|
||||
options = options.Where(t => t.Key >= 0 && t.Key <= _maximumVaultTimeoutPolicy.Value)
|
||||
.ToDictionary(v => v.Key, v => v.Value);
|
||||
}
|
||||
|
||||
options.Add(CUSTOM_VAULT_TIMEOUT_VALUE, AppResources.Custom);
|
||||
|
||||
var vaultTimeout = await _vaultTimeoutService.GetVaultTimeout() ?? NEVER_SESSION_TIMEOUT_VALUE;
|
||||
VaultTimeoutPickerViewModel.Init(options, vaultTimeout, CUSTOM_VAULT_TIMEOUT_VALUE, false);
|
||||
|
||||
if (VaultTimeoutPickerViewModel.SelectedKey == CUSTOM_VAULT_TIMEOUT_VALUE)
|
||||
{
|
||||
_customVaultTimeoutTime = TimeSpan.FromMinutes(vaultTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitVaultTimeoutActionPickerAsync()
|
||||
{
|
||||
var options = new Dictionary<VaultTimeoutAction, string>();
|
||||
if (IsVaultTimeoutActionLockAllowed)
|
||||
{
|
||||
options.Add(VaultTimeoutAction.Lock, AppResources.Lock);
|
||||
}
|
||||
options.Add(VaultTimeoutAction.Logout, AppResources.LogOut);
|
||||
|
||||
var timeoutAction = await _vaultTimeoutService.GetVaultTimeoutAction() ?? VaultTimeoutAction.Lock;
|
||||
if (!IsVaultTimeoutActionLockAllowed && timeoutAction == VaultTimeoutAction.Lock)
|
||||
{
|
||||
timeoutAction = VaultTimeoutAction.Logout;
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, VaultTimeoutAction.Logout);
|
||||
}
|
||||
|
||||
VaultTimeoutActionPickerViewModel.Init(options, timeoutAction, IsVaultTimeoutActionLockAllowed ? VaultTimeoutAction.Lock : VaultTimeoutAction.Logout);
|
||||
}
|
||||
|
||||
private async Task ToggleUseThisDeviceToApproveLoginRequestsAsync()
|
||||
{
|
||||
if (UseThisDeviceToApproveLoginRequests
|
||||
&&
|
||||
!await Page.DisplayAlert(AppResources.ApproveLoginRequests, AppResources.UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices, AppResources.Yes, AppResources.No))
|
||||
{
|
||||
_useThisDeviceToApproveLoginRequests = !UseThisDeviceToApproveLoginRequests;
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(UseThisDeviceToApproveLoginRequests)));
|
||||
return;
|
||||
}
|
||||
|
||||
await _stateService.SetApprovePasswordlessLoginsAsync(UseThisDeviceToApproveLoginRequests);
|
||||
|
||||
if (!UseThisDeviceToApproveLoginRequests || await _pushNotificationService.AreNotificationsSettingsEnabledAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var openAppSettingsResult = await _platformUtilsService.ShowDialogAsync(
|
||||
AppResources.ReceivePushNotificationsForNewLoginRequests,
|
||||
string.Empty,
|
||||
AppResources.Settings,
|
||||
AppResources.NoThanks
|
||||
);
|
||||
if (openAppSettingsResult)
|
||||
{
|
||||
_deviceActionService.OpenAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleCanUnlockWithBiometricsAsync()
|
||||
{
|
||||
if (!_canUnlockWithBiometrics)
|
||||
{
|
||||
await UpdateVaultTimeoutActionIfNeededAsync();
|
||||
await _biometricsService.SetCanUnlockWithBiometricsAsync(CanUnlockWithBiometrics);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
if (!_supportsBiometric
|
||||
||
|
||||
!await _platformUtilsService.AuthenticateBiometricAsync(null, Device.RuntimePlatform == Device.Android ? "." : null))
|
||||
{
|
||||
_canUnlockWithBiometrics = false;
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));
|
||||
return;
|
||||
}
|
||||
|
||||
await _biometricsService.SetCanUnlockWithBiometricsAsync(CanUnlockWithBiometrics);
|
||||
}
|
||||
|
||||
public async Task ToggleCanUnlockWithPinAsync()
|
||||
{
|
||||
if (!CanUnlockWithPin)
|
||||
{
|
||||
await _vaultTimeoutService.ClearAsync();
|
||||
await UpdateVaultTimeoutActionIfNeededAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var newPin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN,
|
||||
AppResources.SetPINDescription, null, AppResources.Submit, AppResources.Cancel, true);
|
||||
if (string.IsNullOrWhiteSpace(newPin))
|
||||
{
|
||||
_canUnlockWithPin = false;
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithPin)));
|
||||
return;
|
||||
}
|
||||
|
||||
var requireMasterPasswordOnRestart = await _userVerificationService.HasMasterPasswordAsync()
|
||||
&&
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PINRequireMasterPasswordRestart,
|
||||
AppResources.UnlockWithPIN,
|
||||
AppResources.Yes,
|
||||
AppResources.No);
|
||||
|
||||
await _userPinService.SetupPinAsync(newPin, requireMasterPasswordOnRestart);
|
||||
}
|
||||
|
||||
private async Task UpdateVaultTimeoutActionIfNeededAsync()
|
||||
{
|
||||
if (IsVaultTimeoutActionLockAllowed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VaultTimeoutActionPickerViewModel.Select(VaultTimeoutAction.Logout);
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, VaultTimeoutAction.Logout);
|
||||
_deviceActionService.Toast(AppResources.VaultTimeoutActionChangedToLogOut);
|
||||
}
|
||||
|
||||
private async Task<bool> OnVaultTimeoutChangingAsync(int newTimeout)
|
||||
{
|
||||
if (newTimeout == NEVER_SESSION_TIMEOUT_VALUE
|
||||
&&
|
||||
!await _platformUtilsService.ShowDialogAsync(AppResources.NeverLockWarning, AppResources.Warning, AppResources.Yes, AppResources.Cancel))
|
||||
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newTimeout == CUSTOM_VAULT_TIMEOUT_VALUE)
|
||||
{
|
||||
_customVaultTimeoutTime = TimeSpan.FromMinutes(0);
|
||||
}
|
||||
|
||||
return await UpdateVaultTimeoutAsync(newTimeout);
|
||||
}
|
||||
|
||||
private async Task<bool> UpdateVaultTimeoutAsync(int newTimeout)
|
||||
{
|
||||
var rawTimeout = GetRawVaultTimeoutFrom(newTimeout);
|
||||
|
||||
if (rawTimeout > _maximumVaultTimeoutPolicy)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutToLarge, AppResources.Warning);
|
||||
VaultTimeoutPickerViewModel.Select(_maximumVaultTimeoutPolicy.Value, false);
|
||||
|
||||
if (VaultTimeoutPickerViewModel.SelectedKey == CUSTOM_VAULT_TIMEOUT_VALUE)
|
||||
{
|
||||
_customVaultTimeoutTime = TimeSpan.FromMinutes(_maximumVaultTimeoutPolicy.Value);
|
||||
}
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(TriggerUpdateCustomVaultTimeoutPicker);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(rawTimeout, VaultTimeoutActionPickerViewModel.SelectedKey);
|
||||
|
||||
await _cryptoService.RefreshKeysAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TriggerUpdateCustomVaultTimeoutPicker()
|
||||
{
|
||||
TriggerPropertyChanged(nameof(ShowCustomVaultTimeoutPicker));
|
||||
TriggerPropertyChanged(nameof(CustomVaultTimeoutTime));
|
||||
}
|
||||
|
||||
private int? GetRawVaultTimeoutFrom(int vaultTimeoutPickerKey)
|
||||
{
|
||||
if (vaultTimeoutPickerKey == NEVER_SESSION_TIMEOUT_VALUE)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (vaultTimeoutPickerKey == CUSTOM_VAULT_TIMEOUT_VALUE
|
||||
&&
|
||||
CustomVaultTimeoutTime.HasValue)
|
||||
{
|
||||
return (int)CustomVaultTimeoutTime.Value.TotalMinutes;
|
||||
}
|
||||
|
||||
return vaultTimeoutPickerKey;
|
||||
}
|
||||
|
||||
private async Task<bool> OnVaultTimeoutActionChangingAsync(VaultTimeoutAction timeoutActionKey)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_vaultTimeoutActionPolicy))
|
||||
{
|
||||
// do nothing if we have a policy set
|
||||
return false;
|
||||
}
|
||||
|
||||
if (timeoutActionKey == VaultTimeoutAction.Logout
|
||||
&&
|
||||
!await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutLogOutConfirmation, AppResources.Warning, AppResources.Yes, AppResources.Cancel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, timeoutActionKey);
|
||||
_messagingService.Send(AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ShowAccountFingerprintPhraseAsync()
|
||||
{
|
||||
List<string> fingerprint;
|
||||
try
|
||||
{
|
||||
fingerprint = await _cryptoService.GetFingerprintAsync(await _stateService.GetActiveUserIdAsync());
|
||||
}
|
||||
catch (Exception e) when (e.Message == "No public key available.")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var phrase = string.Join("-", fingerprint);
|
||||
var text = $"{AppResources.YourAccountsFingerprint}:\n\n{phrase}";
|
||||
|
||||
var learnMore = await _platformUtilsService.ShowDialogAsync(text, AppResources.FingerprintPhrase,
|
||||
AppResources.LearnMore, AppResources.Close);
|
||||
if (learnMore)
|
||||
{
|
||||
_platformUtilsService.LaunchUri(ExternalLinksConstants.HELP_FINGERPRINT_PHRASE);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GoToWebVaultSettingsAsync(string dialogText, string dialogTitle)
|
||||
{
|
||||
if (await _platformUtilsService.ShowDialogAsync(dialogText, dialogTitle, AppResources.Continue, AppResources.Cancel))
|
||||
{
|
||||
_platformUtilsService.LaunchUri(string.Format(ExternalLinksConstants.WEB_VAULT_SETTINGS_FORMAT, _environmentService.GetWebVaultUrl()));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogOutAsync()
|
||||
{
|
||||
if (await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation, AppResources.LogOut, AppResources.Yes, AppResources.Cancel))
|
||||
{
|
||||
_messagingService.Send(AccountsManagerMessageCommands.LOGOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/Core/Pages/Settings/SettingsPage/SettingsPage.xaml
Normal file
32
src/Core/Pages/Settings/SettingsPage/SettingsPage.xaml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.SettingsPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:SettingsPageViewModel"
|
||||
Title="{u:I18n Settings}"
|
||||
x:Name="_page">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<StackLayout BindableLayout.ItemsSource="{Binding SettingsItems}">
|
||||
<BindableLayout.ItemTemplate>
|
||||
<DataTemplate x:DataType="pages:SettingsPageListItem">
|
||||
<StackLayout>
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding BindingContext.ExecuteSettingItemCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"/>
|
||||
</StackLayout.GestureRecognizers>
|
||||
<controls:CustomLabel
|
||||
Text="{Binding Name}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="{Binding AutomationId}" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
</DataTemplate>
|
||||
</BindableLayout.ItemTemplate>
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
||||
29
src/Core/Pages/Settings/SettingsPage/SettingsPage.xaml.cs
Normal file
29
src/Core/Pages/Settings/SettingsPage/SettingsPage.xaml.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SettingsPage : BaseContentPage
|
||||
{
|
||||
private readonly TabsPage _tabsPage;
|
||||
|
||||
public SettingsPage(TabsPage tabsPage)
|
||||
{
|
||||
_tabsPage = tabsPage;
|
||||
InitializeComponent();
|
||||
var vm = BindingContext as SettingsPageViewModel;
|
||||
vm.Page = this;
|
||||
}
|
||||
|
||||
protected override bool OnBackButtonPressed()
|
||||
{
|
||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
if (Device.RuntimePlatform == Device.Android && _tabsPage != null)
|
||||
{
|
||||
_tabsPage.ResetToVaultPage();
|
||||
return true;
|
||||
}
|
||||
return base.OnBackButtonPressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Core/Pages/Settings/SettingsPage/SettingsPageListItem.cs
Normal file
30
src/Core/Pages/Settings/SettingsPage/SettingsPageListItem.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities.Automation;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SettingsPageListItem
|
||||
{
|
||||
private readonly string _nameResourceKey;
|
||||
|
||||
public SettingsPageListItem(string nameResourceKey, Func<Task> executeAsync)
|
||||
{
|
||||
_nameResourceKey = nameResourceKey;
|
||||
ExecuteAsync = executeAsync;
|
||||
}
|
||||
|
||||
public string Name => AppResources.ResourceManager.GetString(_nameResourceKey);
|
||||
|
||||
public Func<Task> ExecuteAsync { get; }
|
||||
|
||||
public string AutomationId
|
||||
{
|
||||
get
|
||||
{
|
||||
return AutomationIdsHelper.AddSuffixFor(AutomationIdsHelper.ToEnglishTitleCase(_nameResourceKey), SuffixType.Cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Resources.Localization;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
public SettingsPageViewModel()
|
||||
{
|
||||
ExecuteSettingItemCommand = new AsyncCommand<SettingsPageListItem>(item => item.ExecuteAsync(),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
SettingsItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem(nameof(AppResources.AccountSecurity), () => NavigateToAsync(new SecuritySettingsPage())),
|
||||
new SettingsPageListItem(nameof(AppResources.Autofill), () => NavigateToAsync(new AutofillSettingsPage())),
|
||||
new SettingsPageListItem(nameof(AppResources.Vault), () => NavigateToAsync(new VaultSettingsPage())),
|
||||
new SettingsPageListItem(nameof(AppResources.Appearance), () => NavigateToAsync(new AppearanceSettingsPage())),
|
||||
new SettingsPageListItem(nameof(AppResources.Other), () => NavigateToAsync(new OtherSettingsPage())),
|
||||
new SettingsPageListItem(nameof(AppResources.About), () => NavigateToAsync(new AboutSettingsPage()))
|
||||
};
|
||||
}
|
||||
|
||||
public List<SettingsPageListItem> SettingsItems { get; }
|
||||
|
||||
public AsyncCommand<SettingsPageListItem> ExecuteSettingItemCommand { get; }
|
||||
|
||||
private async Task NavigateToAsync(Page page)
|
||||
{
|
||||
await Page.Navigation.PushAsync(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/Core/Pages/Settings/VaultSettingsPage.xaml
Normal file
48
src/Core/Pages/Settings/VaultSettingsPage.xaml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
x:DataType="pages:VaultSettingsPageViewModel"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:Class="Bit.App.Pages.VaultSettingsPage"
|
||||
Title="{u:I18n Vault}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:VaultSettingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<StackLayout>
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n Folders}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="FoldersLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToFoldersCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:CustomLabel
|
||||
Text="{u:I18n ExportVault}"
|
||||
StyleClass="settings-navigatable-label"
|
||||
AutomationId="ExportVaultLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding GoToExportVaultCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</controls:CustomLabel>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
<controls:ExternalLinkItemView
|
||||
Title="{u:I18n ImportItems}"
|
||||
GoToLinkCommand="{Binding GoToImportItemsCommand}"
|
||||
StyleClass="settings-external-link-item"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="ImportItemsLinkItemView" />
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
|
||||
</StackLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
12
src/Core/Pages/Settings/VaultSettingsPage.xaml.cs
Normal file
12
src/Core/Pages/Settings/VaultSettingsPage.xaml.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class VaultSettingsPage : BaseContentPage
|
||||
{
|
||||
public VaultSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
var vm = BindingContext as VaultSettingsPageViewModel;
|
||||
vm.Page = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/Core/Pages/Settings/VaultSettingsPageViewModel.cs
Normal file
52
src/Core/Pages/Settings/VaultSettingsPageViewModel.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class VaultSettingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
|
||||
public VaultSettingsPageViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||
|
||||
GoToFoldersCommand = new AsyncCommand(() => Page.Navigation.PushModalAsync(new NavigationPage(new FoldersPage())),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
GoToExportVaultCommand = new AsyncCommand(() => Page.Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage())),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
GoToImportItemsCommand = new AsyncCommand(GoToImportItemsAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public ICommand GoToFoldersCommand { get; }
|
||||
public ICommand GoToExportVaultCommand { get; }
|
||||
public ICommand GoToImportItemsCommand { get; }
|
||||
|
||||
private async Task GoToImportItemsAsync()
|
||||
{
|
||||
var webVaultUrl = _environmentService.GetWebVaultUrl();
|
||||
var body = string.Format(AppResources.YouCanImportDataToYourVaultOnX, webVaultUrl);
|
||||
if (await _platformUtilsService.ShowDialogAsync(body, AppResources.ContinueToWebApp, AppResources.Continue, AppResources.Cancel))
|
||||
{
|
||||
_platformUtilsService.LaunchUri(webVaultUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user