mirror of
https://github.com/bitwarden/mobile
synced 2026-01-04 09:33:16 +00:00
PM-3349 PM-3350 MAUI Migration Initial
This commit is contained in:
177
src/Core/Pages/Accounts/BaseChangePasswordViewModel.cs
Normal file
177
src/Core/Pages/Accounts/BaseChangePasswordViewModel.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
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.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Networking;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class BaseChangePasswordViewModel : BaseViewModel
|
||||
{
|
||||
protected readonly IPlatformUtilsService _platformUtilsService;
|
||||
protected readonly IStateService _stateService;
|
||||
protected readonly IPolicyService _policyService;
|
||||
protected readonly IPasswordGenerationService _passwordGenerationService;
|
||||
protected readonly II18nService _i18nService;
|
||||
protected readonly ICryptoService _cryptoService;
|
||||
protected readonly IDeviceActionService _deviceActionService;
|
||||
protected readonly IApiService _apiService;
|
||||
protected readonly ISyncService _syncService;
|
||||
|
||||
private bool _showPassword;
|
||||
private bool _isPolicyInEffect;
|
||||
private string _policySummary;
|
||||
private MasterPasswordPolicyOptions _policy;
|
||||
|
||||
protected BaseChangePasswordViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_passwordGenerationService =
|
||||
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
}
|
||||
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText)
|
||||
});
|
||||
}
|
||||
|
||||
public bool IsPolicyInEffect
|
||||
{
|
||||
get => _isPolicyInEffect;
|
||||
set => SetProperty(ref _isPolicyInEffect, value);
|
||||
}
|
||||
|
||||
public string PolicySummary
|
||||
{
|
||||
get => _policySummary;
|
||||
set => SetProperty(ref _policySummary, value);
|
||||
}
|
||||
|
||||
public MasterPasswordPolicyOptions Policy
|
||||
{
|
||||
get => _policy;
|
||||
set => SetProperty(ref _policy, value);
|
||||
}
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string MasterPassword { get; set; }
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
|
||||
public virtual async Task InitAsync(bool forceSync = false)
|
||||
{
|
||||
if (forceSync)
|
||||
{
|
||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
||||
await task.ContinueWith(async (t) => await CheckPasswordPolicy());
|
||||
}
|
||||
else
|
||||
{
|
||||
await CheckPasswordPolicy();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckPasswordPolicy()
|
||||
{
|
||||
Policy = await _policyService.GetMasterPasswordPolicyOptions();
|
||||
IsPolicyInEffect = Policy?.InEffect() ?? false;
|
||||
if (!IsPolicyInEffect)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bullet = "\n" + "".PadLeft(4) + "\u2022 ";
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(_i18nService.T("MasterPasswordPolicyInEffect"));
|
||||
if (Policy.MinComplexity > 0)
|
||||
{
|
||||
sb.Append(bullet)
|
||||
.Append(string.Format(_i18nService.T("PolicyInEffectMinComplexity"), Policy.MinComplexity));
|
||||
}
|
||||
if (Policy.MinLength > 0)
|
||||
{
|
||||
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectMinLength"), Policy.MinLength));
|
||||
}
|
||||
if (Policy.RequireUpper)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectUppercase"));
|
||||
}
|
||||
if (Policy.RequireLower)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectLowercase"));
|
||||
}
|
||||
if (Policy.RequireNumbers)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectNumbers"));
|
||||
}
|
||||
if (Policy.RequireSpecial)
|
||||
{
|
||||
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectSpecial"), "!@#$%^&*"));
|
||||
}
|
||||
PolicySummary = sb.ToString();
|
||||
}
|
||||
|
||||
protected async Task<bool> ValidateMasterPasswordAsync()
|
||||
{
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
if (IsPolicyInEffect)
|
||||
{
|
||||
var userInputs = _passwordGenerationService.GetPasswordStrengthUserInput(await _stateService.GetEmailAsync());
|
||||
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInputs);
|
||||
if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordPolicyValidationMessage,
|
||||
AppResources.MasterPasswordPolicyValidationTitle, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MasterPassword.Length < Constants.MasterPasswordMinimumChars)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.MasterPasswordLengthValMessageX, Constants.MasterPasswordMinimumChars),
|
||||
AppResources.MasterPasswordPolicyValidationTitle, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (MasterPassword != ConfirmMasterPassword)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordConfirmationValMessage,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/Core/Pages/Accounts/DeleteAccountPage.xaml
Normal file
81
src/Core/Pages/Accounts/DeleteAccountPage.xaml
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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.Accounts.DeleteAccountPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:DeleteAccountViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:DeleteAccountViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<Style TargetType="Label" x:Key="lblDescription">
|
||||
<Setter Property="FontSize" Value="{OnPlatform Android=Large, iOS=Small}" />
|
||||
</Style>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.Content>
|
||||
<Grid Padding="20, 30" RowSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Source="ic_warning"
|
||||
WidthRequest="28"
|
||||
HeightRequest="25"
|
||||
HorizontalOptions="Start" />
|
||||
<Label
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Text="{u:I18n DeletingYourAccountIsPermanent}"
|
||||
HorizontalOptions="Start"
|
||||
StyleClass="title-danger"
|
||||
Margin="0,15,0,0"/>
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Text="{u:I18n DeleteAccountExplanation}"
|
||||
Style="{StaticResource lblDescription}"
|
||||
HorizontalOptions="Start"
|
||||
Margin="0,6,50,0"
|
||||
Opacity="0.6" />
|
||||
<Button
|
||||
Grid.Row="3"
|
||||
Text="{u:I18n DeleteAccount}"
|
||||
StyleClass="btn-danger"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Start"
|
||||
Margin="0,20,0,0"
|
||||
Padding="16,0"
|
||||
CornerRadius="2"
|
||||
TextTransform="Uppercase"
|
||||
Clicked="DeleteAccount_Clicked"/>
|
||||
<Button
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Text="{u:I18n Cancel}"
|
||||
StyleClass="btn-secondary"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Start"
|
||||
Margin="0,20,0,0"
|
||||
Padding="16,0"
|
||||
CornerRadius="2"
|
||||
TextTransform="Uppercase"
|
||||
Clicked="Close_Clicked" />
|
||||
</Grid>
|
||||
</ContentPage.Content>
|
||||
</pages:BaseContentPage>
|
||||
34
src/Core/Pages/Accounts/DeleteAccountPage.xaml.cs
Normal file
34
src/Core/Pages/Accounts/DeleteAccountPage.xaml.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages.Accounts
|
||||
{
|
||||
public partial class DeleteAccountPage : BaseContentPage
|
||||
{
|
||||
DeleteAccountViewModel _vm;
|
||||
|
||||
public DeleteAccountPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as DeleteAccountViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void DeleteAccount_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.DeleteAccountAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/Core/Pages/Accounts/DeleteAccountViewModel.cs
Normal file
110
src/Core/Pages/Accounts/DeleteAccountViewModel.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class DeleteAccountViewModel : BaseViewModel
|
||||
{
|
||||
readonly IPlatformUtilsService _platformUtilsService;
|
||||
readonly IVerificationActionsFlowHelper _verificationActionsFlowHelper;
|
||||
readonly ILogger _logger;
|
||||
|
||||
public DeleteAccountViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_verificationActionsFlowHelper = ServiceContainer.Resolve<IVerificationActionsFlowHelper>("verificationActionsFlowHelper");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
PageTitle = AppResources.DeleteAccount;
|
||||
}
|
||||
|
||||
public async Task DeleteAccountAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
await _verificationActionsFlowHelper
|
||||
.Configure(VerificationFlowAction.DeleteAccount,
|
||||
null,
|
||||
AppResources.DeleteAccount,
|
||||
true)
|
||||
.ValidateAndExecuteAsync();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IDeleteAccountActionFlowExecutioner : IActionFlowExecutioner { }
|
||||
|
||||
public class DeleteAccountActionFlowExecutioner : IDeleteAccountActionFlowExecutioner
|
||||
{
|
||||
readonly IApiService _apiService;
|
||||
readonly IMessagingService _messagingService;
|
||||
readonly IPlatformUtilsService _platformUtilsService;
|
||||
readonly IDeviceActionService _deviceActionService;
|
||||
readonly ILogger _logger;
|
||||
|
||||
public DeleteAccountActionFlowExecutioner(IApiService apiService,
|
||||
IMessagingService messagingService,
|
||||
IPlatformUtilsService platformUtilsService,
|
||||
IDeviceActionService deviceActionService,
|
||||
ILogger logger)
|
||||
{
|
||||
_apiService = apiService;
|
||||
_messagingService = messagingService;
|
||||
_platformUtilsService = platformUtilsService;
|
||||
_deviceActionService = deviceActionService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Execute(IActionFlowParmeters parameters)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.DeletingYourAccount);
|
||||
|
||||
await _apiService.DeleteAccountAsync(new Core.Models.Request.DeleteAccountRequest
|
||||
{
|
||||
MasterPasswordHash = parameters.VerificationType == Core.Enums.VerificationType.MasterPassword ? parameters.Secret : (string)null,
|
||||
OTP = parameters.VerificationType == Core.Enums.VerificationType.OTP ? parameters.Secret : (string)null
|
||||
});
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
_messagingService.Send("logout");
|
||||
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.YourAccountHasBeenPermanentlyDeleted);
|
||||
}
|
||||
catch (ApiException apiEx)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
if (apiEx?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(apiEx.Error.GetSingleMessage(), AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_logger.Exception(ex);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/Core/Pages/Accounts/EnvironmentPage.xaml
Normal file
102
src/Core/Pages/Accounts/EnvironmentPage.xaml
Normal file
@@ -0,0 +1,102 @@
|
||||
<?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.EnvironmentPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:EnvironmentPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:EnvironmentPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Command="{Binding SubmitCommand}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n SelfHostedEnvironment, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n ServerUrl}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
Text="{Binding BaseUrl}"
|
||||
Keyboard="Url"
|
||||
Placeholder="ex. https://bitwarden.company.com"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="ServerUrlEntry"/>
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n SelfHostedEnvironmentFooter}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n CustomEnvironment, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n WebVaultUrl}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_webVaultEntry"
|
||||
Text="{Binding WebVaultUrl}"
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value"
|
||||
AutomationId="WebVaultUrlEntry"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n ApiUrl}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_apiEntry"
|
||||
Text="{Binding ApiUrl}"
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value"
|
||||
AutomationId="ApiUrlEntry"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n IdentityUrl}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_identityEntry"
|
||||
Text="{Binding IdentityUrl}"
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value"
|
||||
AutomationId="IdentityUrlEntry"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n IconsUrl}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_iconsEntry"
|
||||
Text="{Binding IconsUrl}"
|
||||
Keyboard="Url"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="IconsUrlEntry"/>
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n CustomEnvironmentFooter}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
55
src/Core/Pages/Accounts/EnvironmentPage.xaml.cs
Normal file
55
src/Core/Pages/Accounts/EnvironmentPage.xaml.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class EnvironmentPage : BaseContentPage
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly EnvironmentPageViewModel _vm;
|
||||
|
||||
public EnvironmentPage()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as EnvironmentPageViewModel;
|
||||
_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.Android)
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
}
|
||||
|
||||
_webVaultEntry.ReturnType = ReturnType.Next;
|
||||
_webVaultEntry.ReturnCommand = new Command(() => _apiEntry.Focus());
|
||||
_apiEntry.ReturnType = ReturnType.Next;
|
||||
_apiEntry.ReturnCommand = new Command(() => _identityEntry.Focus());
|
||||
_identityEntry.ReturnType = ReturnType.Next;
|
||||
_identityEntry.ReturnCommand = new Command(() => _iconsEntry.Focus());
|
||||
_vm.SubmitSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SubmitSuccessAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SubmitSuccessAsync()
|
||||
{
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/Core/Pages/Accounts/EnvironmentPageViewModel.cs
Normal file
91
src/Core/Pages/Accounts/EnvironmentPageViewModel.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class EnvironmentPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
public EnvironmentPageViewModel()
|
||||
{
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
|
||||
PageTitle = AppResources.Settings;
|
||||
BaseUrl = _environmentService.BaseUrl == EnvironmentUrlData.DefaultEU.Base || EnvironmentUrlData.DefaultUS.Base == _environmentService.BaseUrl ?
|
||||
string.Empty : _environmentService.BaseUrl;
|
||||
WebVaultUrl = _environmentService.WebVaultUrl;
|
||||
ApiUrl = _environmentService.ApiUrl;
|
||||
IdentityUrl = _environmentService.IdentityUrl;
|
||||
IconsUrl = _environmentService.IconsUrl;
|
||||
NotificationsUrls = _environmentService.NotificationsUrl;
|
||||
SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public ICommand SubmitCommand { get; }
|
||||
public string BaseUrl { get; set; }
|
||||
public string ApiUrl { get; set; }
|
||||
public string IdentityUrl { get; set; }
|
||||
public string WebVaultUrl { get; set; }
|
||||
public string IconsUrl { get; set; }
|
||||
public string NotificationsUrls { get; set; }
|
||||
public Action SubmitSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
if (!ValidateUrls())
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
|
||||
{
|
||||
Base = BaseUrl,
|
||||
Api = ApiUrl,
|
||||
Identity = IdentityUrl,
|
||||
WebVault = WebVaultUrl,
|
||||
Icons = IconsUrl,
|
||||
Notifications = NotificationsUrls
|
||||
});
|
||||
|
||||
// re-set urls since service can change them, ex: prefixing https://
|
||||
BaseUrl = resUrls.Base;
|
||||
WebVaultUrl = resUrls.WebVault;
|
||||
ApiUrl = resUrls.Api;
|
||||
IdentityUrl = resUrls.Identity;
|
||||
IconsUrl = resUrls.Icons;
|
||||
NotificationsUrls = resUrls.Notifications;
|
||||
|
||||
SubmitSuccessAction?.Invoke();
|
||||
}
|
||||
|
||||
public bool ValidateUrls()
|
||||
{
|
||||
bool IsUrlValid(string url)
|
||||
{
|
||||
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute);
|
||||
}
|
||||
|
||||
return IsUrlValid(BaseUrl)
|
||||
&& IsUrlValid(ApiUrl)
|
||||
&& IsUrlValid(IdentityUrl)
|
||||
&& IsUrlValid(WebVaultUrl)
|
||||
&& IsUrlValid(IconsUrl);
|
||||
}
|
||||
|
||||
private void OnSubmitException(Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/Core/Pages/Accounts/HintPage.xaml
Normal file
40
src/Core/Pages/Accounts/HintPage.xaml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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.HintPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:HintPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:HintPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Command="{Binding SubmitCommand}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n EmailAddress}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_email"
|
||||
Text="{Binding Email}"
|
||||
Keyboard="Email"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n EnterEmailForHint}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
37
src/Core/Pages/Accounts/HintPage.xaml.cs
Normal file
37
src/Core/Pages/Accounts/HintPage.xaml.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class HintPage : BaseContentPage
|
||||
{
|
||||
private HintPageViewModel _vm;
|
||||
|
||||
public HintPage(string email = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as HintPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.Email = email;
|
||||
// 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 void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
RequestFocus(_email);
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/Core/Pages/Accounts/HintPageViewModel.cs
Normal file
86
src/Core/Pages/Accounts/HintPageViewModel.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class HintPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly ILogger _logger;
|
||||
private string _email;
|
||||
|
||||
public HintPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
|
||||
PageTitle = AppResources.PasswordHint;
|
||||
SubmitCommand = new AsyncCommand(SubmitAsync,
|
||||
onException: ex =>
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
_deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
|
||||
},
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public ICommand SubmitCommand { get; }
|
||||
|
||||
public string Email
|
||||
{
|
||||
get => _email;
|
||||
set => SetProperty(ref _email, value);
|
||||
}
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Email))
|
||||
{
|
||||
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (!Email.Contains("@"))
|
||||
{
|
||||
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Submitting);
|
||||
await _apiService.PostPasswordHintAsync(
|
||||
new Core.Models.Request.PasswordHintRequest { Email = Email });
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _deviceActionService.DisplayAlertAsync(null, AppResources.PasswordHintAlert, AppResources.Ok);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
146
src/Core/Pages/Accounts/HomePage.xaml
Normal file
146
src/Core/Pages/Accounts/HomePage.xaml
Normal file
@@ -0,0 +1,146 @@
|
||||
<?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.HomePage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:HomeViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:HomeViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<!-- <controls:ExtendedToolbarItem
|
||||
x:Name="_accountAvatar"
|
||||
IconImageSource="{Binding AvatarImageSource}"
|
||||
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
|
||||
Order="Primary"
|
||||
Priority="-1"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" /> -->
|
||||
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<!--<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<StackLayout x:Name="_mainLayout" x:Key="mainLayout" Spacing="30" Padding="20, 50, 20, 0">
|
||||
<Image
|
||||
x:Name="_logo"
|
||||
Source="logo.png"
|
||||
VerticalOptions="Center" />
|
||||
<Label Text="{u:I18n LoginOrCreateNewAccount}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalTextAlignment="Center"/>
|
||||
<StackLayout
|
||||
StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n EmailAddress}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_email"
|
||||
Text="{Binding Email}"
|
||||
Keyboard="Email"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding ContinueCommand}"
|
||||
AutomationId="EmailAddressEntry"
|
||||
>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{DynamicResource MutedColor}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Entry>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Margin="0, 6, 0 ,0">
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer
|
||||
Command="{Binding ShowEnvironmentPickerCommand}" />
|
||||
</StackLayout.GestureRecognizers>
|
||||
<Label
|
||||
Text="{Binding RegionText}"
|
||||
FontSize="13"
|
||||
TextColor="{DynamicResource MutedColor}"
|
||||
VerticalOptions="Center"
|
||||
VerticalTextAlignment="Center"/>
|
||||
<controls:IconLabel
|
||||
Text="{Binding SelectedEnvironmentName}"
|
||||
FontSize="13"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
VerticalOptions="Center"
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="RegionSelectorDropdown"/>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Margin="0, 20, 0 ,0">
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer
|
||||
Command="{Binding RememberEmailCommand}" />
|
||||
</StackLayout.GestureRecognizers>
|
||||
<Label
|
||||
Text="{u:I18n RememberMe}"
|
||||
StyleClass="text-sm"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="Center"
|
||||
VerticalTextAlignment="Center"
|
||||
/>
|
||||
<Switch
|
||||
Scale="0.8"
|
||||
IsToggled="{Binding RememberEmail}"
|
||||
VerticalOptions="Center"
|
||||
AutomationId="RememberMeSwitch"
|
||||
/>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n Continue}"
|
||||
StyleClass="btn-primary"
|
||||
IsEnabled="{Binding CanContinue}"
|
||||
Command="{Binding ContinueCommand}"
|
||||
AutomationId="ContinueButton"
|
||||
/>
|
||||
|
||||
<Label FormattedText="{Binding CreateAccountText}"
|
||||
Margin="0, 10"
|
||||
StyleClass="box-footer-label"
|
||||
AutomationId="CreateAccountLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding CreateAccountCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>-->
|
||||
|
||||
<Label Text="Lalala" />
|
||||
|
||||
<!--<AbsoluteLayout
|
||||
x:Name="_absLayout"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HorizontalOptions="FillAndExpand">
|
||||
<ContentView
|
||||
x:Name="_mainContent"
|
||||
AbsoluteLayout.LayoutFlags="All"
|
||||
AbsoluteLayout.LayoutBounds="0, 0, 1, 1">
|
||||
</ContentView>
|
||||
|
||||
<controls:AccountSwitchingOverlayView
|
||||
x:Name="_accountListOverlay"
|
||||
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
|
||||
AbsoluteLayout.LayoutFlags="All"
|
||||
MainPage="{Binding Source={x:Reference _page}}"
|
||||
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
|
||||
</AbsoluteLayout>-->
|
||||
</pages:BaseContentPage>
|
||||
149
src/Core/Pages/Accounts/HomePage.xaml.cs
Normal file
149
src/Core/Pages/Accounts/HomePage.xaml.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class HomePage : BaseContentPage
|
||||
{
|
||||
private bool _checkRememberedEmail;
|
||||
private readonly HomeViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
private IBroadcasterService _broadcasterService;
|
||||
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
|
||||
public HomePage(AppOptions appOptions = null)
|
||||
{
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as HomeViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.ShowCancelButton = _appOptions?.IosExtension ?? false;
|
||||
_vm.StartLoginAction = async () => await StartLoginAsync();
|
||||
_vm.StartRegisterAction = () => Device.BeginInvokeOnMainThread(async () => await StartRegisterAsync());
|
||||
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
||||
_vm.StartEnvironmentAction = () => Device.BeginInvokeOnMainThread(async () => await StartEnvironmentAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
// await _accountListOverlay.HideAsync();
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
UpdateLogo();
|
||||
|
||||
if (!_vm.ShowCancelButton)
|
||||
{
|
||||
ToolbarItems.Remove(_closeButton);
|
||||
}
|
||||
if (_appOptions?.HideAccountSwitcher ?? false)
|
||||
{
|
||||
// ToolbarItems.Remove(_accountAvatar);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DismissRegisterPageAndLogInAsync(string email)
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
await Navigation.PushModalAsync(new NavigationPage(new LoginPage(email, _appOptions)));
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
// _mainContent.Content = _mainLayout;
|
||||
// _accountAvatar?.OnAppearing();
|
||||
|
||||
if (!_appOptions?.HideAccountSwitcher ?? false)
|
||||
{
|
||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync(false);
|
||||
}
|
||||
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
|
||||
{
|
||||
if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
UpdateLogo();
|
||||
});
|
||||
}
|
||||
});
|
||||
try
|
||||
{
|
||||
await _vm.UpdateEnvironment();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value?.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnBackButtonPressed()
|
||||
{
|
||||
// if (_accountListOverlay.IsVisible)
|
||||
// {
|
||||
// _accountListOverlay.HideAsync().FireAndForget();
|
||||
// return true;
|
||||
// }
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_broadcasterService.Unsubscribe(nameof(HomePage));
|
||||
// _accountAvatar?.OnDisappearing();
|
||||
}
|
||||
|
||||
private void UpdateLogo()
|
||||
{
|
||||
// _logo.Source = !ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png";
|
||||
}
|
||||
|
||||
private void Cancel_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartLoginAsync()
|
||||
{
|
||||
var page = new LoginPage(_vm.Email, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartRegisterAsync()
|
||||
{
|
||||
var page = new RegisterPage(this);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private void LogInSso_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.StartSsoLoginAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartSsoLoginAsync()
|
||||
{
|
||||
var page = new LoginSsoPage(_appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartEnvironmentAsync()
|
||||
{
|
||||
// await _accountListOverlay.HideAsync();
|
||||
var page = new EnvironmentPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
219
src/Core/Pages/Accounts/HomePageViewModel.cs
Normal file
219
src/Core/Pages/Accounts/HomePageViewModel.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class HomeViewModel : BaseViewModel
|
||||
{
|
||||
private const string LOGGING_IN_ON_US = "bitwarden.com";
|
||||
private const string LOGGING_IN_ON_EU = "bitwarden.eu";
|
||||
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IAccountsManager _accountManager;
|
||||
private readonly IConfigService _configService;
|
||||
|
||||
private bool _showCancelButton;
|
||||
private bool _rememberEmail;
|
||||
private string _email;
|
||||
private string _selectedEnvironmentName;
|
||||
private bool _displayEuEnvironment;
|
||||
|
||||
public HomeViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||
_accountManager = ServiceContainer.Resolve<IAccountsManager>();
|
||||
_configService = ServiceContainer.Resolve<IConfigService>();
|
||||
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
|
||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||
{
|
||||
AllowActiveAccountSelection = true
|
||||
};
|
||||
RememberEmailCommand = new Command(() => RememberEmail = !RememberEmail);
|
||||
ContinueCommand = new AsyncCommand(ContinueToLoginStepAsync, allowsMultipleExecutions: false);
|
||||
CreateAccountCommand = new AsyncCommand(async () => Device.InvokeOnMainThreadAsync(StartRegisterAction),
|
||||
onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||
CloseCommand = new AsyncCommand(async () => Device.InvokeOnMainThreadAsync(CloseAction),
|
||||
onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||
ShowEnvironmentPickerCommand = new AsyncCommand(ShowEnvironmentPickerAsync,
|
||||
onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||
InitAsync().FireAndForget();
|
||||
}
|
||||
|
||||
public bool ShowCancelButton
|
||||
{
|
||||
get => _showCancelButton;
|
||||
set => SetProperty(ref _showCancelButton, value);
|
||||
}
|
||||
|
||||
public bool RememberEmail
|
||||
{
|
||||
get => _rememberEmail;
|
||||
set => SetProperty(ref _rememberEmail, value);
|
||||
}
|
||||
|
||||
public string Email
|
||||
{
|
||||
get => _email;
|
||||
set => SetProperty(ref _email, value,
|
||||
additionalPropertyNames: new[] { nameof(CanContinue) });
|
||||
}
|
||||
|
||||
public string SelectedEnvironmentName
|
||||
{
|
||||
get => $"{_selectedEnvironmentName} {BitwardenIcons.AngleDown}";
|
||||
set => SetProperty(ref _selectedEnvironmentName, value);
|
||||
}
|
||||
|
||||
public string RegionText => $"{AppResources.LoggingInOn}:";
|
||||
public bool CanContinue => !string.IsNullOrEmpty(Email);
|
||||
|
||||
public FormattedString CreateAccountText
|
||||
{
|
||||
get
|
||||
{
|
||||
var fs = new FormattedString();
|
||||
fs.Spans.Add(new Span
|
||||
{
|
||||
Text = $"{AppResources.NewAroundHere} "
|
||||
});
|
||||
fs.Spans.Add(new Span
|
||||
{
|
||||
Text = AppResources.CreateAccount,
|
||||
TextColor = ThemeManager.GetResourceColor("PrimaryColor")
|
||||
});
|
||||
return fs;
|
||||
}
|
||||
}
|
||||
|
||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||
public Action StartLoginAction { get; set; }
|
||||
public Action StartRegisterAction { get; set; }
|
||||
public Action StartSsoLoginAction { get; set; }
|
||||
public Action StartEnvironmentAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public Command RememberEmailCommand { get; set; }
|
||||
public AsyncCommand ContinueCommand { get; }
|
||||
public AsyncCommand CloseCommand { get; }
|
||||
public AsyncCommand CreateAccountCommand { get; }
|
||||
public AsyncCommand ShowEnvironmentPickerCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
Email = await _stateService.GetRememberedEmailAsync();
|
||||
RememberEmail = !string.IsNullOrEmpty(Email);
|
||||
_displayEuEnvironment = await _configService.GetFeatureFlagBoolAsync(Constants.DisplayEuEnvironmentFlag, forceRefresh: true);
|
||||
}
|
||||
|
||||
public async Task ContinueToLoginStepAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Email))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (!Email.Contains("@"))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidEmail, AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
await _stateService.SetRememberedEmailAsync(RememberEmail ? Email : null);
|
||||
var userId = await _stateService.GetUserIdAsync(Email);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(userId) &&
|
||||
(await _stateService.GetEnvironmentUrlsAsync(userId))?.Base == _environmentService.BaseUrl &&
|
||||
await _stateService.IsAuthenticatedAsync(userId))
|
||||
{
|
||||
await _accountManager.PromptToSwitchToExistingAccountAsync(userId);
|
||||
return;
|
||||
}
|
||||
StartLoginAction();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ShowEnvironmentPickerAsync()
|
||||
{
|
||||
_displayEuEnvironment = await _configService.GetFeatureFlagBoolAsync(Constants.DisplayEuEnvironmentFlag);
|
||||
var options = _displayEuEnvironment
|
||||
? new string[] { LOGGING_IN_ON_US, LOGGING_IN_ON_EU, AppResources.SelfHosted }
|
||||
: new string[] { LOGGING_IN_ON_US, AppResources.SelfHosted };
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
var result = await Page.DisplayActionSheet(AppResources.LoggingInOn, AppResources.Cancel, null, options);
|
||||
|
||||
if (result is null || result == AppResources.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == AppResources.SelfHosted)
|
||||
{
|
||||
StartEnvironmentAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
await _environmentService.SetUrlsAsync(result == LOGGING_IN_ON_EU ? EnvironmentUrlData.DefaultEU : EnvironmentUrlData.DefaultUS);
|
||||
await _configService.GetAsync(true);
|
||||
SelectedEnvironmentName = result;
|
||||
});
|
||||
}
|
||||
|
||||
public async Task UpdateEnvironment()
|
||||
{
|
||||
var environmentsSaved = await _stateService.GetPreAuthEnvironmentUrlsAsync();
|
||||
if (environmentsSaved == null || environmentsSaved.IsEmpty)
|
||||
{
|
||||
await _environmentService.SetUrlsAsync(EnvironmentUrlData.DefaultUS);
|
||||
environmentsSaved = EnvironmentUrlData.DefaultUS;
|
||||
SelectedEnvironmentName = LOGGING_IN_ON_US;
|
||||
return;
|
||||
}
|
||||
|
||||
if (environmentsSaved.Base == EnvironmentUrlData.DefaultUS.Base)
|
||||
{
|
||||
SelectedEnvironmentName = LOGGING_IN_ON_US;
|
||||
}
|
||||
else if (environmentsSaved.Base == EnvironmentUrlData.DefaultEU.Base)
|
||||
{
|
||||
SelectedEnvironmentName = LOGGING_IN_ON_EU;
|
||||
}
|
||||
else
|
||||
{
|
||||
await _configService.GetAsync(true);
|
||||
SelectedEnvironmentName = AppResources.SelfHosted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
182
src/Core/Pages/Accounts/LockPage.xaml
Normal file
182
src/Core/Pages/Accounts/LockPage.xaml
Normal file
@@ -0,0 +1,182 @@
|
||||
<?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.LockPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LockPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LockPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<controls:ExtendedToolbarItem
|
||||
x:Name="_accountAvatar"
|
||||
IconImageSource="{Binding AvatarImageSource}"
|
||||
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
|
||||
Order="Primary"
|
||||
Priority="-1"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<ToolbarItem IconImageSource="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
||||
x:Name="_moreItem" x:Key="moreItem"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Options}" />
|
||||
<ToolbarItem Text="{u:I18n LogOut}"
|
||||
x:Key="logOut"
|
||||
x:Name="_logOut"
|
||||
Clicked="LogOut_Clicked"
|
||||
Order="Secondary"/>
|
||||
|
||||
<ScrollView x:Name="_mainLayout" x:Key="mainLayout">
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding PinEnabled}"
|
||||
Padding="0, 10, 0, 0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n PIN}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_pin"
|
||||
Text="{Binding Pin}"
|
||||
StyleClass="box-value"
|
||||
Keyboard="Numeric"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="PinEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="PinVisibilityToggle" />
|
||||
</Grid>
|
||||
<Grid
|
||||
x:Name="_passwordGrid"
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding PinEnabled, Converter={StaticResource inverseBool}}"
|
||||
Padding="0, 10, 0, 0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n MasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="MasterPasswordEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="PasswordVisibilityToggle"
|
||||
/>
|
||||
</Grid>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Padding="0, 10, 0, 0">
|
||||
<Label
|
||||
Text="{Binding LockedVerifyText}"
|
||||
StyleClass="box-footer-label" />
|
||||
<Label
|
||||
Text="{Binding LoggedInAsText}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0, 10, 0, 0" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 0">
|
||||
<Label
|
||||
Text="{u:I18n AccountBiometricInvalidated}"
|
||||
StyleClass="box-footer-label,text-danger,text-bold"
|
||||
IsVisible="{Binding BiometricIntegrityValid, Converter={StaticResource inverseBool}}" />
|
||||
<Button Text="{Binding BiometricButtonText}" Clicked="Biometric_Clicked"
|
||||
IsVisible="{Binding BiometricButtonVisible}">
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="_unlockButton"
|
||||
Text="{u:I18n Unlock}"
|
||||
StyleClass="btn-primary"
|
||||
Clicked="Unlock_Clicked"
|
||||
AutomationId="UnlockVaultButton" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</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>
|
||||
|
||||
<controls:AccountSwitchingOverlayView
|
||||
x:Name="_accountListOverlay"
|
||||
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
|
||||
AbsoluteLayout.LayoutFlags="All"
|
||||
MainPage="{Binding Source={x:Reference _page}}"
|
||||
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
|
||||
|
||||
</AbsoluteLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
207
src/Core/Pages/Accounts/LockPage.xaml.cs
Normal file
207
src/Core/Pages/Accounts/LockPage.xaml.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LockPage : BaseContentPage
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly AppOptions _appOptions;
|
||||
private readonly bool _autoPromptBiometric;
|
||||
private readonly LockPageViewModel _vm;
|
||||
|
||||
private bool _promptedAfterResume;
|
||||
private bool _appeared;
|
||||
|
||||
public LockPage(AppOptions appOptions = null, bool autoPromptBiometric = true, bool checkPendingAuthRequests = true)
|
||||
{
|
||||
_appOptions = appOptions;
|
||||
_autoPromptBiometric = autoPromptBiometric;
|
||||
InitializeComponent();
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
||||
_vm = BindingContext as LockPageViewModel;
|
||||
_vm.CheckPendingAuthRequests = checkPendingAuthRequests;
|
||||
_vm.Page = this;
|
||||
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
||||
|
||||
// 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)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolbarItems.Add(_logOut);
|
||||
}
|
||||
}
|
||||
|
||||
public Entry SecretEntry
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_vm?.PinEnabled ?? false)
|
||||
{
|
||||
return _pin;
|
||||
}
|
||||
return _masterPassword;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PromptBiometricAfterResumeAsync()
|
||||
{
|
||||
if (_vm.BiometricEnabled)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
if (!_promptedAfterResume)
|
||||
{
|
||||
_promptedAfterResume = true;
|
||||
await _vm?.PromptBiometricAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
_broadcasterService.Subscribe(nameof(LockPage), message =>
|
||||
{
|
||||
if (message.Command == Constants.ClearSensitiveFields)
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(_vm.ResetPinPasswordFields);
|
||||
}
|
||||
});
|
||||
if (_appeared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appeared = true;
|
||||
_mainContent.Content = _mainLayout;
|
||||
|
||||
_accountAvatar?.OnAppearing();
|
||||
|
||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
||||
|
||||
await _vm.InitAsync();
|
||||
|
||||
_vm.FocusSecretEntry += PerformFocusSecretEntry;
|
||||
|
||||
if (!_vm.BiometricEnabled)
|
||||
{
|
||||
RequestFocus(SecretEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_vm.HasMasterPassword && !_vm.PinEnabled)
|
||||
{
|
||||
_passwordGrid.IsVisible = false;
|
||||
_unlockButton.IsVisible = false;
|
||||
}
|
||||
if (_autoPromptBiometric)
|
||||
{
|
||||
var tasks = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(async () => await _vm.PromptBiometricAsync());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformFocusSecretEntry(int? cursorPosition)
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
SecretEntry.Focus();
|
||||
if (cursorPosition.HasValue)
|
||||
{
|
||||
SecretEntry.CursorPosition = cursorPosition.Value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override bool OnBackButtonPressed()
|
||||
{
|
||||
if (_accountListOverlay.IsVisible)
|
||||
{
|
||||
_accountListOverlay.HideAsync().FireAndForget();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
|
||||
_accountAvatar?.OnDisappearing();
|
||||
_broadcasterService.Unsubscribe(nameof(LockPage));
|
||||
}
|
||||
|
||||
private void Unlock_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var tasks = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(50);
|
||||
Device.BeginInvokeOnMainThread(async () => await _vm.SubmitAsync());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async void LogOut_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
await _accountListOverlay.HideAsync();
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.LogOutAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Biometric_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.PromptBiometricAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
await _accountListOverlay.HideAsync();
|
||||
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selection = await DisplayActionSheet(AppResources.Options,
|
||||
AppResources.Cancel, null, AppResources.LogOut);
|
||||
|
||||
if (selection == AppResources.LogOut)
|
||||
{
|
||||
await _vm.LogOutAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UnlockedAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
579
src/Core/Pages/Accounts/LockPageViewModel.cs
Normal file
579
src/Core/Pages/Accounts/LockPageViewModel.cs
Normal file
@@ -0,0 +1,579 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using CommunityToolkit.Maui.Converters;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using AsyncAwaitBestPractices;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LockPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IBiometricService _biometricService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IWatchDeviceService _watchDeviceService;
|
||||
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly ISyncService _syncService;
|
||||
private string _email;
|
||||
private string _masterPassword;
|
||||
private string _pin;
|
||||
private bool _showPassword;
|
||||
private PinLockType _pinStatus;
|
||||
private bool _pinEnabled;
|
||||
private bool _biometricEnabled;
|
||||
private bool _biometricIntegrityValid = true;
|
||||
private bool _biometricButtonVisible;
|
||||
private bool _hasMasterPassword;
|
||||
private string _biometricButtonText;
|
||||
private string _loggedInAsText;
|
||||
private string _lockedVerifyText;
|
||||
|
||||
public LockPageViewModel()
|
||||
{
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>();
|
||||
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||
|
||||
PageTitle = AppResources.VerifyMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
|
||||
AccountSwitchingOverlayViewModel =
|
||||
new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||
{
|
||||
AllowAddAccountRow = true,
|
||||
AllowActiveAccountSelection = true
|
||||
};
|
||||
}
|
||||
|
||||
public string MasterPassword
|
||||
{
|
||||
get => _masterPassword;
|
||||
set => SetProperty(ref _masterPassword, value);
|
||||
}
|
||||
|
||||
public string Pin
|
||||
{
|
||||
get => _pin;
|
||||
set => SetProperty(ref _pin, value);
|
||||
}
|
||||
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText),
|
||||
});
|
||||
}
|
||||
|
||||
public bool PinEnabled
|
||||
{
|
||||
get => _pinEnabled;
|
||||
set => SetProperty(ref _pinEnabled, value);
|
||||
}
|
||||
|
||||
public bool HasMasterPassword
|
||||
{
|
||||
get => _hasMasterPassword;
|
||||
}
|
||||
|
||||
public bool BiometricEnabled
|
||||
{
|
||||
get => _biometricEnabled;
|
||||
set => SetProperty(ref _biometricEnabled, value);
|
||||
}
|
||||
|
||||
public bool BiometricIntegrityValid
|
||||
{
|
||||
get => _biometricIntegrityValid;
|
||||
set => SetProperty(ref _biometricIntegrityValid, value);
|
||||
}
|
||||
|
||||
public bool BiometricButtonVisible
|
||||
{
|
||||
get => _biometricButtonVisible;
|
||||
set => SetProperty(ref _biometricButtonVisible, value);
|
||||
}
|
||||
|
||||
public string BiometricButtonText
|
||||
{
|
||||
get => _biometricButtonText;
|
||||
set => SetProperty(ref _biometricButtonText, value);
|
||||
}
|
||||
|
||||
public string LoggedInAsText
|
||||
{
|
||||
get => _loggedInAsText;
|
||||
set => SetProperty(ref _loggedInAsText, value);
|
||||
}
|
||||
|
||||
public string LockedVerifyText
|
||||
{
|
||||
get => _lockedVerifyText;
|
||||
set => SetProperty(ref _lockedVerifyText, value);
|
||||
}
|
||||
|
||||
public bool CheckPendingAuthRequests { get; set; }
|
||||
|
||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword
|
||||
? AppResources.PasswordIsVisibleTapToHide
|
||||
: AppResources.PasswordIsNotVisibleTapToShow;
|
||||
|
||||
public Action UnlockedAction { get; set; }
|
||||
public event Action<int?> FocusSecretEntry
|
||||
{
|
||||
add => _secretEntryFocusWeakEventManager.AddEventHandler(value);
|
||||
remove => _secretEntryFocusWeakEventManager.RemoveEventHandler(value);
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||
if (pendingRequest != null && CheckPendingAuthRequests)
|
||||
{
|
||||
await _vaultTimeoutService.LogOutAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_pinStatus = await _vaultTimeoutService.GetPinLockTypeAsync();
|
||||
|
||||
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
|
||||
?? await _stateService.GetPinProtectedKeyAsync();
|
||||
PinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) ||
|
||||
_pinStatus == PinLockType.Persistent;
|
||||
|
||||
BiometricEnabled = await IsBiometricsEnabledAsync();
|
||||
|
||||
// Users without MP and without biometric or pin has no MP to unlock with
|
||||
_hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync();
|
||||
if (await _stateService.IsAuthenticatedAsync()
|
||||
&& !_hasMasterPassword
|
||||
&& !BiometricEnabled
|
||||
&& !PinEnabled)
|
||||
{
|
||||
await _vaultTimeoutService.LogOutAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_email = await _stateService.GetEmailAsync();
|
||||
if (string.IsNullOrWhiteSpace(_email))
|
||||
{
|
||||
await _vaultTimeoutService.LogOutAsync();
|
||||
_logger.Exception(new NullReferenceException("Email not found in storage"));
|
||||
return;
|
||||
}
|
||||
var webVault = _environmentService.GetWebVaultUrl(true);
|
||||
if (string.IsNullOrWhiteSpace(webVault))
|
||||
{
|
||||
webVault = "https://bitwarden.com";
|
||||
}
|
||||
var webVaultHostname = CoreHelpers.GetHostname(webVault);
|
||||
LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, webVaultHostname);
|
||||
if (PinEnabled)
|
||||
{
|
||||
PageTitle = AppResources.VerifyPIN;
|
||||
LockedVerifyText = AppResources.VaultLockedPIN;
|
||||
}
|
||||
else
|
||||
{
|
||||
PageTitle = _hasMasterPassword ? AppResources.VerifyMasterPassword : AppResources.UnlockVault;
|
||||
LockedVerifyText = _hasMasterPassword
|
||||
? AppResources.VaultLockedMasterPassword
|
||||
: AppResources.VaultLockedIdentity;
|
||||
}
|
||||
|
||||
if (BiometricEnabled)
|
||||
{
|
||||
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
||||
if (!_biometricIntegrityValid)
|
||||
{
|
||||
BiometricButtonVisible = false;
|
||||
return;
|
||||
}
|
||||
BiometricButtonVisible = true;
|
||||
BiometricButtonText = AppResources.UseBiometricsToUnlock;
|
||||
// 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)
|
||||
{
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
|
||||
AppResources.UseFingerprintToUnlock;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
ShowPassword = false;
|
||||
try
|
||||
{
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
if (PinEnabled)
|
||||
{
|
||||
await UnlockWithPinAsync(kdfConfig);
|
||||
}
|
||||
else
|
||||
{
|
||||
await UnlockWithMasterPasswordAsync(kdfConfig);
|
||||
}
|
||||
|
||||
}
|
||||
catch (LegacyUserException)
|
||||
{
|
||||
await HandleLegacyUserAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UnlockWithPinAsync(KdfConfig kdfConfig)
|
||||
{
|
||||
if (PinEnabled && string.IsNullOrWhiteSpace(Pin))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.PIN),
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
var failed = true;
|
||||
try
|
||||
{
|
||||
EncString userKeyPin;
|
||||
EncString oldPinProtected;
|
||||
switch (_pinStatus)
|
||||
{
|
||||
case PinLockType.Persistent:
|
||||
{
|
||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
|
||||
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
|
||||
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
|
||||
break;
|
||||
}
|
||||
case PinLockType.Transient:
|
||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
|
||||
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
|
||||
break;
|
||||
case PinLockType.Disabled:
|
||||
default:
|
||||
throw new Exception("Pin is disabled");
|
||||
}
|
||||
|
||||
UserKey userKey;
|
||||
if (oldPinProtected != null)
|
||||
{
|
||||
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
|
||||
_pinStatus == PinLockType.Transient,
|
||||
Pin,
|
||||
_email,
|
||||
kdfConfig,
|
||||
oldPinProtected
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
|
||||
Pin,
|
||||
_email,
|
||||
kdfConfig,
|
||||
userKeyPin
|
||||
);
|
||||
}
|
||||
|
||||
var protectedPin = await _stateService.GetProtectedPinAsync();
|
||||
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
|
||||
failed = decryptedPin != Pin;
|
||||
if (!failed)
|
||||
{
|
||||
Pin = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
}
|
||||
}
|
||||
catch (LegacyUserException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
if (failed)
|
||||
{
|
||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||
if (invalidUnlockAttempts >= 5)
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
return;
|
||||
}
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UnlockWithMasterPasswordAsync(KdfConfig kdfConfig)
|
||||
{
|
||||
if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig);
|
||||
if (await _cryptoService.IsLegacyUserAsync(masterKey))
|
||||
{
|
||||
throw new LegacyUserException();
|
||||
}
|
||||
|
||||
var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync();
|
||||
var passwordValid = false;
|
||||
MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null;
|
||||
|
||||
if (storedKeyHash != null)
|
||||
{
|
||||
// Offline unlock possible
|
||||
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Online unlock required
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey,
|
||||
HashPurpose.ServerAuthorization);
|
||||
var request = new PasswordVerificationRequest();
|
||||
request.MasterPasswordHash = keyHash;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
enforcedMasterPasswordOptions = response.MasterPasswordPolicy;
|
||||
passwordValid = true;
|
||||
var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey,
|
||||
HashPurpose.LocalAuthorization);
|
||||
await _cryptoService.SetMasterKeyHashAsync(localKeyHash);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
|
||||
if (passwordValid)
|
||||
{
|
||||
if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions))
|
||||
{
|
||||
// Save the ForcePasswordResetReason to force a password reset after unlock
|
||||
await _stateService.SetForcePasswordResetReasonAsync(
|
||||
ForcePasswordResetReason.WeakMasterPasswordOnLogin);
|
||||
}
|
||||
|
||||
MasterPassword = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
|
||||
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
||||
await _cryptoService.SetMasterKeyAsync(masterKey);
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
|
||||
// Re-enable biometrics
|
||||
if (BiometricEnabled & !BiometricIntegrityValid)
|
||||
{
|
||||
await _biometricService.SetupBiometricAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||
if (invalidUnlockAttempts >= 5)
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
return;
|
||||
}
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the master password requires updating to meet the enforced policy requirements
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
private async Task<bool> RequirePasswordChangeAsync(MasterPasswordPolicyOptions options = null)
|
||||
{
|
||||
// If no policy options are provided, attempt to load them from the policy service
|
||||
var enforcedOptions = options ?? await _policyService.GetMasterPasswordPolicyOptions();
|
||||
|
||||
// No policy to enforce on login/unlock
|
||||
if (!(enforcedOptions is { EnforceOnLogin: true }))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var strength = _passwordGenerationService.PasswordStrength(
|
||||
MasterPassword, _passwordGenerationService.GetPasswordStrengthUserInput(_email))?.Score;
|
||||
|
||||
if (!strength.HasValue)
|
||||
{
|
||||
_logger.Error("Unable to evaluate master password strength during unlock");
|
||||
return false;
|
||||
}
|
||||
|
||||
return !await _policyService.EvaluateMasterPassword(
|
||||
strength.Value,
|
||||
MasterPassword,
|
||||
enforcedOptions
|
||||
);
|
||||
}
|
||||
|
||||
public async Task LogOutAsync()
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation,
|
||||
AppResources.LogOut, AppResources.Yes, AppResources.Cancel);
|
||||
if (confirmed)
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetPinPasswordFields()
|
||||
{
|
||||
try
|
||||
{
|
||||
MasterPassword = string.Empty;
|
||||
Pin = string.Empty;
|
||||
ShowPassword = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
var secret = PinEnabled ? Pin : MasterPassword;
|
||||
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length,
|
||||
nameof(FocusSecretEntry));
|
||||
}
|
||||
|
||||
public async Task PromptBiometricAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
|
||||
BiometricButtonVisible = BiometricIntegrityValid;
|
||||
if (!BiometricEnabled || !BiometricIntegrityValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)),
|
||||
!PinEnabled && !HasMasterPassword);
|
||||
|
||||
await _stateService.SetBiometricLockedAsync(!success);
|
||||
if (success)
|
||||
{
|
||||
var userKey = await _cryptoService.GetBiometricUnlockKeyAsync();
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
}
|
||||
}
|
||||
catch (LegacyUserException)
|
||||
{
|
||||
await HandleLegacyUserAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetUserKeyAndContinueAsync(UserKey key)
|
||||
{
|
||||
var hasKey = await _cryptoService.HasUserKeyAsync();
|
||||
if (!hasKey)
|
||||
{
|
||||
await _cryptoService.SetUserKeyAsync(key);
|
||||
}
|
||||
await _deviceTrustCryptoService.TrustDeviceIfNeededAsync();
|
||||
await DoContinueAsync();
|
||||
}
|
||||
|
||||
private async Task DoContinueAsync()
|
||||
{
|
||||
_syncService.FullSyncAsync(false).FireAndForget();
|
||||
await _stateService.SetBiometricLockedAsync(false);
|
||||
_watchDeviceService.SyncDataToWatchAsync().FireAndForget();
|
||||
_messagingService.Send("unlocked");
|
||||
UnlockedAction?.Invoke();
|
||||
}
|
||||
|
||||
private async Task<bool> IsBiometricsEnabledAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _vaultTimeoutService.IsBiometricLockSetAsync() &&
|
||||
await _biometricService.CanUseBiometricsUnlockAsync();
|
||||
}
|
||||
catch (LegacyUserException)
|
||||
{
|
||||
await HandleLegacyUserAsync();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task HandleLegacyUserAsync()
|
||||
{
|
||||
// Legacy users must migrate on web vault.
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong,
|
||||
AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
await _vaultTimeoutService.LogOutAsync();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
76
src/Core/Pages/Accounts/LoginApproveDevicePage.xaml
Normal file
76
src/Core/Pages/Accounts/LoginApproveDevicePage.xaml
Normal file
@@ -0,0 +1,76 @@
|
||||
<?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.LoginApproveDevicePage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginApproveDeviceViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginApproveDeviceViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<StackLayout Padding="10, 10">
|
||||
<StackLayout Padding="5, 10" Orientation="Horizontal">
|
||||
<StackLayout HorizontalOptions="FillAndExpand">
|
||||
<Label
|
||||
StyleClass="text-md"
|
||||
Text="{u:I18n RememberThisDevice}"/>
|
||||
<Label
|
||||
StyleClass="box-sub-label"
|
||||
Text="{u:I18n TurnOffUsingPublicDevice}"/>
|
||||
</StackLayout>
|
||||
<Switch
|
||||
Scale="0.8"
|
||||
IsToggled="{Binding RememberThisDevice}"
|
||||
VerticalOptions="Center"/>
|
||||
</StackLayout>
|
||||
<StackLayout Margin="0, 20, 0, 0">
|
||||
<Button
|
||||
x:Name="_continue"
|
||||
Text="{u:I18n Continue}"
|
||||
StyleClass="btn-primary"
|
||||
Command="{Binding ContinueCommand}"
|
||||
IsVisible="{Binding IsNewUser}"/>
|
||||
<Button
|
||||
x:Name="_approveWithMyOtherDevice"
|
||||
Text="{u:I18n ApproveWithMyOtherDevice}"
|
||||
StyleClass="btn-primary"
|
||||
Command="{Binding ApproveWithMyOtherDeviceCommand}"
|
||||
IsVisible="{Binding ApproveWithMyOtherDeviceEnabled}"/>
|
||||
<Button
|
||||
x:Name="_requestAdminApproval"
|
||||
Text="{u:I18n RequestAdminApproval}"
|
||||
StyleClass="box-button-row"
|
||||
Command="{Binding RequestAdminApprovalCommand}"
|
||||
IsVisible="{Binding RequestAdminApprovalEnabled}"/>
|
||||
<Button
|
||||
x:Name="_approveWithMasterPassword"
|
||||
Text="{u:I18n ApproveWithMasterPassword}"
|
||||
StyleClass="box-button-row"
|
||||
Command="{Binding ApproveWithMasterPasswordCommand}"
|
||||
IsVisible="{Binding ApproveWithMasterPasswordEnabled}"/>
|
||||
<Label
|
||||
Text="{Binding LoggingInAsText}"
|
||||
StyleClass="text-sm"
|
||||
Margin="0,40,0,0"
|
||||
AutomationId="LoggingInAsLabel"
|
||||
/>
|
||||
<Label
|
||||
Text="{u:I18n NotYou}"
|
||||
StyleClass="text-md"
|
||||
HorizontalOptions="Start"
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationId="NotYouLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding LogoutCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
||||
|
||||
|
||||
65
src/Core/Pages/Accounts/LoginApproveDevicePage.xaml.cs
Normal file
65
src/Core/Pages/Accounts/LoginApproveDevicePage.xaml.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginApproveDevicePage : BaseContentPage
|
||||
{
|
||||
|
||||
private readonly LoginApproveDeviceViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
public LoginApproveDevicePage(AppOptions appOptions = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginApproveDeviceViewModel;
|
||||
_vm.LogInWithMasterPasswordAction = () => StartLogInWithMasterPasswordAsync().FireAndForget();
|
||||
_vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget();
|
||||
_vm.RequestAdminApprovalAction = () => RequestAdminApprovalAsync().FireAndForget();
|
||||
_vm.ContinueToVaultAction = () => ContinueToVaultAsync().FireAndForget();
|
||||
_vm.Page = this;
|
||||
_appOptions = appOptions;
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
_vm.InitAsync();
|
||||
}
|
||||
|
||||
private async Task ContinueToVaultAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
|
||||
private async Task StartLogInWithMasterPasswordAsync()
|
||||
{
|
||||
var page = new LockPage(_appOptions, checkPendingAuthRequests: false);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartLoginWithDeviceAsync()
|
||||
{
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions, true);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task RequestAdminApprovalAsync()
|
||||
{
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AdminApproval, _appOptions, true);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
151
src/Core/Pages/Accounts/LoginApproveDeviceViewModel.cs
Normal file
151
src/Core/Pages/Accounts/LoginApproveDeviceViewModel.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities.AccountManagement;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginApproveDeviceViewModel : BaseViewModel
|
||||
{
|
||||
private bool _rememberThisDevice;
|
||||
private bool _approveWithMyOtherDeviceEnabled;
|
||||
private bool _requestAdminApprovalEnabled;
|
||||
private bool _approveWithMasterPasswordEnabled;
|
||||
private string _email;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IApiService _apiService;
|
||||
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
|
||||
public ICommand ApproveWithMyOtherDeviceCommand { get; }
|
||||
public ICommand RequestAdminApprovalCommand { get; }
|
||||
public ICommand ApproveWithMasterPasswordCommand { get; }
|
||||
public ICommand ContinueCommand { get; }
|
||||
public ICommand LogoutCommand { get; }
|
||||
|
||||
public Action LogInWithMasterPasswordAction { get; set; }
|
||||
public Action LogInWithDeviceAction { get; set; }
|
||||
public Action RequestAdminApprovalAction { get; set; }
|
||||
public Action ContinueToVaultAction { get; set; }
|
||||
|
||||
public LoginApproveDeviceViewModel()
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_apiService = ServiceContainer.Resolve<IApiService>();
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_authService = ServiceContainer.Resolve<IAuthService>();
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>();
|
||||
|
||||
PageTitle = AppResources.LogInInitiated;
|
||||
RememberThisDevice = true;
|
||||
|
||||
ApproveWithMyOtherDeviceCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithDeviceAction),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
RequestAdminApprovalCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(RequestAdminApprovalAction),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
ApproveWithMasterPasswordCommand = new AsyncCommand(() => SetDeviceTrustAndInvokeAsync(LogInWithMasterPasswordAction),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
ContinueCommand = new AsyncCommand(CreateNewSsoUserAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
LogoutCommand = new Command(() => _messagingService.Send(AccountsManagerMessageCommands.LOGOUT));
|
||||
}
|
||||
|
||||
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
|
||||
|
||||
public bool RememberThisDevice
|
||||
{
|
||||
get => _rememberThisDevice;
|
||||
set => SetProperty(ref _rememberThisDevice, value);
|
||||
}
|
||||
|
||||
public bool ApproveWithMyOtherDeviceEnabled
|
||||
{
|
||||
get => _approveWithMyOtherDeviceEnabled;
|
||||
set => SetProperty(ref _approveWithMyOtherDeviceEnabled, value);
|
||||
}
|
||||
|
||||
public bool RequestAdminApprovalEnabled
|
||||
{
|
||||
get => _requestAdminApprovalEnabled;
|
||||
set => SetProperty(ref _requestAdminApprovalEnabled, value,
|
||||
additionalPropertyNames: new[] { nameof(IsNewUser) });
|
||||
}
|
||||
|
||||
public bool ApproveWithMasterPasswordEnabled
|
||||
{
|
||||
get => _approveWithMasterPasswordEnabled;
|
||||
set => SetProperty(ref _approveWithMasterPasswordEnabled, value,
|
||||
additionalPropertyNames: new[] { nameof(IsNewUser) });
|
||||
}
|
||||
|
||||
public bool IsNewUser => !RequestAdminApprovalEnabled && !ApproveWithMasterPasswordEnabled;
|
||||
|
||||
public string Email
|
||||
{
|
||||
get => _email;
|
||||
set => SetProperty(ref _email, value, additionalPropertyNames:
|
||||
new string[] {
|
||||
nameof(LoggingInAsText)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Email = await _stateService.GetActiveUserEmailAsync();
|
||||
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
RequestAdminApprovalEnabled = decryptOptions?.TrustedDeviceOption?.HasAdminApproval ?? false;
|
||||
ApproveWithMasterPasswordEnabled = decryptOptions?.HasMasterPassword ?? false;
|
||||
ApproveWithMyOtherDeviceEnabled = decryptOptions?.TrustedDeviceOption?.HasLoginApprovingDevice ?? false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateNewSsoUserAsync()
|
||||
{
|
||||
await _authService.CreateNewSsoUserAsync(await _stateService.GetRememberedOrgIdentifierAsync());
|
||||
if (RememberThisDevice)
|
||||
{
|
||||
await _deviceTrustCryptoService.TrustDeviceAsync();
|
||||
}
|
||||
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
await Device.InvokeOnMainThreadAsync(ContinueToVaultAction);
|
||||
}
|
||||
|
||||
private async Task SetDeviceTrustAndInvokeAsync(Action action)
|
||||
{
|
||||
await _deviceTrustCryptoService.SetShouldTrustDeviceAsync(RememberThisDevice);
|
||||
await Device.InvokeOnMainThreadAsync(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
175
src/Core/Pages/Accounts/LoginPage.xaml
Normal file
175
src/Core/Pages/Accounts/LoginPage.xaml
Normal file
@@ -0,0 +1,175 @@
|
||||
<?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.LoginPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:core="clr-namespace:Bit.Core"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}"
|
||||
AutomationId="PageTitleLabel">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
<ContentPage.ToolbarItems>
|
||||
<controls:ExtendedToolbarItem
|
||||
x:Name="_accountAvatar"
|
||||
IconImageSource="{Binding AvatarImageSource}"
|
||||
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
|
||||
Order="Primary"
|
||||
Priority="-1"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<ToolbarItem IconImageSource="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
||||
x:Name="_moreItem" x:Key="moreItem"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Options}"
|
||||
AutomationId="OptionsButton" />
|
||||
<ToolbarItem Text="{u:I18n GetPasswordHint}"
|
||||
x:Key="getPasswordHint"
|
||||
x:Name="_getPasswordHint"
|
||||
Clicked="Hint_Clicked"
|
||||
Order="Secondary" />
|
||||
<ToolbarItem Text="{u:I18n RemoveAccount}"
|
||||
x:Key="removeAccount"
|
||||
x:Name="_removeAccount"
|
||||
Clicked="RemoveAccount_Clicked"
|
||||
Order="Secondary" />
|
||||
|
||||
<ScrollView x:Name="_mainLayout" x:Key="mainLayout">
|
||||
<StackLayout Spacing="0">
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n MasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Padding="0, 10, 0, 0"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding LogInCommand}"
|
||||
AutomationId="MasterPasswordEntry"
|
||||
/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationId="PasswordVisibilityToggle"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||
<Label
|
||||
Text="{u:I18n GetMasterPasswordwordHint}"
|
||||
StyleClass="box-footer-label"
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
Padding="0,5,0,0"
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
AutomationId="GetMasterPasswordHintLabel"
|
||||
>
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="Hint_Clicked" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 10">
|
||||
<Button x:Name="_loginWithMasterPassword"
|
||||
Text="{u:I18n LogInWithMasterPassword}"
|
||||
StyleClass="btn-primary"
|
||||
Clicked="LogIn_Clicked"
|
||||
AutomationId="LogInWithMasterPasswordButton"
|
||||
/>
|
||||
<controls:IconLabelButton
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
|
||||
Label="{u:I18n LogInWithAnotherDevice}"
|
||||
ButtonCommand="{Binding LogInWithDeviceCommand}"
|
||||
IsVisible="{Binding IsKnownDevice}"
|
||||
AutomationId="LogInWithAnotherDeviceButton"
|
||||
/>
|
||||
<controls:IconLabelButton
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Suitcase}}"
|
||||
Label="{u:I18n LogInSso}"
|
||||
AutomationId="LogInWithSsoButton">
|
||||
<controls:IconLabelButton.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="LogInSSO_Clicked" />
|
||||
</controls:IconLabelButton.GestureRecognizers>
|
||||
</controls:IconLabelButton>
|
||||
<Label
|
||||
Text="{Binding LoggingInAsText}"
|
||||
StyleClass="text-sm"
|
||||
Margin="0,40,0,0"
|
||||
AutomationId="LoggingInAsLabel"
|
||||
/>
|
||||
<Label
|
||||
Text="{u:I18n NotYou}"
|
||||
StyleClass="text-md"
|
||||
HorizontalOptions="Start"
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationId="NotYouLabel">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="Cancel_Clicked" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</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>
|
||||
|
||||
<controls:AccountSwitchingOverlayView
|
||||
x:Name="_accountListOverlay"
|
||||
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
|
||||
AbsoluteLayout.LayoutFlags="All"
|
||||
MainPage="{Binding Source={x:Reference _page}}"
|
||||
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
|
||||
</AbsoluteLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
212
src/Core/Pages/Accounts/LoginPage.xaml.cs
Normal file
212
src/Core/Pages/Accounts/LoginPage.xaml.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginPage : BaseContentPage
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly LoginPageViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
private bool _inputFocused;
|
||||
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
public LoginPage(string email = null, AppOptions appOptions = null)
|
||||
{
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
||||
_vm = BindingContext as LoginPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||
_vm.LogInWithDeviceAction = () => StartLoginWithDeviceAsync().FireAndForget();
|
||||
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
||||
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
await _accountListOverlay.HideAsync();
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
_vm.IsEmailEnabled = string.IsNullOrWhiteSpace(email);
|
||||
_vm.IsIosExtension = _appOptions?.IosExtension ?? false;
|
||||
|
||||
if (_vm.IsEmailEnabled)
|
||||
{
|
||||
_vm.ShowCancelButton = true;
|
||||
}
|
||||
_vm.Email = email;
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
|
||||
// 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)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolbarItems.Add(_getPasswordHint);
|
||||
}
|
||||
|
||||
if (_appOptions?.IosExtension ?? false)
|
||||
{
|
||||
_vm.ShowCancelButton = true;
|
||||
}
|
||||
|
||||
if (_appOptions?.HideAccountSwitcher ?? false)
|
||||
{
|
||||
ToolbarItems.Remove(_accountAvatar);
|
||||
}
|
||||
}
|
||||
|
||||
public Entry MasterPasswordEntry { get; set; }
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
_broadcasterService.Subscribe(nameof(LoginPage), message =>
|
||||
{
|
||||
if (message.Command == Constants.ClearSensitiveFields)
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(_vm.ResetPasswordField);
|
||||
}
|
||||
});
|
||||
_mainContent.Content = _mainLayout;
|
||||
_accountAvatar?.OnAppearing();
|
||||
|
||||
await _vm.InitAsync();
|
||||
if (!_appOptions?.HideAccountSwitcher ?? false)
|
||||
{
|
||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync(_vm.EmailIsInSavedAccounts);
|
||||
}
|
||||
if (!_inputFocused)
|
||||
{
|
||||
RequestFocus(_masterPassword);
|
||||
_inputFocused = true;
|
||||
}
|
||||
// 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 && !_vm.CanRemoveAccount)
|
||||
{
|
||||
ToolbarItems.Add(_removeAccount);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnBackButtonPressed()
|
||||
{
|
||||
if (_accountListOverlay.IsVisible)
|
||||
{
|
||||
_accountListOverlay.HideAsync().FireAndForget();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
|
||||
_accountAvatar?.OnDisappearing();
|
||||
_broadcasterService.Unsubscribe(nameof(LoginPage));
|
||||
}
|
||||
|
||||
private async void LogIn_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.LogInAsync(true, _vm.IsEmailEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void LogInSSO_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.StartSsoLoginAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartLoginWithDeviceAsync()
|
||||
{
|
||||
var page = new LoginPasswordlessRequestPage(_vm.Email, AuthRequestType.AuthenticateAndUnlock, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartSsoLoginAsync()
|
||||
{
|
||||
var page = new LoginSsoPage(_appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private void Hint_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.ShowMasterPasswordHintAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
|
||||
private async void RemoveAccount_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
await _accountListOverlay.HideAsync();
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.RemoveAccountAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _accountListOverlay.HideAsync();
|
||||
_vm.MoreCommand.Execute(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartTwoFactorAsync()
|
||||
{
|
||||
var page = new TwoFactorPage(false, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task LogInSuccessAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
|
||||
private async Task UpdateTempPasswordAsync()
|
||||
{
|
||||
var page = new UpdateTempPasswordPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
359
src/Core/Pages/Accounts/LoginPageViewModel.cs
Normal file
359
src/Core/Pages/Accounts/LoginPageViewModel.cs
Normal file
@@ -0,0 +1,359 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Models;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.App.Utilities.AccountManagement;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginPageViewModel : CaptchaProtectedViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IAppIdService _appIdService;
|
||||
private readonly IAccountsManager _accountManager;
|
||||
private bool _showPassword;
|
||||
private bool _showCancelButton;
|
||||
private string _email;
|
||||
private string _masterPassword;
|
||||
private bool _isEmailEnabled;
|
||||
private bool _isKnownDevice;
|
||||
private bool _isExecutingLogin;
|
||||
private string _environmentHostName;
|
||||
|
||||
public LoginPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>();
|
||||
_appIdService = ServiceContainer.Resolve<IAppIdService>();
|
||||
_accountManager = ServiceContainer.Resolve<IAccountsManager>();
|
||||
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
LogInCommand = new Command(async () => await LogInAsync());
|
||||
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||
LogInWithDeviceCommand = new AsyncCommand(() => Device.InvokeOnMainThreadAsync(LogInWithDeviceAction), onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||
|
||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||
{
|
||||
AllowAddAccountRow = true,
|
||||
AllowActiveAccountSelection = true
|
||||
};
|
||||
}
|
||||
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText)
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShowCancelButton
|
||||
{
|
||||
get => _showCancelButton;
|
||||
set => SetProperty(ref _showCancelButton, value);
|
||||
}
|
||||
|
||||
public string Email
|
||||
{
|
||||
get => _email;
|
||||
set => SetProperty(ref _email, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(LoggingInAsText)
|
||||
});
|
||||
}
|
||||
|
||||
public string MasterPassword
|
||||
{
|
||||
get => _masterPassword;
|
||||
set => SetProperty(ref _masterPassword, value);
|
||||
}
|
||||
|
||||
public bool IsEmailEnabled
|
||||
{
|
||||
get => _isEmailEnabled;
|
||||
set => SetProperty(ref _isEmailEnabled, value);
|
||||
}
|
||||
|
||||
public bool IsKnownDevice
|
||||
{
|
||||
get => _isKnownDevice;
|
||||
set => SetProperty(ref _isKnownDevice, value);
|
||||
}
|
||||
|
||||
public string EnvironmentDomainName
|
||||
{
|
||||
get => _environmentHostName;
|
||||
set => SetProperty(ref _environmentHostName, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(LoggingInAsText)
|
||||
});
|
||||
}
|
||||
|
||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||
public Command LogInCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public ICommand MoreCommand { get; internal set; }
|
||||
public ICommand LogInWithDeviceCommand { get; }
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string LoggingInAsText => string.Format(AppResources.LoggingInAsXOnY, Email, EnvironmentDomainName);
|
||||
public bool IsIosExtension { get; set; }
|
||||
public bool CanRemoveAccount { get; set; }
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action LogInSuccessAction { get; set; }
|
||||
public Action LogInWithDeviceAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
public Action StartSsoLoginAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public bool EmailIsInSavedAccounts => _stateService.AccountViews != null && _stateService.AccountViews.Any(e => e.Email == Email);
|
||||
|
||||
protected override II18nService i18nService => _i18nService;
|
||||
protected override IEnvironmentService environmentService => _environmentService;
|
||||
protected override IDeviceActionService deviceActionService => _deviceActionService;
|
||||
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
await _stateService.SetPreLoginEmailAsync(Email);
|
||||
await AccountSwitchingOverlayViewModel.RefreshAccountViewsAsync();
|
||||
if (string.IsNullOrWhiteSpace(Email))
|
||||
{
|
||||
Email = await _stateService.GetRememberedEmailAsync();
|
||||
}
|
||||
CanRemoveAccount = await _stateService.GetActiveUserEmailAsync() != Email;
|
||||
EnvironmentDomainName = CoreHelpers.GetDomain((await _stateService.GetPreAuthEnvironmentUrlsAsync())?.Base);
|
||||
IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, await _appIdService.GetAppIdAsync());
|
||||
}
|
||||
catch (ApiException apiEx) when (apiEx.Error.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
_logger.Exception(apiEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogInAsync(bool showLoading = true, bool checkForExistingAccount = false)
|
||||
{
|
||||
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Email))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (!Email.Contains("@"))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidEmail, AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
ShowPassword = false;
|
||||
try
|
||||
{
|
||||
_isExecutingLogin = true;
|
||||
if (checkForExistingAccount)
|
||||
{
|
||||
var userId = await _stateService.GetUserIdAsync(Email);
|
||||
if (!string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
var userEnvUrls = await _stateService.GetEnvironmentUrlsAsync(userId);
|
||||
if (userEnvUrls?.Base == _environmentService.BaseUrl)
|
||||
{
|
||||
await _accountManager.PromptToSwitchToExistingAccountAsync(userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showLoading)
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
||||
}
|
||||
|
||||
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
|
||||
if (response.CaptchaNeeded)
|
||||
{
|
||||
if (await HandleCaptchaAsync(response.CaptchaSiteKey))
|
||||
{
|
||||
await LogInAsync(false);
|
||||
_captchaToken = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
MasterPassword = string.Empty;
|
||||
_captchaToken = null;
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
if (response.RequiresEncryptionKeyMigration)
|
||||
{
|
||||
// Legacy users must migrate on web vault.
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong, AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.TwoFactor)
|
||||
{
|
||||
StartTwoFactorAction?.Invoke();
|
||||
}
|
||||
else if (response.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
||||
LogInSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
_captchaToken = null;
|
||||
MasterPassword = string.Empty;
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecutingLogin = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetPasswordField()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_isExecutingLogin)
|
||||
{
|
||||
MasterPassword = string.Empty;
|
||||
ShowPassword = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MoreAsync()
|
||||
{
|
||||
var buttons = IsEmailEnabled || CanRemoveAccount
|
||||
? new[] { AppResources.GetPasswordHint }
|
||||
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
|
||||
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, buttons);
|
||||
|
||||
if (selection == AppResources.GetPasswordHint)
|
||||
{
|
||||
await ShowMasterPasswordHintAsync();
|
||||
}
|
||||
else if (selection == AppResources.RemoveAccount)
|
||||
{
|
||||
await RemoveAccountAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ShowMasterPasswordHintAsync()
|
||||
{
|
||||
var hintNavigationPage = new NavigationPage(new HintPage(Email));
|
||||
if (IsIosExtension)
|
||||
{
|
||||
ThemeManager.ApplyResourcesTo(hintNavigationPage);
|
||||
}
|
||||
await Page.Navigation.PushModalAsync(hintNavigationPage);
|
||||
}
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
var entry = (Page as LoginPage).MasterPasswordEntry;
|
||||
entry.Focus();
|
||||
entry.CursorPosition = String.IsNullOrEmpty(MasterPassword) ? 0 : MasterPassword.Length;
|
||||
}
|
||||
|
||||
public async Task RemoveAccountAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.RemoveAccountConfirmation,
|
||||
AppResources.RemoveAccount, AppResources.Yes, AppResources.Cancel);
|
||||
if (confirmed)
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Exception(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/Core/Pages/Accounts/LoginPasswordlessPage.xaml
Normal file
92
src/Core/Pages/Accounts/LoginPasswordlessPage.xaml
Normal file
@@ -0,0 +1,92 @@
|
||||
<?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.LoginPasswordlessPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginPasswordlessViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginPasswordlessViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
<StackLayout
|
||||
Padding="7, 0, 7, 20">
|
||||
<ScrollView
|
||||
VerticalOptions="FillAndExpand">
|
||||
<StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n AreYouTryingToLogIn}"
|
||||
FontSize="Title"
|
||||
FontAttributes="Bold"
|
||||
Margin="0,14,0,21"/>
|
||||
<Label
|
||||
Text="{Binding LogInAttemptByLabel}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,24"
|
||||
AutomationId="LogInAttemptByLabel" />
|
||||
<Label
|
||||
Text="{u:I18n FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold"/>
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding LoginRequest.FingerprintPhrase}"
|
||||
FontSize="Medium"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"
|
||||
Margin="0,0,0,27"
|
||||
AutomationId="FingerprintValueLabel" />
|
||||
<Label
|
||||
Text="{u:I18n DeviceType}"
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold"/>
|
||||
<Label
|
||||
Text="{Binding LoginRequest.DeviceType}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,21"
|
||||
AutomationId="DeviceTypeValueLabel" />
|
||||
<Label
|
||||
Text="{u:I18n IpAddress}"
|
||||
IsVisible="{Binding ShowIpAddress}"
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold"/>
|
||||
<Label
|
||||
Text="{Binding LoginRequest.IpAddress}"
|
||||
IsVisible="{Binding ShowIpAddress}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,21"
|
||||
AutomationId="IpAddressValueLabel" />
|
||||
<Label
|
||||
Text="{u:I18n Time}"
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold"/>
|
||||
<Label
|
||||
Text="{Binding TimeOfRequestText}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,57"
|
||||
AutomationId="TimeOfRequestValueLabel" />
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
Text="{u:I18n ConfirmLogIn}"
|
||||
Command="{Binding AcceptRequestCommand}"
|
||||
Margin="0,0,0,17"
|
||||
StyleClass="btn-primary"
|
||||
AutomationId="ConfirmLoginButton" />
|
||||
<Button
|
||||
Text="{u:I18n DenyLogIn}"
|
||||
Command="{Binding RejectRequestCommand}"
|
||||
StyleClass="btn-secundary"
|
||||
AutomationId="DenyLoginButton" />
|
||||
|
||||
</StackLayout>
|
||||
</pages:BaseContentPage>
|
||||
41
src/Core/Pages/Accounts/LoginPasswordlessPage.xaml.cs
Normal file
41
src/Core/Pages/Accounts/LoginPasswordlessPage.xaml.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginPasswordlessPage : BaseContentPage
|
||||
{
|
||||
private LoginPasswordlessViewModel _vm;
|
||||
|
||||
public LoginPasswordlessPage(LoginPasswordlessDetails loginPasswordlessDetails)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginPasswordlessViewModel;
|
||||
_vm.Page = this;
|
||||
|
||||
_vm.LoginRequest = loginPasswordlessDetails;
|
||||
|
||||
ToolbarItems.Add(_closeItem);
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
_vm.StartRequestTimeUpdater();
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_vm.StopRequestTimeUpdater();
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml
Normal file
78
src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml
Normal file
@@ -0,0 +1,78 @@
|
||||
<?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.LoginPasswordlessRequestPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginPasswordlessRequestViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginPasswordlessRequestViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1" x:Name="_closeItem"/>
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout
|
||||
Padding="7, 0, 7, 20">
|
||||
<Label
|
||||
Text="{Binding Title}"
|
||||
FontSize="Title"
|
||||
FontAttributes="Bold"
|
||||
Margin="0,14,0,21"
|
||||
AutomationId="LogInInitiatedLabel" />
|
||||
<Label
|
||||
Text="{Binding SubTitle}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,10"/>
|
||||
<Label
|
||||
Text="{Binding Description}"
|
||||
FontSize="Small"
|
||||
Margin="0,0,0,24"/>
|
||||
<Label
|
||||
Text="{u:I18n FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
FontAttributes="Bold"/>
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
TextColor="{DynamicResource FingerprintPhrase}"
|
||||
AutomationId="FingerprintPhraseValue" />
|
||||
<Label
|
||||
Text="{u:I18n ResendNotification}"
|
||||
IsVisible="{Binding ResendNotificationVisible}"
|
||||
StyleClass="text-sm"
|
||||
FontAttributes="Bold"
|
||||
HorizontalOptions="Start"
|
||||
Margin="0,24,0,0"
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationId="ResendNotificationButton">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding CreatePasswordlessLoginCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
<BoxView
|
||||
HeightRequest="1"
|
||||
Margin="0,24,0,24"
|
||||
Color="{DynamicResource DisabledIconColor}" />
|
||||
<Label
|
||||
Text="{Binding OtherOptions}"
|
||||
FontSize="Small"/>
|
||||
<Label
|
||||
Text="{u:I18n ViewAllLoginOptions}"
|
||||
StyleClass="text-sm"
|
||||
FontAttributes="Bold"
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationId="ViewAllLoginOptionsButton">
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding CloseCommand}" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</pages:BaseContentPage>
|
||||
69
src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs
Normal file
69
src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginPasswordlessRequestPage : BaseContentPage
|
||||
{
|
||||
private LoginPasswordlessRequestViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
public LoginPasswordlessRequestPage(string email, AuthRequestType authRequestType, AppOptions appOptions = null, bool authingWithSso = false)
|
||||
{
|
||||
InitializeComponent();
|
||||
_appOptions = appOptions;
|
||||
_vm = BindingContext as LoginPasswordlessRequestViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.Email = email;
|
||||
_vm.AuthRequestType = authRequestType;
|
||||
_vm.AuthingWithSso = authingWithSso;
|
||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||
_vm.UpdateTempPasswordAction = () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
_vm.CloseAction = () => { Navigation.PopModalAsync(); };
|
||||
|
||||
_vm.CreatePasswordlessLoginCommand.Execute(null);
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
_vm.StartCheckLoginRequestStatus();
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_vm.StopCheckLoginRequestStatus();
|
||||
}
|
||||
|
||||
private async Task StartTwoFactorAsync()
|
||||
{
|
||||
var page = new TwoFactorPage(false, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task LogInSuccessAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
|
||||
private async Task UpdateTempPasswordAsync()
|
||||
{
|
||||
var page = new UpdateTempPasswordPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
354
src/Core/Pages/Accounts/LoginPasswordlessRequestViewModel.cs
Normal file
354
src/Core/Pages/Accounts/LoginPasswordlessRequestViewModel.cs
Normal file
@@ -0,0 +1,354 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginPasswordlessRequestViewModel : CaptchaProtectedViewModel
|
||||
{
|
||||
private const int REQUEST_TIME_UPDATE_PERIOD_IN_SECONDS = 4;
|
||||
|
||||
private IDeviceActionService _deviceActionService;
|
||||
private IAuthService _authService;
|
||||
private ISyncService _syncService;
|
||||
private II18nService _i18nService;
|
||||
private IStateService _stateService;
|
||||
private IPlatformUtilsService _platformUtilsService;
|
||||
private IEnvironmentService _environmentService;
|
||||
private ILogger _logger;
|
||||
private IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
|
||||
protected override II18nService i18nService => _i18nService;
|
||||
protected override IEnvironmentService environmentService => _environmentService;
|
||||
protected override IDeviceActionService deviceActionService => _deviceActionService;
|
||||
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
|
||||
|
||||
private CancellationTokenSource _checkLoginRequestStatusCts;
|
||||
private Task _checkLoginRequestStatusTask;
|
||||
private string _fingerprintPhrase;
|
||||
private string _email;
|
||||
private string _requestId;
|
||||
private string _requestAccessCode;
|
||||
private AuthRequestType _authRequestType;
|
||||
// Item1 publicKey, Item2 privateKey
|
||||
private Tuple<byte[], byte[]> _requestKeyPair;
|
||||
|
||||
public LoginPasswordlessRequestViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||
_authService = ServiceContainer.Resolve<IAuthService>();
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>();
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>();
|
||||
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>();
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
||||
|
||||
CreatePasswordlessLoginCommand = new AsyncCommand(CreatePasswordlessLoginAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
CloseCommand = new AsyncCommand(() => Device.InvokeOnMainThreadAsync(CloseAction),
|
||||
onException: _logger.Exception,
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action LogInSuccessAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public bool AuthingWithSso { get; set; }
|
||||
|
||||
public ICommand CreatePasswordlessLoginCommand { get; }
|
||||
public ICommand CloseCommand { get; }
|
||||
|
||||
public string HeaderTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.LogInWithDevice;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.LogInInitiated;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string Title
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.LogInInitiated;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.AdminApprovalRequested;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string SubTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.ANotificationHasBeenSentToYourDevice;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.YourRequestHasBeenSentToYourAdmin;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.PleaseMakeSureYourVaultIsUnlockedAndTheFingerprintPhraseMatchesOnTheOtherDevice;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.YouWillBeNotifiedOnceApproved;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string OtherOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (_authRequestType)
|
||||
{
|
||||
case AuthRequestType.AuthenticateAndUnlock:
|
||||
return AppResources.LogInWithDeviceMustBeSetUpInTheSettingsOfTheBitwardenAppNeedAnotherOption;
|
||||
case AuthRequestType.AdminApproval:
|
||||
return AppResources.TroubleLoggingIn;
|
||||
default:
|
||||
return string.Empty;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string FingerprintPhrase
|
||||
{
|
||||
get => _fingerprintPhrase;
|
||||
set => SetProperty(ref _fingerprintPhrase, value);
|
||||
}
|
||||
|
||||
public string Email
|
||||
{
|
||||
get => _email;
|
||||
set => SetProperty(ref _email, value);
|
||||
}
|
||||
|
||||
public AuthRequestType AuthRequestType
|
||||
{
|
||||
get => _authRequestType;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _authRequestType, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(Title),
|
||||
nameof(SubTitle),
|
||||
nameof(Description),
|
||||
nameof(OtherOptions),
|
||||
nameof(ResendNotificationVisible)
|
||||
});
|
||||
PageTitle = HeaderTitle;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ResendNotificationVisible => AuthRequestType == AuthRequestType.AuthenticateAndUnlock;
|
||||
|
||||
public void StartCheckLoginRequestStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
_checkLoginRequestStatusCts?.Cancel();
|
||||
_checkLoginRequestStatusCts = new CancellationTokenSource();
|
||||
_checkLoginRequestStatusTask = new TimerTask(_logger, CheckLoginRequestStatus, _checkLoginRequestStatusCts).RunPeriodic(TimeSpan.FromSeconds(REQUEST_TIME_UPDATE_PERIOD_IN_SECONDS));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopCheckLoginRequestStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
_checkLoginRequestStatusCts?.Cancel();
|
||||
_checkLoginRequestStatusCts?.Dispose();
|
||||
_checkLoginRequestStatusCts = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckLoginRequestStatus()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_requestId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
PasswordlessLoginResponse response = null;
|
||||
if (AuthingWithSso)
|
||||
{
|
||||
response = await _authService.GetPasswordlessLoginRequestByIdAsync(_requestId);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = await _authService.GetPasswordlessLoginResquestAsync(_requestId, _requestAccessCode);
|
||||
}
|
||||
|
||||
if (response?.RequestApproved != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopCheckLoginRequestStatus();
|
||||
|
||||
var authResult = await _authService.LogInPasswordlessAsync(AuthingWithSso, Email, _requestAccessCode, _requestId, _requestKeyPair.Item2, response.Key, response.MasterPasswordHash);
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
|
||||
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
await HandleLoginCompleteAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (await HandleCaptchaAsync(authResult.CaptchaSiteKey, authResult.CaptchaNeeded, CheckLoginRequestStatus))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (authResult.TwoFactor)
|
||||
{
|
||||
StartTwoFactorAction?.Invoke();
|
||||
}
|
||||
else if (authResult.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleLoginCompleteAsync();
|
||||
}
|
||||
}
|
||||
catch (ApiException ex) when (ex.Error?.StatusCode == System.Net.HttpStatusCode.BadRequest)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StartCheckLoginRequestStatus();
|
||||
HandleException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleLoginCompleteAsync()
|
||||
{
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
LogInSuccessAction?.Invoke();
|
||||
}
|
||||
|
||||
private async Task CreatePasswordlessLoginAsync()
|
||||
{
|
||||
await Device.InvokeOnMainThreadAsync(() => _deviceActionService.ShowLoadingAsync(AppResources.Loading));
|
||||
|
||||
PasswordlessLoginResponse response = null;
|
||||
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||
if (pendingRequest != null && _authRequestType == AuthRequestType.AdminApproval)
|
||||
{
|
||||
response = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
|
||||
if (response == null || (response.IsAnswered && !response.RequestApproved.Value))
|
||||
{
|
||||
// handle pending auth request not valid remove it from state
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
pendingRequest = null;
|
||||
response = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Derive pubKey from privKey in state to avoid MITM attacks
|
||||
// Also generate FingerprintPhrase locally for the same reason
|
||||
var derivedPublicKey = await _cryptoFunctionService.RsaExtractPublicKeyAsync(pendingRequest.PrivateKey);
|
||||
response.FingerprintPhrase = string.Join("-", await _cryptoService.GetFingerprintAsync(Email, derivedPublicKey));
|
||||
response.RequestKeyPair = new Tuple<byte[], byte[]>(derivedPublicKey, pendingRequest.PrivateKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
response = await _authService.PasswordlessCreateLoginRequestAsync(_email, AuthRequestType);
|
||||
}
|
||||
|
||||
await HandlePasswordlessLoginAsync(response, pendingRequest == null && _authRequestType == AuthRequestType.AdminApproval);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
|
||||
private async Task HandlePasswordlessLoginAsync(PasswordlessLoginResponse response, bool createPendingAdminRequest)
|
||||
{
|
||||
if (response == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(response));
|
||||
}
|
||||
|
||||
if (createPendingAdminRequest)
|
||||
{
|
||||
var pendingAuthRequest = new PendingAdminAuthRequest { Id = response.Id, PrivateKey = response.RequestKeyPair.Item2 };
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(pendingAuthRequest);
|
||||
}
|
||||
|
||||
FingerprintPhrase = response.FingerprintPhrase;
|
||||
_requestId = response.Id;
|
||||
_requestAccessCode = response.RequestAccessCode;
|
||||
_requestKeyPair = response.RequestKeyPair;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
176
src/Core/Pages/Accounts/LoginPasswordlessViewModel.cs
Normal file
176
src/Core/Pages/Accounts/LoginPasswordlessViewModel.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginPasswordlessViewModel : BaseViewModel
|
||||
{
|
||||
private IDeviceActionService _deviceActionService;
|
||||
private IAuthService _authService;
|
||||
private IPlatformUtilsService _platformUtilsService;
|
||||
private ILogger _logger;
|
||||
private LoginPasswordlessDetails _resquest;
|
||||
private CancellationTokenSource _requestTimeCts;
|
||||
private Task _requestTimeTask;
|
||||
|
||||
private const int REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES = 5;
|
||||
|
||||
public LoginPasswordlessViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
PageTitle = AppResources.LogInRequested;
|
||||
|
||||
AcceptRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(true),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
RejectRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(false),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public ICommand AcceptRequestCommand { get; }
|
||||
|
||||
public ICommand RejectRequestCommand { get; }
|
||||
|
||||
public string LogInAttemptByLabel => LoginRequest != null ? string.Format(AppResources.LogInAttemptByXOnY, LoginRequest.Email, LoginRequest.Origin) : string.Empty;
|
||||
|
||||
public string TimeOfRequestText => CreateRequestDate(LoginRequest?.RequestDate);
|
||||
|
||||
public bool ShowIpAddress => !string.IsNullOrEmpty(LoginRequest?.IpAddress);
|
||||
|
||||
public LoginPasswordlessDetails LoginRequest
|
||||
{
|
||||
get => _resquest;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _resquest, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(LogInAttemptByLabel),
|
||||
nameof(TimeOfRequestText),
|
||||
nameof(ShowIpAddress),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void StopRequestTimeUpdater()
|
||||
{
|
||||
try
|
||||
{
|
||||
_requestTimeCts?.Cancel();
|
||||
_requestTimeCts?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void StartRequestTimeUpdater()
|
||||
{
|
||||
try
|
||||
{
|
||||
_requestTimeCts?.Cancel();
|
||||
_requestTimeCts = new CancellationTokenSource();
|
||||
_requestTimeTask = new TimerTask(_logger, UpdateRequestTime, _requestTimeCts).RunPeriodic(TimeSpan.FromMinutes(REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateRequestTime()
|
||||
{
|
||||
TriggerPropertyChanged(nameof(TimeOfRequestText));
|
||||
if (LoginRequest?.IsExpired ?? false)
|
||||
{
|
||||
StopRequestTimeUpdater();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PasswordlessLoginAsync(bool approveRequest)
|
||||
{
|
||||
if (LoginRequest.IsExpired)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(LoginRequest.Id);
|
||||
if (loginRequestData.IsAnswered)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.ThisRequestIsNoLongerValid);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
await _authService.PasswordlessLoginAsync(LoginRequest.Id, LoginRequest.PubKey, approveRequest);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.Navigation.PopModalAsync();
|
||||
_platformUtilsService.ShowToast("info", null, approveRequest ? AppResources.LogInAccepted : AppResources.LogInDenied);
|
||||
|
||||
StopRequestTimeUpdater();
|
||||
}
|
||||
|
||||
private string CreateRequestDate(DateTime? requestDate)
|
||||
{
|
||||
if (!requestDate.HasValue)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow < requestDate.Value.ToUniversalTime().AddMinutes(REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES))
|
||||
{
|
||||
return AppResources.JustNow;
|
||||
}
|
||||
|
||||
return string.Format(AppResources.XMinutesAgo, DateTime.UtcNow.Minute - requestDate.Value.ToUniversalTime().Minute);
|
||||
}
|
||||
}
|
||||
|
||||
public class LoginPasswordlessDetails
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Key { get; set; }
|
||||
|
||||
public string PubKey { get; set; }
|
||||
|
||||
public string Origin { get; set; }
|
||||
|
||||
public string Email { get; set; }
|
||||
|
||||
public string FingerprintPhrase { get; set; }
|
||||
|
||||
public DateTime RequestDate { get; set; }
|
||||
|
||||
public string DeviceType { get; set; }
|
||||
|
||||
public string IpAddress { get; set; }
|
||||
|
||||
public bool IsExpired => RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) < DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
46
src/Core/Pages/Accounts/LoginSsoPage.xaml
Normal file
46
src/Core/Pages/Accounts/LoginSsoPage.xaml
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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.LoginSsoPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginSsoPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginSsoPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="20" Margin="0,10,0,0">
|
||||
<StackLayout StyleClass="box">
|
||||
<Label Text="{u:I18n LogInSsoSummary}"
|
||||
StyleClass="text-md"
|
||||
HorizontalTextAlignment="Start"></Label>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n OrgIdentifier}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_orgIdentifier"
|
||||
Text="{Binding OrgIdentifier}"
|
||||
Keyboard="Default"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding LogInCommand}" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 0">
|
||||
<Button Text="{u:I18n LogIn}"
|
||||
StyleClass="btn-primary"
|
||||
Clicked="LogIn_Clicked"></Button>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
134
src/Core/Pages/Accounts/LoginSsoPage.xaml.cs
Normal file
134
src/Core/Pages/Accounts/LoginSsoPage.xaml.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginSsoPage : BaseContentPage
|
||||
{
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly LoginSsoPageViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
private AppOptions _appOptionsCopy;
|
||||
|
||||
public LoginSsoPage(AppOptions appOptions = null)
|
||||
{
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginSsoPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.StartSetPasswordAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
_vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync());
|
||||
_vm.UpdateTempPasswordAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
_vm.StartDeviceApprovalOptionsAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
// 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 _vm.InitAsync();
|
||||
if (string.IsNullOrWhiteSpace(_vm.OrgIdentifier))
|
||||
{
|
||||
RequestFocus(_orgIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyAppOptions()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
{
|
||||
// create an object copy of _appOptions to persist values when app is exited during web auth flow
|
||||
_appOptionsCopy = new AppOptions();
|
||||
_appOptionsCopy.SetAllFrom(_appOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreAppOptionsFromCopy()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
{
|
||||
// restore values to original readonly _appOptions object from copy
|
||||
_appOptions.SetAllFrom(_appOptionsCopy);
|
||||
_appOptionsCopy = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void LogIn_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
CopyAppOptions();
|
||||
_vm.LogInCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartTwoFactorAsync()
|
||||
{
|
||||
RestoreAppOptionsFromCopy();
|
||||
var page = new TwoFactorPage(true, _appOptions, _vm.OrgIdentifier);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartSetPasswordAsync()
|
||||
{
|
||||
RestoreAppOptionsFromCopy();
|
||||
var page = new SetPasswordPage(_appOptions, _vm.OrgIdentifier);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task UpdateTempPasswordAsync()
|
||||
{
|
||||
var page = new UpdateTempPasswordPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartDeviceApprovalOptionsAsync()
|
||||
{
|
||||
var page = new LoginApproveDevicePage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task SsoAuthSuccessAsync()
|
||||
{
|
||||
RestoreAppOptionsFromCopy();
|
||||
await AppHelpers.ClearPreviousPage();
|
||||
|
||||
if (await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
337
src/Core/Pages/Accounts/LoginSsoPageViewModel.cs
Normal file
337
src/Core/Pages/Accounts/LoginSsoPageViewModel.cs
Normal file
@@ -0,0 +1,337 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
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.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.Authentication;
|
||||
using Microsoft.Maui.Networking;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginSsoPageViewModel : BaseViewModel
|
||||
{
|
||||
private const string REDIRECT_URI = "bitwarden://sso-callback";
|
||||
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
|
||||
private string _orgIdentifier;
|
||||
private bool _useEphemeralWebBrowserSession;
|
||||
|
||||
public LoginSsoPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_passwordGenerationService =
|
||||
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
|
||||
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>("cryptoFunctionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_organizationService = ServiceContainer.Resolve<IOrganizationService>();
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
|
||||
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public string OrgIdentifier
|
||||
{
|
||||
get => _orgIdentifier;
|
||||
set => SetProperty(ref _orgIdentifier, value);
|
||||
}
|
||||
|
||||
public ICommand LogInCommand { get; }
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action SsoAuthSuccessAction { get; set; }
|
||||
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await TryClaimedDomainLogin())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(OrgIdentifier))
|
||||
{
|
||||
OrgIdentifier = await _stateService.GetRememberedOrgIdentifierAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogInAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(OrgIdentifier))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier),
|
||||
AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
||||
|
||||
var response = await _apiService.PreValidateSsoAsync(OrgIdentifier);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(response?.Token))
|
||||
{
|
||||
_logger.Error(response is null ? "Login SSO Error: response is null" : "Login SSO Error: response.Token is null or whitespace");
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError);
|
||||
return;
|
||||
}
|
||||
|
||||
var ssoToken = response.Token;
|
||||
|
||||
var passwordOptions = PasswordGenerationOptions.CreateDefault
|
||||
.WithLength(64);
|
||||
|
||||
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
||||
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
|
||||
var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash);
|
||||
|
||||
var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
||||
|
||||
var url = _apiService.IdentityBaseUrl + "/connect/authorize?" +
|
||||
"client_id=" + _platformUtilsService.GetClientType().GetString() + "&" +
|
||||
"redirect_uri=" + Uri.EscapeDataString(REDIRECT_URI) + "&" +
|
||||
"response_type=code&scope=api%20offline_access&" +
|
||||
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
|
||||
"code_challenge_method=S256&response_mode=query&" +
|
||||
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier) + "&" +
|
||||
"ssoToken=" + Uri.EscapeDataString(ssoToken);
|
||||
|
||||
WebAuthenticatorResult authResult = null;
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions()
|
||||
{
|
||||
CallbackUrl = new Uri(REDIRECT_URI),
|
||||
Url = new Uri(url),
|
||||
PrefersEphemeralWebBrowserSession = _useEphemeralWebBrowserSession,
|
||||
});
|
||||
|
||||
var code = GetResultCode(authResult, state);
|
||||
if (!string.IsNullOrEmpty(code))
|
||||
{
|
||||
await LogIn(code, codeVerifier, OrgIdentifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
_logger.Exception(e);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(e?.Error?.GetSingleMessage() ?? AppResources.LoginSsoError,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// user canceled
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
// Workaroung for cached expired sso token PM-3551
|
||||
_useEphemeralWebBrowserSession = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetResultCode(WebAuthenticatorResult authResult, string state)
|
||||
{
|
||||
string code = null;
|
||||
if (authResult != null)
|
||||
{
|
||||
authResult.Properties.TryGetValue("state", out var resultState);
|
||||
if (resultState == state)
|
||||
{
|
||||
authResult.Properties.TryGetValue("code", out var resultCode);
|
||||
code = resultCode;
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private async Task LogIn(string code, string codeVerifier, string orgId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
|
||||
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (response.TwoFactor)
|
||||
{
|
||||
StartTwoFactorAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
// Trusted device option is sent regardless if this is a trusted device or not
|
||||
// If it is trusted, it will have the necessary keys
|
||||
if (decryptOptions?.TrustedDeviceOption != null)
|
||||
{
|
||||
if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
|
||||
{
|
||||
// If we have a device key but no keys on server, we need to remove the device key
|
||||
if (decryptOptions.TrustedDeviceOption.EncryptedPrivateKey == null && decryptOptions.TrustedDeviceOption.EncryptedUserKey == null)
|
||||
{
|
||||
await _deviceTrustCryptoService.RemoveTrustedDeviceAsync();
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||
if (!decryptOptions.HasMasterPassword &&
|
||||
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
// Update temp password only if the device is trusted and therefore has a decrypted User Key set
|
||||
if (response.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
// Device is trusted and has keys, so we can decrypt
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for pending Admin Auth requests before navigating to device approval options
|
||||
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||
if (pendingRequest != null)
|
||||
{
|
||||
var authRequest = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
|
||||
if (authRequest?.RequestApproved == true)
|
||||
{
|
||||
var authResult = await _authService.LogInPasswordlessAsync(true, await _stateService.GetActiveUserEmailAsync(), authRequest.RequestAccessCode, pendingRequest.Id, pendingRequest.PrivateKey, authRequest.Key, authRequest.MasterPasswordHash);
|
||||
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
await Microsoft.Maui.ApplicationModel.MainThread.InvokeOnMainThreadAsync(
|
||||
() => _platformUtilsService.ShowToast("info", null, AppResources.LoginApproved));
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In the standard, non TDE case, a user must set password if they don't
|
||||
// have one and they aren't using key connector.
|
||||
// Note: TDE & Key connector are mutually exclusive org config options.
|
||||
if (response.ResetMasterPassword || (decryptOptions?.RequireSetPassword == true))
|
||||
{
|
||||
// TODO: We need to look into how to handle this when Org removes TDE
|
||||
// Will we have the User Key by now to set a new password?
|
||||
StartSetPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> TryClaimedDomainLogin()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
var userEmail = await _stateService.GetPreLoginEmailAsync();
|
||||
var claimedDomainOrgDetails = await _organizationService.GetClaimedOrganizationDomainAsync(userEmail);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
if (claimedDomainOrgDetails == null || !claimedDomainOrgDetails.SsoAvailable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(claimedDomainOrgDetails.OrganizationIdentifier))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.OrganizationSsoIdentifierRequired, AppResources.AnErrorHasOccurred);
|
||||
return false;
|
||||
}
|
||||
|
||||
OrgIdentifier = claimedDomainOrgDetails.OrganizationIdentifier;
|
||||
await LogInAsync();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
194
src/Core/Pages/Accounts/RegisterPage.xaml
Normal file
194
src/Core/Pages/Accounts/RegisterPage.xaml
Normal file
@@ -0,0 +1,194 @@
|
||||
<?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.RegisterPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:RegisterPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:RegisterPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="10">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n EmailAddress}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_email"
|
||||
Text="{Binding Email}"
|
||||
Keyboard="Email"
|
||||
StyleClass="box-value"
|
||||
AutomationId="EmailAddressEntry"/>
|
||||
</StackLayout>
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n MasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
AutomationId="MasterPasswordEntry"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="PasswordVisibilityToggle"/>
|
||||
</Grid>
|
||||
<Label
|
||||
StyleClass="box-sub-label"
|
||||
Margin="0,0,0,10">
|
||||
<Label.FormattedText>
|
||||
<FormattedString>
|
||||
<Span Text="{u:I18n Important}" TextColor="{DynamicResource InfoColor}"/>
|
||||
<Span Text=": " TextColor="{DynamicResource InfoColor}"/>
|
||||
<Span Text="{Binding MasterPasswordMininumCharactersDescription}" TextColor="{DynamicResource MutedColor}"/>
|
||||
</FormattedString>
|
||||
</Label.FormattedText>
|
||||
</Label>
|
||||
<controls:PasswordStrengthProgressBar
|
||||
BindingContext="{Binding PasswordStrengthViewModel}"
|
||||
Margin="0,0"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n RetypeMasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_confirmMasterPassword"
|
||||
Text="{Binding ConfirmMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
AutomationId="ConfirmMasterPasswordEntry"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding ToggleConfirmPasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationId="ConfirmPasswordVisibilityToggle"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHint}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_hint"
|
||||
Text="{Binding Hint}"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="MasterPasswordHintLabel" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHintDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Switch
|
||||
IsToggled="{Binding CheckExposedMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="Start"
|
||||
Margin="0, 0, 10, 0"
|
||||
AutomationId="CheckExposedMasterPasswordToggle"/>
|
||||
<Label
|
||||
Text="{u:I18n CheckKnownDataBreachesForThisPassword}"
|
||||
StyleClass="box-footer-label"
|
||||
VerticalOptions="Center"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch"
|
||||
IsVisible="{Binding ShowTerms}">
|
||||
<Switch
|
||||
IsToggled="{Binding AcceptPolicies}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="Start"
|
||||
Margin="0, 0, 10, 0"
|
||||
AutomationId="AcceptPoliciesToggle"/>
|
||||
<Label StyleClass="box-footer-label"
|
||||
HorizontalOptions="Fill">
|
||||
<Label.FormattedText>
|
||||
<FormattedString>
|
||||
<Span Text="{u:I18n AcceptPolicies}" />
|
||||
<Span Text="{u:I18n TermsOfService}"
|
||||
TextColor="{DynamicResource HyperlinkColor}">
|
||||
<Span.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding PoliciesClickCommand}"
|
||||
CommandParameter="https://bitwarden.com/terms/" />
|
||||
</Span.GestureRecognizers>
|
||||
</Span>
|
||||
<Span Text=", " />
|
||||
<Span Text="{u:I18n PrivacyPolicy}"
|
||||
TextColor="{DynamicResource HyperlinkColor}">
|
||||
<Span.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding PoliciesClickCommand}"
|
||||
CommandParameter="https://bitwarden.com/privacy/" />
|
||||
</Span.GestureRecognizers>
|
||||
</Span>
|
||||
</FormattedString>
|
||||
</Label.FormattedText>
|
||||
</Label>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
77
src/Core/Pages/Accounts/RegisterPage.xaml.cs
Normal file
77
src/Core/Pages/Accounts/RegisterPage.xaml.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class RegisterPage : BaseContentPage
|
||||
{
|
||||
private readonly RegisterPageViewModel _vm;
|
||||
|
||||
private bool _inputFocused;
|
||||
|
||||
public RegisterPage(HomePage homePage)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as RegisterPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.RegistrationSuccess = () => Device.BeginInvokeOnMainThread(async () => await RegistrationSuccessAsync(homePage));
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
ConfirmMasterPasswordEntry = _confirmMasterPassword;
|
||||
// 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);
|
||||
}
|
||||
|
||||
_email.ReturnType = ReturnType.Next;
|
||||
_email.ReturnCommand = new Command(() => _masterPassword.Focus());
|
||||
_masterPassword.ReturnType = ReturnType.Next;
|
||||
_masterPassword.ReturnCommand = new Command(() => _confirmMasterPassword.Focus());
|
||||
_confirmMasterPassword.ReturnType = ReturnType.Next;
|
||||
_confirmMasterPassword.ReturnCommand = new Command(() => _hint.Focus());
|
||||
}
|
||||
|
||||
public Entry MasterPasswordEntry { get; set; }
|
||||
public Entry ConfirmMasterPasswordEntry { get; set; }
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
if (!_inputFocused)
|
||||
{
|
||||
RequestFocus(_email);
|
||||
_inputFocused = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Submit_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RegistrationSuccessAsync(HomePage homePage)
|
||||
{
|
||||
if (homePage != null)
|
||||
{
|
||||
await homePage.DismissRegisterPageAndLogInAsync(_vm.Email);
|
||||
}
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
294
src/Core/Pages/Accounts/RegisterPageViewModel.cs
Normal file
294
src/Core/Pages/Accounts/RegisterPageViewModel.cs
Normal file
@@ -0,0 +1,294 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class RegisterPageViewModel : CaptchaProtectedViewModel, IPasswordStrengthable
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private string _email;
|
||||
private string _masterPassword;
|
||||
private bool _showPassword;
|
||||
private bool _acceptPolicies;
|
||||
private bool _checkExposedMasterPassword;
|
||||
|
||||
public RegisterPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_auditService = ServiceContainer.Resolve<IAuditService>();
|
||||
|
||||
PageTitle = AppResources.CreateAccount;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
ShowTerms = !_platformUtilsService.IsSelfHost();
|
||||
PasswordStrengthViewModel = new PasswordStrengthViewModel(this);
|
||||
CheckExposedMasterPassword = true;
|
||||
}
|
||||
|
||||
public ICommand PoliciesClickCommand => new Command<string>((url) =>
|
||||
{
|
||||
_platformUtilsService.LaunchUri(url);
|
||||
});
|
||||
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText)
|
||||
});
|
||||
}
|
||||
|
||||
public bool AcceptPolicies
|
||||
{
|
||||
get => _acceptPolicies;
|
||||
set => SetProperty(ref _acceptPolicies, value);
|
||||
}
|
||||
|
||||
public bool CheckExposedMasterPassword
|
||||
{
|
||||
get => _checkExposedMasterPassword;
|
||||
set => SetProperty(ref _checkExposedMasterPassword, value);
|
||||
}
|
||||
|
||||
public string MasterPassword
|
||||
{
|
||||
get => _masterPassword;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _masterPassword, value);
|
||||
PasswordStrengthViewModel.CalculatePasswordStrength();
|
||||
}
|
||||
}
|
||||
|
||||
public string Email
|
||||
{
|
||||
get => _email;
|
||||
set => SetProperty(ref _email, value);
|
||||
}
|
||||
|
||||
public string Password => MasterPassword;
|
||||
public List<string> UserInputs => PasswordStrengthViewModel.GetPasswordStrengthUserInput(Email);
|
||||
public string MasterPasswordMininumCharactersDescription => string.Format(AppResources.YourMasterPasswordCannotBeRecoveredIfYouForgetItXCharactersMinimum,
|
||||
Constants.MasterPasswordMinimumChars);
|
||||
public PasswordStrengthViewModel PasswordStrengthViewModel { get; }
|
||||
public bool ShowTerms { get; set; }
|
||||
public Command SubmitCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public Command ToggleConfirmPasswordCommand { get; }
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string Name { get; set; }
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public Action RegistrationSuccess { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
protected override II18nService i18nService => _i18nService;
|
||||
protected override IEnvironmentService environmentService => _environmentService;
|
||||
protected override IDeviceActionService deviceActionService => _deviceActionService;
|
||||
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
|
||||
|
||||
public async Task SubmitAsync(bool showLoading = true)
|
||||
{
|
||||
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Email))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
||||
AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (!Email.Contains("@"))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidEmail, AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (MasterPassword.Length < Constants.MasterPasswordMinimumChars)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.MasterPasswordLengthValMessageX, Constants.MasterPasswordMinimumChars),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (MasterPassword != ConfirmMasterPassword)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.MasterPasswordConfirmationValMessage,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (ShowTerms && !AcceptPolicies)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.AcceptPoliciesError,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (await IsPasswordWeakOrExposed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (showLoading)
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
|
||||
}
|
||||
|
||||
Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
|
||||
Email = Email.Trim().ToLower();
|
||||
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
|
||||
var newMasterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, Email, kdfConfig);
|
||||
var (newUserKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(
|
||||
newMasterKey,
|
||||
await _cryptoService.MakeUserKeyAsync()
|
||||
);
|
||||
var hashedPassword = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey);
|
||||
var (newPublicKey, newProtectedPrivateKey) = await _cryptoService.MakeKeyPairAsync(newUserKey);
|
||||
var request = new RegisterRequest
|
||||
{
|
||||
Email = Email,
|
||||
Name = Name,
|
||||
MasterPasswordHash = hashedPassword,
|
||||
MasterPasswordHint = Hint,
|
||||
Key = newProtectedUserKey.EncryptedString,
|
||||
Kdf = kdfConfig.Type,
|
||||
KdfIterations = kdfConfig.Iterations,
|
||||
KdfMemory = kdfConfig.Memory,
|
||||
KdfParallelism = kdfConfig.Parallelism,
|
||||
Keys = new KeysRequest
|
||||
{
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newProtectedPrivateKey.EncryptedString
|
||||
},
|
||||
CaptchaResponse = _captchaToken,
|
||||
};
|
||||
|
||||
// TODO: org invite?
|
||||
|
||||
try
|
||||
{
|
||||
await _apiService.PostRegisterAsync(request);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.AccountCreated,
|
||||
new System.Collections.Generic.Dictionary<string, object>
|
||||
{
|
||||
["longDuration"] = true
|
||||
});
|
||||
RegistrationSuccess?.Invoke();
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
if (e?.Error != null && e.Error.CaptchaRequired)
|
||||
{
|
||||
if (await HandleCaptchaAsync(e.Error.CaptchaSiteKey))
|
||||
{
|
||||
await SubmitAsync(false);
|
||||
_captchaToken = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
var entry = (Page as RegisterPage).MasterPasswordEntry;
|
||||
entry.Focus();
|
||||
entry.CursorPosition = String.IsNullOrEmpty(MasterPassword) ? 0 : MasterPassword.Length;
|
||||
}
|
||||
|
||||
public void ToggleConfirmPassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
var entry = (Page as RegisterPage).ConfirmMasterPasswordEntry;
|
||||
entry.Focus();
|
||||
entry.CursorPosition = String.IsNullOrEmpty(ConfirmMasterPassword) ? 0 : ConfirmMasterPassword.Length;
|
||||
}
|
||||
|
||||
private async Task<bool> IsPasswordWeakOrExposed()
|
||||
{
|
||||
try
|
||||
{
|
||||
var title = string.Empty;
|
||||
var message = string.Empty;
|
||||
var exposedPassword = CheckExposedMasterPassword ? await _auditService.PasswordLeakedAsync(MasterPassword) > 0 : false;
|
||||
var weakPassword = PasswordStrengthViewModel.PasswordStrengthLevel <= PasswordStrengthLevel.Weak;
|
||||
|
||||
if (exposedPassword && weakPassword)
|
||||
{
|
||||
title = AppResources.WeakAndExposedMasterPassword;
|
||||
message = AppResources.WeakPasswordIdentifiedAndFoundInADataBreachAlertDescription;
|
||||
}
|
||||
else if (exposedPassword)
|
||||
{
|
||||
title = AppResources.ExposedMasterPassword;
|
||||
message = AppResources.PasswordFoundInADataBreachAlertDescription;
|
||||
}
|
||||
else if (weakPassword)
|
||||
{
|
||||
title = AppResources.WeakMasterPassword;
|
||||
message = AppResources.WeakPasswordIdentifiedUseAStrongPasswordToProtectYourAccount;
|
||||
}
|
||||
|
||||
if (exposedPassword || weakPassword)
|
||||
{
|
||||
return !await _platformUtilsService.ShowDialogAsync(message, title, AppResources.Yes, AppResources.No);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/Core/Pages/Accounts/RemoveMasterPasswordPage.xaml
Normal file
33
src/Core/Pages/Accounts/RemoveMasterPasswordPage.xaml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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.RemoveMasterPasswordPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:RemoveMasterPasswordPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:RemoveMasterPasswordPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<StackLayout Spacing="20"
|
||||
Padding="10, 5">
|
||||
<StackLayout Spacing="18"
|
||||
Padding="30">
|
||||
<Label x:Name="_warningLabel"
|
||||
HorizontalTextAlignment="Center"/>
|
||||
<Label x:Name="_warningLabel2"
|
||||
HorizontalTextAlignment="Center"/>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="5">
|
||||
<Button Text="{u:I18n Continue}"
|
||||
StyleClass="btn-primary"
|
||||
Clicked="Continue_Clicked" />
|
||||
<Button Text="{u:I18n LeaveOrganization}"
|
||||
Clicked="LeaveOrg_Clicked" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
59
src/Core/Pages/Accounts/RemoveMasterPasswordPage.xaml.cs
Normal file
59
src/Core/Pages/Accounts/RemoveMasterPasswordPage.xaml.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class RemoveMasterPasswordPage : BaseContentPage
|
||||
{
|
||||
private readonly RemoveMasterPasswordPageViewModel _vm;
|
||||
|
||||
public Action NavigateAction { get; set; }
|
||||
|
||||
public RemoveMasterPasswordPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as RemoveMasterPasswordPageViewModel;
|
||||
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
await _vm.Init();
|
||||
_warningLabel.Text = string.Format(AppResources.RemoveMasterPasswordWarning,
|
||||
_vm.Organization.Name);
|
||||
_warningLabel2.Text = AppResources.RemoveMasterPasswordWarning2;
|
||||
}
|
||||
|
||||
private async void Continue_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.MigrateAccount();
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void LeaveOrg_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var confirm = await DisplayAlert(AppResources.LeaveOrganization,
|
||||
string.Format(AppResources.LeaveOrganizationName, _vm.Organization.Name),
|
||||
AppResources.Yes, AppResources.No);
|
||||
if (confirm)
|
||||
{
|
||||
await _vm.LeaveOrganization();
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnDisappearing()
|
||||
{
|
||||
NavigateAction?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Core/Pages/Accounts/RemoveMasterPasswordPageViewModel.cs
Normal file
56
src/Core/Pages/Accounts/RemoveMasterPasswordPageViewModel.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class RemoveMasterPasswordPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IKeyConnectorService _keyConnectorService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly ISyncService _syncService;
|
||||
|
||||
public Organization Organization;
|
||||
|
||||
public RemoveMasterPasswordPageViewModel()
|
||||
{
|
||||
PageTitle = AppResources.RemoveMasterPassword;
|
||||
|
||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
|
||||
}
|
||||
|
||||
public async Task Init()
|
||||
{
|
||||
Organization = await _keyConnectorService.GetManagingOrganizationAsync();
|
||||
}
|
||||
|
||||
public async Task MigrateAccount()
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
|
||||
await _keyConnectorService.MigrateUserAsync();
|
||||
await _syncService.FullSyncAsync(true);
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
|
||||
public async Task LeaveOrganization()
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
|
||||
await _apiService.PostLeaveOrganizationAsync(Organization.Id);
|
||||
await _syncService.FullSyncAsync(true);
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
177
src/Core/Pages/Accounts/SetPasswordPage.xaml
Normal file
177
src/Core/Pages/Accounts/SetPasswordPage.xaml
Normal file
@@ -0,0 +1,177 @@
|
||||
<?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.SetPasswordPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:SetPasswordPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SetPasswordPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label Text="{u:I18n SetMasterPasswordSummary}"
|
||||
StyleClass="text-md"
|
||||
HorizontalTextAlignment="Start"></Label>
|
||||
</StackLayout>
|
||||
<Grid IsVisible="{Binding ResetPasswordAutoEnroll}"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0, 12, 0, 0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{u:I18n ResetPasswordAutoEnrollInviteWarning}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Start"
|
||||
AutomationId="ResetPasswordAutoEnrollInviteWarningLabel" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0, 12, 0, 0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{Binding PolicySummary}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Start"
|
||||
AutomationId="PolicyInEffectLabel" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n MasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
AutomationId="MasterPasswordField" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="ToggleMasterPasswordVisibilityButton" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n RetypeMasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_confirmMasterPassword"
|
||||
Text="{Binding ConfirmMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
AutomationId="RetypePasswordField" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding ToggleConfirmPasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="ToggleRetypePasswordVisibilityButton" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHint}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_hint"
|
||||
Text="{Binding Hint}"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="MasterPasswordHintLabel" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHintDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
79
src/Core/Pages/Accounts/SetPasswordPage.xaml.cs
Normal file
79
src/Core/Pages/Accounts/SetPasswordPage.xaml.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SetPasswordPage : BaseContentPage
|
||||
{
|
||||
private readonly SetPasswordPageViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
public SetPasswordPage(AppOptions appOptions = null, string orgIdentifier = null)
|
||||
{
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SetPasswordPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.SetPasswordSuccessAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await SetPasswordSuccessAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
_vm.OrgIdentifier = orgIdentifier;
|
||||
// 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);
|
||||
}
|
||||
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
ConfirmMasterPasswordEntry = _confirmMasterPassword;
|
||||
|
||||
_masterPassword.ReturnType = ReturnType.Next;
|
||||
_masterPassword.ReturnCommand = new Command(() => _confirmMasterPassword.Focus());
|
||||
_confirmMasterPassword.ReturnType = ReturnType.Next;
|
||||
_confirmMasterPassword.ReturnCommand = new Command(() => _hint.Focus());
|
||||
}
|
||||
|
||||
public Entry MasterPasswordEntry { get; set; }
|
||||
public Entry ConfirmMasterPasswordEntry { get; set; }
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
RequestFocus(_masterPassword);
|
||||
}
|
||||
|
||||
private async void Submit_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetPasswordSuccessAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
305
src/Core/Pages/Accounts/SetPasswordPageViewModel.cs
Normal file
305
src/Core/Pages/Accounts/SetPasswordPageViewModel.cs
Normal file
@@ -0,0 +1,305 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Networking;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SetPasswordPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private readonly II18nService _i18nService;
|
||||
|
||||
private bool _showPassword;
|
||||
private bool _isPolicyInEffect;
|
||||
private bool _resetPasswordAutoEnroll;
|
||||
private string _policySummary;
|
||||
private MasterPasswordPolicyOptions _policy;
|
||||
|
||||
public SetPasswordPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_passwordGenerationService =
|
||||
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
|
||||
PageTitle = AppResources.SetMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
}
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText)
|
||||
});
|
||||
}
|
||||
|
||||
public bool IsPolicyInEffect
|
||||
{
|
||||
get => _isPolicyInEffect;
|
||||
set => SetProperty(ref _isPolicyInEffect, value);
|
||||
}
|
||||
|
||||
public bool ResetPasswordAutoEnroll
|
||||
{
|
||||
get => _resetPasswordAutoEnroll;
|
||||
set => SetProperty(ref _resetPasswordAutoEnroll, value);
|
||||
}
|
||||
|
||||
public string PolicySummary
|
||||
{
|
||||
get => _policySummary;
|
||||
set => SetProperty(ref _policySummary, value);
|
||||
}
|
||||
|
||||
public MasterPasswordPolicyOptions Policy
|
||||
{
|
||||
get => _policy;
|
||||
set => SetProperty(ref _policy, value);
|
||||
}
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public Command ToggleConfirmPasswordCommand { get; }
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string MasterPassword { get; set; }
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public Action SetPasswordSuccessAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public string OrgIdentifier { get; set; }
|
||||
public string OrgId { get; set; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await CheckPasswordPolicy();
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _apiService.GetOrganizationAutoEnrollStatusAsync(OrgIdentifier);
|
||||
OrgId = response.Id;
|
||||
ResetPasswordAutoEnroll = response.ResetPasswordEnabled;
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (IsPolicyInEffect)
|
||||
{
|
||||
var userInputs = _passwordGenerationService.GetPasswordStrengthUserInput(await _stateService.GetEmailAsync());
|
||||
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInputs);
|
||||
if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
|
||||
AppResources.MasterPasswordPolicyValidationMessage, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MasterPassword.Length < Constants.MasterPasswordMinimumChars)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
|
||||
string.Format(AppResources.MasterPasswordLengthValMessageX, Constants.MasterPasswordMinimumChars), AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (MasterPassword != ConfirmMasterPassword)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
AppResources.MasterPasswordConfirmationValMessage, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
var kdfConfig = new KdfConfig(KdfType.PBKDF2_SHA256, Constants.Pbkdf2Iterations, null, null);
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
var newMasterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, email, kdfConfig);
|
||||
var masterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey, HashPurpose.ServerAuthorization);
|
||||
var localMasterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, newMasterKey, HashPurpose.LocalAuthorization);
|
||||
|
||||
var (newUserKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(newMasterKey,
|
||||
await _cryptoService.GetUserKeyAsync() ?? await _cryptoService.MakeUserKeyAsync());
|
||||
|
||||
var (newPublicKey, newProtectedPrivateKey) = await _cryptoService.MakeKeyPairAsync(newUserKey);
|
||||
var request = new SetPasswordRequest
|
||||
{
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
Key = newProtectedUserKey.EncryptedString,
|
||||
MasterPasswordHint = Hint,
|
||||
Kdf = kdfConfig.Type.GetValueOrDefault(KdfType.PBKDF2_SHA256),
|
||||
KdfIterations = kdfConfig.Iterations.GetValueOrDefault(Constants.Pbkdf2Iterations),
|
||||
KdfMemory = kdfConfig.Memory,
|
||||
KdfParallelism = kdfConfig.Parallelism,
|
||||
OrgIdentifier = OrgIdentifier,
|
||||
Keys = new KeysRequest
|
||||
{
|
||||
PublicKey = newPublicKey,
|
||||
EncryptedPrivateKey = newProtectedPrivateKey.EncryptedString
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
|
||||
// Set Password and relevant information
|
||||
await _apiService.SetPasswordAsync(request);
|
||||
await _stateService.SetKdfConfigurationAsync(kdfConfig);
|
||||
await _cryptoService.SetUserKeyAsync(newUserKey);
|
||||
await _cryptoService.SetMasterKeyAsync(newMasterKey);
|
||||
await _cryptoService.SetMasterKeyHashAsync(localMasterPasswordHash);
|
||||
await _cryptoService.SetMasterKeyEncryptedUserKeyAsync(newProtectedUserKey.EncryptedString);
|
||||
await _cryptoService.SetUserPrivateKeyAsync(newProtectedPrivateKey.EncryptedString);
|
||||
|
||||
if (ResetPasswordAutoEnroll)
|
||||
{
|
||||
// Grab Organization Keys
|
||||
var response = await _apiService.GetOrganizationKeysAsync(OrgId);
|
||||
var publicKey = CoreHelpers.Base64UrlDecode(response.PublicKey);
|
||||
// Grab User Key and encrypt with Org Public Key
|
||||
var userKey = await _cryptoService.GetUserKeyAsync();
|
||||
var encryptedKey = await _cryptoService.RsaEncryptAsync(userKey.Key, publicKey);
|
||||
// Request
|
||||
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
|
||||
{
|
||||
ResetPasswordKey = encryptedKey.EncryptedString,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
};
|
||||
var userId = await _stateService.GetActiveUserIdAsync();
|
||||
// Enroll user
|
||||
await _apiService.PutOrganizationUserResetPasswordEnrollmentAsync(OrgId, userId, resetRequest);
|
||||
}
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
SetPasswordSuccessAction?.Invoke();
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as SetPasswordPage).MasterPasswordEntry.Focus();
|
||||
}
|
||||
|
||||
public void ToggleConfirmPassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as SetPasswordPage).ConfirmMasterPasswordEntry.Focus();
|
||||
}
|
||||
|
||||
private async Task CheckPasswordPolicy()
|
||||
{
|
||||
Policy = await _policyService.GetMasterPasswordPolicyOptions();
|
||||
IsPolicyInEffect = Policy?.InEffect() ?? false;
|
||||
if (!IsPolicyInEffect)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bullet = "\n" + "".PadLeft(4) + "\u2022 ";
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(_i18nService.T("MasterPasswordPolicyInEffect"));
|
||||
if (Policy.MinComplexity > 0)
|
||||
{
|
||||
sb.Append(bullet)
|
||||
.Append(string.Format(_i18nService.T("PolicyInEffectMinComplexity"), Policy.MinComplexity));
|
||||
}
|
||||
if (Policy.MinLength > 0)
|
||||
{
|
||||
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectMinLength"), Policy.MinLength));
|
||||
}
|
||||
if (Policy.RequireUpper)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectUppercase"));
|
||||
}
|
||||
if (Policy.RequireLower)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectLowercase"));
|
||||
}
|
||||
if (Policy.RequireNumbers)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectNumbers"));
|
||||
}
|
||||
if (Policy.RequireSpecial)
|
||||
{
|
||||
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectSpecial"), "!@#$%^&*"));
|
||||
}
|
||||
PolicySummary = sb.ToString();
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetPasswordStrengthUserInput()
|
||||
{
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
List<string> userInput = null;
|
||||
var atPosition = email.IndexOf('@');
|
||||
if (atPosition > -1)
|
||||
{
|
||||
var rx = new Regex("/[^A-Za-z0-9]/", RegexOptions.Compiled);
|
||||
var data = rx.Split(email.Substring(0, atPosition).Trim().ToLower());
|
||||
userInput = new List<string>(data);
|
||||
}
|
||||
return userInput;
|
||||
}
|
||||
}
|
||||
}
|
||||
179
src/Core/Pages/Accounts/TwoFactorPage.xaml
Normal file
179
src/Core/Pages/Accounts/TwoFactorPage.xaml
Normal file
@@ -0,0 +1,179 @@
|
||||
<?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.TwoFactorPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:TwoFactorPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:TwoFactorPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_cancelItem" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:IsNullConverter x:Key="isNull" />
|
||||
<ToolbarItem
|
||||
x:Name="_moreItem"
|
||||
x:Key="moreItem"
|
||||
IconImageSource="more_vert.png"
|
||||
Order="Primary"
|
||||
Command="{Binding MoreCommand}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Options}" />
|
||||
<ToolbarItem Text="{u:I18n UseAnotherTwoStepMethod}"
|
||||
Clicked="Methods_Clicked"
|
||||
Order="Secondary"
|
||||
x:Name="_useAnotherTwoStepMethod"
|
||||
x:Key="useAnotherTwoStepMethod" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ScrollView x:Name="_scrollView">
|
||||
<StackLayout Spacing="10" Padding="0, 0, 0, 10" VerticalOptions="FillAndExpand">
|
||||
<StackLayout Spacing="20" Padding="0" IsVisible="{Binding TotpMethod, Mode=OneWay}">
|
||||
<Label
|
||||
Text="{Binding TotpInstruction, Mode=OneWay}"
|
||||
Margin="10, 20, 10, 0"
|
||||
HorizontalTextAlignment="Center" />
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
Text="{u:I18n VerificationCode}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
Text="{Binding Token}"
|
||||
x:Name="_totpEntry"
|
||||
Keyboard="Numeric"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
TextChanged="Token_TextChanged"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n RememberMe}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Remember}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="20" Padding="0" IsVisible="{Binding YubikeyMethod, Mode=OneWay}">
|
||||
<Label
|
||||
Text="{Binding YubikeyInstruction, Mode=OneWay}"
|
||||
Margin="10, 20, 10, 0"
|
||||
HorizontalTextAlignment="Center" />
|
||||
<Image
|
||||
Source="yubikey.png"
|
||||
Margin="10, 0"
|
||||
WidthRequest="266"
|
||||
HeightRequest="160"
|
||||
HorizontalOptions="Center" />
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Entry
|
||||
x:Name="_yubikeyTokenEntry"
|
||||
Text="{Binding Token}"
|
||||
StyleClass="box-value"
|
||||
IsPassword="True"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n RememberMe}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Remember}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="20" Padding="0" IsVisible="{Binding Fido2Method, Mode=OneWay}">
|
||||
<Label
|
||||
Text="{u:I18n Fido2Instruction}"
|
||||
Margin="10, 20, 10, 0"
|
||||
HorizontalTextAlignment="Center" />
|
||||
<Image
|
||||
Source="yubikey.png"
|
||||
Margin="10, 0"
|
||||
WidthRequest="266"
|
||||
HeightRequest="160"
|
||||
HorizontalOptions="Center" />
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n RememberMe}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Remember}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding DuoMethod, Mode=OneWay}"
|
||||
VerticalOptions="FillAndExpand">
|
||||
<controls:HybridWebView
|
||||
x:Name="_duoWebView"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand"
|
||||
MinimumHeightRequest="400" />
|
||||
<StackLayout StyleClass="box" VerticalOptions="End">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n RememberMe}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Remember}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Spacing="0"
|
||||
Padding="0"
|
||||
IsVisible="{Binding SelectedProviderType, Mode=OneWay, Converter={StaticResource isNull}}">
|
||||
<Label
|
||||
Text="{u:I18n NoTwoStepAvailable}"
|
||||
Margin="10, 20, 10, 10"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n Continue}"
|
||||
IsEnabled="{Binding EnableContinue}"
|
||||
IsVisible="{Binding ShowContinue}"
|
||||
Clicked="Continue_Clicked"
|
||||
Margin="10, 0"
|
||||
x:Name="_continue"></Button>
|
||||
<Button Text="{u:I18n SendVerificationCodeAgain}"
|
||||
IsVisible="{Binding EmailMethod}"
|
||||
Clicked="ResendEmail_Clicked"
|
||||
Margin="10, 0"></Button>
|
||||
<Button Text="{u:I18n TryAgain}"
|
||||
IsVisible="{Binding ShowTryAgain}"
|
||||
Clicked="TryAgain_Clicked"
|
||||
Margin="10, 0"></Button>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
216
src/Core/Pages/Accounts/TwoFactorPage.xaml.cs
Normal file
216
src/Core/Pages/Accounts/TwoFactorPage.xaml.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class TwoFactorPage : BaseContentPage
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
private TwoFactorPageViewModel _vm;
|
||||
private bool _inited;
|
||||
private string _orgIdentifier;
|
||||
|
||||
public TwoFactorPage(bool? authingWithSso = false, AppOptions appOptions = null, string orgIdentifier = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
SetActivityIndicator();
|
||||
_appOptions = appOptions;
|
||||
_orgIdentifier = orgIdentifier;
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_vm = BindingContext as TwoFactorPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.AuthingWithSso = authingWithSso ?? false;
|
||||
_vm.StartSetPasswordAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
_vm.TwoFactorAuthSuccessAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessToMainAsync());
|
||||
_vm.LockAction = () =>
|
||||
Device.BeginInvokeOnMainThread(TwoFactorAuthSuccessWithSSOLocked);
|
||||
_vm.UpdateTempPasswordAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||
_vm.StartDeviceApprovalOptionsAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync());
|
||||
_vm.CloseAction = async () => await Navigation.PopModalAsync();
|
||||
DuoWebView = _duoWebView;
|
||||
// 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.Remove(_cancelItem);
|
||||
}
|
||||
// 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)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolbarItems.Add(_useAnotherTwoStepMethod);
|
||||
}
|
||||
}
|
||||
|
||||
public HybridWebView DuoWebView { get; set; }
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
_broadcasterService.Subscribe(nameof(TwoFactorPage), (message) =>
|
||||
{
|
||||
if (message.Command == "gotYubiKeyOTP")
|
||||
{
|
||||
var token = (string)message.Data;
|
||||
if (_vm.YubikeyMethod && !string.IsNullOrWhiteSpace(token) &&
|
||||
token.Length == 44 && !token.Contains(" "))
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
_vm.Token = token;
|
||||
await _vm.SubmitAsync();
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (message.Command == "resumeYubiKey")
|
||||
{
|
||||
if (_vm.YubikeyMethod)
|
||||
{
|
||||
_messagingService.Send("listenYubiKeyOTP", true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await LoadOnAppearedAsync(_scrollView, true, () =>
|
||||
{
|
||||
if (!_inited)
|
||||
{
|
||||
_inited = true;
|
||||
_vm.Init();
|
||||
}
|
||||
if (_vm.TotpMethod)
|
||||
{
|
||||
RequestFocus(_totpEntry);
|
||||
}
|
||||
else if (_vm.YubikeyMethod)
|
||||
{
|
||||
RequestFocus(_yubikeyTokenEntry);
|
||||
}
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
if (!_vm.YubikeyMethod)
|
||||
{
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
|
||||
}
|
||||
}
|
||||
protected override bool OnBackButtonPressed()
|
||||
{
|
||||
if (_vm.YubikeyMethod)
|
||||
{
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
|
||||
}
|
||||
return base.OnBackButtonPressed();
|
||||
}
|
||||
|
||||
private async void Continue_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Methods_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.AnotherMethodAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void ResendEmail_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SendEmailAsync(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async void TryAgain_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
if (_vm.Fido2Method)
|
||||
{
|
||||
await _vm.Fido2AuthenticateAsync();
|
||||
}
|
||||
else if (_vm.YubikeyMethod)
|
||||
{
|
||||
_messagingService.Send("listenYubiKeyOTP", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartSetPasswordAsync()
|
||||
{
|
||||
_vm.CloseAction();
|
||||
var page = new SetPasswordPage(_appOptions, _orgIdentifier);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task UpdateTempPasswordAsync()
|
||||
{
|
||||
var page = new UpdateTempPasswordPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartDeviceApprovalOptionsAsync()
|
||||
{
|
||||
var page = new LoginApproveDevicePage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private void TwoFactorAuthSuccessWithSSOLocked()
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
||||
}
|
||||
|
||||
private async Task TwoFactorAuthSuccessToMainAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
|
||||
private void Token_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
_vm.EnableContinue = !string.IsNullOrWhiteSpace(e.NewTextValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
510
src/Core/Pages/Accounts/TwoFactorPageViewModel.cs
Normal file
510
src/Core/Pages/Accounts/TwoFactorPageViewModel.cs
Normal file
@@ -0,0 +1,510 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
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.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Microsoft.Maui.Authentication;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class TwoFactorPageViewModel : CaptchaProtectedViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IAppIdService _appIdService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDeviceTrustCryptoService _deviceTrustCryptoService;
|
||||
private TwoFactorProviderType? _selectedProviderType;
|
||||
private string _totpInstruction;
|
||||
private string _webVaultUrl = "https://vault.bitwarden.com";
|
||||
private bool _enableContinue = false;
|
||||
private bool _showContinue = true;
|
||||
|
||||
public TwoFactorPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||
_logger = ServiceContainer.Resolve<ILogger>();
|
||||
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
||||
|
||||
PageTitle = AppResources.TwoStepLogin;
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public string TotpInstruction
|
||||
{
|
||||
get => _totpInstruction;
|
||||
set => SetProperty(ref _totpInstruction, value);
|
||||
}
|
||||
|
||||
public bool Remember { get; set; }
|
||||
|
||||
public bool AuthingWithSso { get; set; }
|
||||
|
||||
public string Token { get; set; }
|
||||
|
||||
public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo ||
|
||||
SelectedProviderType == TwoFactorProviderType.OrganizationDuo;
|
||||
|
||||
public bool Fido2Method => SelectedProviderType == TwoFactorProviderType.Fido2WebAuthn;
|
||||
|
||||
public bool YubikeyMethod => SelectedProviderType == TwoFactorProviderType.YubiKey;
|
||||
|
||||
public bool AuthenticatorMethod => SelectedProviderType == TwoFactorProviderType.Authenticator;
|
||||
|
||||
public bool EmailMethod => SelectedProviderType == TwoFactorProviderType.Email;
|
||||
|
||||
public bool TotpMethod => AuthenticatorMethod || EmailMethod;
|
||||
|
||||
public bool ShowTryAgain => (YubikeyMethod && // 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) || Fido2Method;
|
||||
|
||||
public bool ShowContinue
|
||||
{
|
||||
get => _showContinue;
|
||||
set => SetProperty(ref _showContinue, value);
|
||||
}
|
||||
|
||||
public bool EnableContinue
|
||||
{
|
||||
get => _enableContinue;
|
||||
set => SetProperty(ref _enableContinue, value);
|
||||
}
|
||||
|
||||
public string YubikeyInstruction => // 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 ? AppResources.YubiKeyInstructionIos :
|
||||
AppResources.YubiKeyInstruction;
|
||||
|
||||
public TwoFactorProviderType? SelectedProviderType
|
||||
{
|
||||
get => _selectedProviderType;
|
||||
set => SetProperty(ref _selectedProviderType, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(EmailMethod),
|
||||
nameof(DuoMethod),
|
||||
nameof(Fido2Method),
|
||||
nameof(YubikeyMethod),
|
||||
nameof(AuthenticatorMethod),
|
||||
nameof(TotpMethod),
|
||||
nameof(ShowTryAgain),
|
||||
});
|
||||
}
|
||||
public Command SubmitCommand { get; }
|
||||
public ICommand MoreCommand { get; }
|
||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||
public Action LockAction { get; set; }
|
||||
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
public Action UpdateTempPasswordAction { get; set; }
|
||||
|
||||
protected override II18nService i18nService => _i18nService;
|
||||
protected override IEnvironmentService environmentService => _environmentService;
|
||||
protected override IDeviceActionService deviceActionService => _deviceActionService;
|
||||
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
if ((!_authService.AuthingWithSso() && !_authService.AuthingWithPassword()) ||
|
||||
_authService.TwoFactorProvidersData == null)
|
||||
{
|
||||
// TODO: dismiss modal?
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl))
|
||||
{
|
||||
_webVaultUrl = _environmentService.BaseUrl;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(_environmentService.WebVaultUrl))
|
||||
{
|
||||
_webVaultUrl = _environmentService.WebVaultUrl;
|
||||
}
|
||||
|
||||
SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_platformUtilsService.SupportsFido2());
|
||||
Load();
|
||||
}
|
||||
|
||||
public void Load()
|
||||
{
|
||||
if (SelectedProviderType == null)
|
||||
{
|
||||
PageTitle = AppResources.LoginUnavailable;
|
||||
return;
|
||||
}
|
||||
var page = Page as TwoFactorPage;
|
||||
PageTitle = _authService.TwoFactorProviders[SelectedProviderType.Value].Name;
|
||||
var providerData = _authService.TwoFactorProvidersData[SelectedProviderType.Value];
|
||||
switch (SelectedProviderType.Value)
|
||||
{
|
||||
case TwoFactorProviderType.Fido2WebAuthn:
|
||||
Fido2AuthenticateAsync(providerData);
|
||||
break;
|
||||
case TwoFactorProviderType.YubiKey:
|
||||
_messagingService.Send("listenYubiKeyOTP", true);
|
||||
break;
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.OrganizationDuo:
|
||||
var host = WebUtility.UrlEncode(providerData["Host"] as string);
|
||||
var req = WebUtility.UrlEncode(providerData["Signature"] as string);
|
||||
page.DuoWebView.Uri = $"{_webVaultUrl}/duo-connector.html?host={host}&request={req}";
|
||||
page.DuoWebView.RegisterAction(sig =>
|
||||
{
|
||||
Token = sig;
|
||||
Device.BeginInvokeOnMainThread(async () => await SubmitAsync());
|
||||
});
|
||||
break;
|
||||
case TwoFactorProviderType.Email:
|
||||
TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail,
|
||||
providerData["Email"] as string);
|
||||
if (_authService.TwoFactorProvidersData.Count > 1)
|
||||
{
|
||||
var emailTask = Task.Run(() => SendEmailAsync(false, false));
|
||||
}
|
||||
break;
|
||||
case TwoFactorProviderType.Authenticator:
|
||||
TotpInstruction = AppResources.EnterVerificationCodeApp;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!YubikeyMethod)
|
||||
{
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
}
|
||||
ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method);
|
||||
}
|
||||
|
||||
public async Task Fido2AuthenticateAsync(Dictionary<string, object> providerData = null)
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
|
||||
|
||||
if (providerData == null)
|
||||
{
|
||||
providerData = _authService.TwoFactorProvidersData[TwoFactorProviderType.Fido2WebAuthn];
|
||||
}
|
||||
|
||||
var callbackUri = "bitwarden://webauthn-callback";
|
||||
var data = AppHelpers.EncodeDataParameter(new
|
||||
{
|
||||
callbackUri = callbackUri,
|
||||
data = JsonConvert.SerializeObject(providerData),
|
||||
headerText = AppResources.Fido2Title,
|
||||
btnText = AppResources.Fido2AuthenticateWebAuthn,
|
||||
btnReturnText = AppResources.Fido2ReturnToApp,
|
||||
});
|
||||
|
||||
var url = _webVaultUrl + "/webauthn-mobile-connector.html?" + "data=" + data +
|
||||
"&parent=" + Uri.EscapeDataString(callbackUri) + "&v=2";
|
||||
|
||||
WebAuthenticatorResult authResult = null;
|
||||
try
|
||||
{
|
||||
var options = new WebAuthenticatorOptions
|
||||
{
|
||||
Url = new Uri(url),
|
||||
CallbackUrl = new Uri(callbackUri),
|
||||
PrefersEphemeralWebBrowserSession = true,
|
||||
};
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(options);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// user canceled
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
string response = null;
|
||||
if (authResult != null && authResult.Properties.TryGetValue("data", out var resultData))
|
||||
{
|
||||
response = Uri.UnescapeDataString(resultData);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(response))
|
||||
{
|
||||
Token = response;
|
||||
await SubmitAsync(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError))
|
||||
{
|
||||
var message = AppResources.Fido2CheckBrowser + "\n\n" + resultError;
|
||||
await _platformUtilsService.ShowDialogAsync(message, AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.Fido2CheckBrowser,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SubmitAsync(bool showLoading = true)
|
||||
{
|
||||
if (SelectedProviderType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Token))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.VerificationCode),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (SelectedProviderType == TwoFactorProviderType.Email ||
|
||||
SelectedProviderType == TwoFactorProviderType.Authenticator)
|
||||
{
|
||||
Token = Token.Replace(" ", string.Empty).Trim();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (showLoading)
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
|
||||
}
|
||||
var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, _captchaToken, Remember);
|
||||
|
||||
if (result.CaptchaNeeded)
|
||||
{
|
||||
if (await HandleCaptchaAsync(result.CaptchaSiteKey))
|
||||
{
|
||||
await SubmitAsync(false);
|
||||
_captchaToken = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_captchaToken = null;
|
||||
|
||||
var task = Task.Run(() => _syncService.FullSyncAsync(true));
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
var decryptOptions = await _stateService.GetAccountDecryptionOptions();
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
|
||||
|
||||
if (decryptOptions?.TrustedDeviceOption != null)
|
||||
{
|
||||
if (await _deviceTrustCryptoService.IsDeviceTrustedAsync())
|
||||
{
|
||||
// If we have a device key but no keys on server, we need to remove the device key
|
||||
if (decryptOptions.TrustedDeviceOption.EncryptedPrivateKey == null && decryptOptions.TrustedDeviceOption.EncryptedUserKey == null)
|
||||
{
|
||||
await _deviceTrustCryptoService.RemoveTrustedDeviceAsync();
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||
if (!decryptOptions.HasMasterPassword &&
|
||||
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
// Update temp password only if the device is trusted and therefore has a decrypted User Key set
|
||||
if (result.ForcePasswordReset)
|
||||
{
|
||||
UpdateTempPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
// Device is trusted and has keys, so we can decrypt
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
await TwoFactorAuthSuccessAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for pending Admin Auth requests before navigating to device approval options
|
||||
var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync();
|
||||
if (pendingRequest != null)
|
||||
{
|
||||
var authRequest = await _authService.GetPasswordlessLoginRequestByIdAsync(pendingRequest.Id);
|
||||
if (authRequest?.RequestApproved == true)
|
||||
{
|
||||
var authResult = await _authService.LogInPasswordlessAsync(true, await _stateService.GetActiveUserEmailAsync(), authRequest.RequestAccessCode, pendingRequest.Id, pendingRequest.PrivateKey, authRequest.Key, authRequest.MasterPasswordHash);
|
||||
if (authResult == null && await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
await Microsoft.Maui.ApplicationModel.MainThread.InvokeOnMainThreadAsync(
|
||||
() => _platformUtilsService.ShowToast("info", null, AppResources.LoginApproved));
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
await TwoFactorAuthSuccessAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stateService.SetPendingAdminAuthRequestAsync(null);
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StartDeviceApprovalOptionsAction?.Invoke();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In the standard, non TDE case, a user must set password if they don't
|
||||
// have one and they aren't using key connector.
|
||||
// Note: TDE & Key connector are mutually exclusive org config options.
|
||||
if (result.ResetMasterPassword || (decryptOptions?.RequireSetPassword ?? false))
|
||||
{
|
||||
// TODO: We need to look into how to handle this when Org removes TDE
|
||||
// Will we have the User Key by now to set a new password?
|
||||
StartSetPasswordAction?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
_syncService.FullSyncAsync(true).FireAndForget();
|
||||
await TwoFactorAuthSuccessAsync();
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
_captchaToken = null;
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MoreAsync()
|
||||
{
|
||||
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, AppResources.UseAnotherTwoStepMethod);
|
||||
if (selection == AppResources.UseAnotherTwoStepMethod)
|
||||
{
|
||||
await AnotherMethodAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AnotherMethodAsync()
|
||||
{
|
||||
var supportedProviders = _authService.GetSupportedTwoFactorProviders();
|
||||
var options = supportedProviders.Select(p => p.Name).ToList();
|
||||
options.Add(AppResources.RecoveryCodeTitle);
|
||||
var method = await _deviceActionService.DisplayActionSheetAsync(AppResources.TwoStepLoginOptions,
|
||||
AppResources.Cancel, null, options.ToArray());
|
||||
if (method == AppResources.RecoveryCodeTitle)
|
||||
{
|
||||
_platformUtilsService.LaunchUri("https://bitwarden.com/help/lost-two-step-device/");
|
||||
}
|
||||
else if (method != AppResources.Cancel && method != null)
|
||||
{
|
||||
var selected = supportedProviders.FirstOrDefault(p => p.Name == method)?.Type;
|
||||
if (selected == SelectedProviderType)
|
||||
{
|
||||
// Nothing changed
|
||||
return;
|
||||
}
|
||||
SelectedProviderType = selected;
|
||||
Load();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SendEmailAsync(bool showLoading, bool doToast)
|
||||
{
|
||||
if (!EmailMethod)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (showLoading)
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Submitting);
|
||||
}
|
||||
var request = new TwoFactorEmailRequest
|
||||
{
|
||||
Email = _authService.Email,
|
||||
MasterPasswordHash = _authService.MasterPasswordHash,
|
||||
DeviceIdentifier = await _appIdService.GetAppIdAsync(),
|
||||
SsoEmail2FaSessionToken = _authService.SsoEmail2FaSessionToken
|
||||
};
|
||||
await _apiService.PostTwoFactorEmailAsync(request);
|
||||
if (showLoading)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
if (doToast)
|
||||
{
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.VerificationEmailSent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (ApiException)
|
||||
{
|
||||
if (showLoading)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.VerificationEmailNotSent,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TwoFactorAuthSuccessAsync()
|
||||
{
|
||||
if (AuthingWithSso && await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
LockAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
TwoFactorAuthSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
208
src/Core/Pages/Accounts/UpdateTempPasswordPage.xaml
Normal file
208
src/Core/Pages/Accounts/UpdateTempPasswordPage.xaml
Normal file
@@ -0,0 +1,208 @@
|
||||
<?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.UpdateTempPasswordPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:UpdateTempPasswordPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:UpdateTempPasswordPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n LogOut}" Clicked="LogOut_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout
|
||||
x:Name="_mainLayout"
|
||||
Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid Margin="0, 12, 0, 0"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{Binding UpdateMasterPasswordWarningText }"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="UpdatePasswordWarningLabel" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||
Margin="0, 12, 0, 0"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{Binding PolicySummary}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Start"
|
||||
AutomationId="PolicySummaryLabel" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row" IsVisible="{Binding RequireCurrentPassword }">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n CurrentMasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_currentMasterPassword"
|
||||
Text="{Binding CurrentMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
AutomationId="MasterPasswordField" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="ToggleMasterPasswordVisibilityButton" />
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n MasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
AutomationId="NewPasswordField" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="NewPasswordVisibilityButton" />
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n RetypeMasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_confirmMasterPassword"
|
||||
Text="{Binding ConfirmMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
AutomationId="RetypePasswordField" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding ToggleConfirmPasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="ToggleRetypePasswordVisibilityButton" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHint}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_hint"
|
||||
Text="{Binding Hint}"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
AutomationId="MasterPasswordHintLabel" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHintDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
87
src/Core/Pages/Accounts/UpdateTempPasswordPage.xaml.cs
Normal file
87
src/Core/Pages/Accounts/UpdateTempPasswordPage.xaml.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class UpdateTempPasswordPage : BaseContentPage
|
||||
{
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly UpdateTempPasswordPageViewModel _vm;
|
||||
private readonly string _pageName;
|
||||
|
||||
public UpdateTempPasswordPage()
|
||||
{
|
||||
// Service Init
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
|
||||
// Binding
|
||||
InitializeComponent();
|
||||
_pageName = string.Concat(nameof(UpdateTempPasswordPage), "_", DateTime.UtcNow.Ticks);
|
||||
_vm = BindingContext as UpdateTempPasswordPageViewModel;
|
||||
_vm.Page = this;
|
||||
SetActivityIndicator();
|
||||
|
||||
// Actions Declaration
|
||||
_vm.LogOutAction = () =>
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
};
|
||||
_vm.UpdateTempPasswordSuccessAction = () => Device.BeginInvokeOnMainThread(UpdateTempPasswordSuccess);
|
||||
|
||||
// Link fields that will be referenced in codebehind
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
ConfirmMasterPasswordEntry = _confirmMasterPassword;
|
||||
|
||||
// Return Types and Commands
|
||||
_masterPassword.ReturnType = ReturnType.Next;
|
||||
_masterPassword.ReturnCommand = new Command(() => _confirmMasterPassword.Focus());
|
||||
_confirmMasterPassword.ReturnType = ReturnType.Next;
|
||||
_confirmMasterPassword.ReturnCommand = new Command(() => _hint.Focus());
|
||||
}
|
||||
|
||||
public Entry MasterPasswordEntry { get; set; }
|
||||
public Entry ConfirmMasterPasswordEntry { get; set; }
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await LoadOnAppearedAsync(_mainLayout, true, async () =>
|
||||
{
|
||||
await _vm.InitAsync(true);
|
||||
});
|
||||
RequestFocus(_masterPassword);
|
||||
}
|
||||
|
||||
private async void Submit_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void LogOut_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation,
|
||||
AppResources.LogOut, AppResources.Yes, AppResources.Cancel);
|
||||
if (confirmed)
|
||||
{
|
||||
_vm.LogOutAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTempPasswordSuccess()
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
}
|
||||
}
|
||||
}
|
||||
173
src/Core/Pages/Accounts/UpdateTempPasswordPageViewModel.cs
Normal file
173
src/Core/Pages/Accounts/UpdateTempPasswordPageViewModel.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class UpdateTempPasswordPageViewModel : BaseChangePasswordViewModel
|
||||
{
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
|
||||
private ForcePasswordResetReason _reason = ForcePasswordResetReason.AdminForcePasswordReset;
|
||||
|
||||
public UpdateTempPasswordPageViewModel()
|
||||
{
|
||||
PageTitle = AppResources.UpdateMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
|
||||
SubmitCommand = new AsyncCommand(SubmitAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
||||
}
|
||||
|
||||
public AsyncCommand SubmitCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public Command ToggleConfirmPasswordCommand { get; }
|
||||
public Action UpdateTempPasswordSuccessAction { get; set; }
|
||||
public Action LogOutAction { get; set; }
|
||||
public string CurrentMasterPassword { get; set; }
|
||||
|
||||
public override async Task InitAsync(bool forceSync = false)
|
||||
{
|
||||
await base.InitAsync(forceSync);
|
||||
|
||||
var forcePasswordResetReason = await _stateService.GetForcePasswordResetReasonAsync();
|
||||
|
||||
if (forcePasswordResetReason.HasValue)
|
||||
{
|
||||
_reason = forcePasswordResetReason.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RequireCurrentPassword
|
||||
{
|
||||
get => _reason == ForcePasswordResetReason.WeakMasterPasswordOnLogin;
|
||||
}
|
||||
|
||||
public string UpdateMasterPasswordWarningText
|
||||
{
|
||||
get
|
||||
{
|
||||
return _reason == ForcePasswordResetReason.WeakMasterPasswordOnLogin
|
||||
? AppResources.UpdateWeakMasterPasswordWarning
|
||||
: AppResources.UpdateMasterPasswordWarning;
|
||||
}
|
||||
}
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as UpdateTempPasswordPage).MasterPasswordEntry.Focus();
|
||||
}
|
||||
|
||||
public void ToggleConfirmPassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as UpdateTempPasswordPage).ConfirmMasterPasswordEntry.Focus();
|
||||
}
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
if (!await ValidateMasterPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (RequireCurrentPassword &&
|
||||
!await _userVerificationService.VerifyUser(CurrentMasterPassword, VerificationType.MasterPassword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve details for key generation
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
|
||||
// Create new master key and hash new password
|
||||
var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, email, kdfConfig);
|
||||
var masterPasswordHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey);
|
||||
|
||||
// Encrypt user key with new master key
|
||||
var (userKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(masterKey);
|
||||
|
||||
// Initiate API action
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.UpdatingPassword);
|
||||
|
||||
switch (_reason)
|
||||
{
|
||||
case ForcePasswordResetReason.AdminForcePasswordReset:
|
||||
await UpdateTempPasswordAsync(masterPasswordHash, newProtectedUserKey.EncryptedString);
|
||||
break;
|
||||
case ForcePasswordResetReason.WeakMasterPasswordOnLogin:
|
||||
await UpdatePasswordAsync(masterPasswordHash, newProtectedUserKey.EncryptedString);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
// Clear the force reset password reason
|
||||
await _stateService.SetForcePasswordResetReasonAsync(null);
|
||||
|
||||
_platformUtilsService.ShowToast(null, null, AppResources.UpdatedMasterPassword);
|
||||
|
||||
UpdateTempPasswordSuccessAction?.Invoke();
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.UpdatePasswordError,
|
||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateTempPasswordAsync(string newMasterPasswordHash, string newEncKey)
|
||||
{
|
||||
var request = new UpdateTempPasswordRequest
|
||||
{
|
||||
Key = newEncKey,
|
||||
NewMasterPasswordHash = newMasterPasswordHash,
|
||||
MasterPasswordHint = Hint
|
||||
};
|
||||
|
||||
await _apiService.PutUpdateTempPasswordAsync(request);
|
||||
}
|
||||
|
||||
private async Task UpdatePasswordAsync(string newMasterPasswordHash, string newEncKey)
|
||||
{
|
||||
var currentPasswordHash = await _cryptoService.HashMasterKeyAsync(CurrentMasterPassword, null);
|
||||
|
||||
var request = new PasswordRequest
|
||||
{
|
||||
MasterPasswordHash = currentPasswordHash,
|
||||
Key = newEncKey,
|
||||
NewMasterPasswordHash = newMasterPasswordHash,
|
||||
MasterPasswordHint = Hint
|
||||
};
|
||||
|
||||
await _apiService.PostPasswordAsync(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
99
src/Core/Pages/Accounts/VerificationCodePage.xaml
Normal file
99
src/Core/Pages/Accounts/VerificationCodePage.xaml
Normal file
@@ -0,0 +1,99 @@
|
||||
<?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.Accounts.VerificationCodePage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
x:DataType="pages:VerificationCodeViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:VerificationCodeViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<Style TargetType="Label" x:Key="lblDescription">
|
||||
<Setter Property="FontSize" Value="{OnPlatform Android=Large, iOS=Small}" />
|
||||
</Style>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.Content>
|
||||
<ScrollView>
|
||||
<Grid
|
||||
RowSpacing="10"
|
||||
ColumnSpacing="10"
|
||||
RowDefinitions="Auto, Auto, Auto"
|
||||
ColumnDefinitions="*, *"
|
||||
Padding="10">
|
||||
<Label
|
||||
Grid.Row="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Padding="0,10"
|
||||
Text="{Binding SendCodeStatus}"
|
||||
StyleClass="box-label"
|
||||
LineBreakMode="WordWrap"
|
||||
Margin="0,0,0,10" />
|
||||
<Grid
|
||||
StyleClass="box-row"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
RowDefinitions="Auto,Auto,Auto"
|
||||
ColumnDefinitions="*,Auto"
|
||||
Padding="0">
|
||||
<Label
|
||||
Text="{u:I18n VerificationCode}"
|
||||
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}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding MainActionCommand}" />
|
||||
<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}"/>
|
||||
<Label
|
||||
Text="{u:I18n ConfirmYourIdentity}"
|
||||
StyleClass="box-footer-label"
|
||||
LineBreakMode="WordWrap"
|
||||
Grid.Row="2"
|
||||
Margin="0,10,0,0" />
|
||||
</Grid>
|
||||
<Button
|
||||
x:Name="_mainActionButton"
|
||||
Grid.Row="2"
|
||||
Padding="10,0"
|
||||
Text="{Binding MainActionText}"
|
||||
Command="{Binding MainActionCommand}"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="Start" />
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Text="{u:I18n ResendCode}"
|
||||
Padding="10,0"
|
||||
Command="{Binding RequestOTPCommand}"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="Start"/>
|
||||
</Grid>
|
||||
</ScrollView>
|
||||
</ContentPage.Content>
|
||||
</pages:BaseContentPage>
|
||||
49
src/Core/Pages/Accounts/VerificationCodePage.xaml.cs
Normal file
49
src/Core/Pages/Accounts/VerificationCodePage.xaml.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Bit.App.Pages.Accounts
|
||||
{
|
||||
public partial class VerificationCodePage : BaseContentPage
|
||||
{
|
||||
VerificationCodeViewModel _vm;
|
||||
|
||||
public VerificationCodePage(string mainActionText, bool mainActionStyleDanger)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_vm = BindingContext as VerificationCodeViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.MainActionText = mainActionText;
|
||||
|
||||
_mainActionButton.StyleClass = new[]
|
||||
{
|
||||
mainActionStyleDanger ? "btn-danger" : "btn-primary"
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
base.OnPropertyChanged(propertyName);
|
||||
|
||||
if (propertyName == nameof(VerificationCodeViewModel.ShowPassword))
|
||||
{
|
||||
RequestFocus(_secret);
|
||||
}
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
RequestFocus(_secret);
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
177
src/Core/Pages/Accounts/VerificationCodeViewModel.cs
Normal file
177
src/Core/Pages/Accounts/VerificationCodeViewModel.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
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 VerificationCodeViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IVerificationActionsFlowHelper _verificationActionsFlowHelper;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private bool _showPassword;
|
||||
private string _secret, _mainActionText, _sendCodeStatus;
|
||||
|
||||
public VerificationCodeViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>("userVerificationService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_verificationActionsFlowHelper = ServiceContainer.Resolve<IVerificationActionsFlowHelper>("verificationActionsFlowHelper");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
PageTitle = AppResources.VerificationCode;
|
||||
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
MainActionCommand = new AsyncCommand(MainActionAsync, allowsMultipleExecutions: false);
|
||||
RequestOTPCommand = new AsyncCommand(RequestOTPAsync, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new string[] { nameof(ShowPasswordIcon) });
|
||||
}
|
||||
|
||||
public string Secret
|
||||
{
|
||||
get => _secret;
|
||||
set => SetProperty(ref _secret, value);
|
||||
}
|
||||
|
||||
public string MainActionText
|
||||
{
|
||||
get => _mainActionText;
|
||||
set => SetProperty(ref _mainActionText, value);
|
||||
}
|
||||
|
||||
public string SendCodeStatus
|
||||
{
|
||||
get => _sendCodeStatus;
|
||||
set => SetProperty(ref _sendCodeStatus, value);
|
||||
}
|
||||
|
||||
public ICommand TogglePasswordCommand { get; }
|
||||
|
||||
public ICommand MainActionCommand { get; }
|
||||
|
||||
public ICommand RequestOTPCommand { get; }
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
|
||||
public void TogglePassword() => ShowPassword = !ShowPassword;
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await RequestOTPAsync();
|
||||
}
|
||||
|
||||
public async Task RequestOTPAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
|
||||
SendCodeStatus = AppResources.AnErrorOccurredWhileSendingAVerificationCodeToYourEmailPleaseTryAgain;
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.SendingCode);
|
||||
|
||||
await _apiService.PostAccountRequestOTP();
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
SendCodeStatus = AppResources.AVerificationCodeWasSentToYourEmail;
|
||||
|
||||
_platformUtilsService.ShowToast(null, null, AppResources.CodeSent);
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
SendCodeStatus = AppResources.AnErrorOccurredWhileSendingAVerificationCodeToYourEmailPleaseTryAgain;
|
||||
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(), AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
SendCodeStatus = AppResources.AnErrorOccurredWhileSendingAVerificationCodeToYourEmailPleaseTryAgain;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MainActionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Secret))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.EnterTheVerificationCodeThatWasSentToYourEmail, AppResources.AnErrorHasOccurred);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Verifying);
|
||||
|
||||
if (!await _userVerificationService.VerifyUser(Secret, VerificationType.OTP))
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
var parameters = _verificationActionsFlowHelper.GetParameters();
|
||||
parameters.Secret = Secret;
|
||||
parameters.VerificationType = VerificationType.OTP;
|
||||
await _verificationActionsFlowHelper.ExecuteAsync(parameters);
|
||||
|
||||
Secret = string.Empty;
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
166
src/Core/Pages/BaseContentPage.cs
Normal file
166
src/Core/Pages/BaseContentPage.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls.PlatformConfiguration;
|
||||
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
|
||||
using Microsoft.Maui.ApplicationModel.Communication;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class BaseContentPage : ContentPage
|
||||
{
|
||||
private IStateService _stateService;
|
||||
private IDeviceActionService _deviceActionService;
|
||||
|
||||
protected int ShowModalAnimationDelay = 400;
|
||||
protected int ShowPageAnimationDelay = 100;
|
||||
|
||||
public BaseContentPage()
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
On<iOS>().SetUseSafeArea(true);
|
||||
On<iOS>().SetModalPresentationStyle(UIModalPresentationStyle.FullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? LastPageAction { get; set; }
|
||||
|
||||
public bool IsThemeDirty { get; set; }
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
if (IsThemeDirty)
|
||||
{
|
||||
UpdateOnThemeChanged();
|
||||
}
|
||||
|
||||
await SaveActivityAsync();
|
||||
}
|
||||
|
||||
public bool DoOnce(Action action = null, int milliseconds = 1000)
|
||||
{
|
||||
if (LastPageAction.HasValue && (DateTime.UtcNow - LastPageAction.Value).TotalMilliseconds < milliseconds)
|
||||
{
|
||||
// Last action occurred recently.
|
||||
return false;
|
||||
}
|
||||
LastPageAction = DateTime.UtcNow;
|
||||
action?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual Task UpdateOnThemeChanged()
|
||||
{
|
||||
IsThemeDirty = false;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected void SetActivityIndicator(ContentView targetView = null)
|
||||
{
|
||||
var indicator = new ActivityIndicator
|
||||
{
|
||||
IsRunning = true,
|
||||
VerticalOptions = LayoutOptions.CenterAndExpand,
|
||||
HorizontalOptions = LayoutOptions.Center,
|
||||
Color = ThemeManager.GetResourceColor("PrimaryColor"),
|
||||
};
|
||||
if (targetView != null)
|
||||
{
|
||||
targetView.Content = indicator;
|
||||
}
|
||||
else
|
||||
{
|
||||
Content = indicator;
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task LoadOnAppearedAsync(View sourceView, bool fromModal, Func<Task> workFunction,
|
||||
ContentView targetView = null)
|
||||
{
|
||||
async Task DoWorkAsync()
|
||||
{
|
||||
await workFunction.Invoke();
|
||||
if (sourceView != null)
|
||||
{
|
||||
if (targetView != null)
|
||||
{
|
||||
targetView.Content = sourceView;
|
||||
}
|
||||
else
|
||||
{
|
||||
Content = sourceView;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
{
|
||||
await DoWorkAsync();
|
||||
return;
|
||||
}
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(fromModal ? ShowModalAnimationDelay : ShowPageAnimationDelay);
|
||||
Device.BeginInvokeOnMainThread(async () => await DoWorkAsync());
|
||||
});
|
||||
}
|
||||
|
||||
protected void RequestFocus(InputView input)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(ShowModalAnimationDelay);
|
||||
Device.BeginInvokeOnMainThread(() => input.Focus());
|
||||
});
|
||||
}
|
||||
|
||||
protected async Task<bool> ShowAccountSwitcherAsync()
|
||||
{
|
||||
return await _stateService.GetActiveUserIdAsync() != null;
|
||||
}
|
||||
|
||||
protected async Task<AvatarImageSource> GetAvatarImageSourceAsync(bool useCurrentActiveAccount = true)
|
||||
{
|
||||
if (useCurrentActiveAccount)
|
||||
{
|
||||
var user = await _stateService.GetActiveUserCustomDataAsync(a => (a?.Profile?.UserId, a?.Profile?.Name, a?.Profile?.Email, a?.Profile?.AvatarColor));
|
||||
return new AvatarImageSource(user.UserId, user.Name, user.Email, user.AvatarColor);
|
||||
}
|
||||
return new AvatarImageSource();
|
||||
}
|
||||
|
||||
private void SetServices()
|
||||
{
|
||||
if (_stateService == null)
|
||||
{
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
}
|
||||
if (_deviceActionService == null)
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveActivityAsync()
|
||||
{
|
||||
SetServices();
|
||||
if (await _stateService.GetActiveUserIdAsync() == null)
|
||||
{
|
||||
// Fresh install and/or all users logged out won't have an active user, skip saving last active time
|
||||
return;
|
||||
}
|
||||
|
||||
await _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Core/Pages/BaseModalContentPage.cs
Normal file
18
src/Core/Pages/BaseModalContentPage.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class BaseModalContentPage : BaseContentPage
|
||||
{
|
||||
public BaseModalContentPage()
|
||||
{
|
||||
}
|
||||
|
||||
protected void PopModal_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/Core/Pages/BaseViewModel.cs
Normal file
75
src/Core/Pages/BaseViewModel.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.Networking;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public abstract class BaseViewModel : ExtendedViewModel
|
||||
{
|
||||
private string _pageTitle = string.Empty;
|
||||
private AvatarImageSource _avatar;
|
||||
private LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>();
|
||||
private LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
|
||||
public string PageTitle
|
||||
{
|
||||
get => _pageTitle;
|
||||
set => SetProperty(ref _pageTitle, value);
|
||||
}
|
||||
|
||||
public AvatarImageSource AvatarImageSource
|
||||
{
|
||||
get => _avatar ?? new AvatarImageSource();
|
||||
set => SetProperty(ref _avatar, value);
|
||||
}
|
||||
|
||||
public ContentPage Page { get; set; }
|
||||
|
||||
protected void HandleException(Exception ex, string message = null)
|
||||
{
|
||||
if (ex is ApiException apiException && apiException.Error != null)
|
||||
{
|
||||
message = apiException.Error.GetSingleMessage();
|
||||
}
|
||||
|
||||
Microsoft.Maui.ApplicationModel.MainThread.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
await _deviceActionService.Value.HideLoadingAsync();
|
||||
await _platformUtilsService.Value.ShowDialogAsync(message ?? AppResources.GenericErrorMessage);
|
||||
}).FireAndForget();
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
|
||||
protected AsyncCommand CreateDefaultAsyncCommnad(Func<Task> execute, Func<bool> canExecute = null)
|
||||
{
|
||||
return new AsyncCommand(execute,
|
||||
canExecute,
|
||||
ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
protected async Task<bool> HasConnectivityAsync()
|
||||
{
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.Value.ShowDialogAsync(
|
||||
AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/Core/Pages/CaptchaProtectedViewModel.cs
Normal file
86
src/Core/Pages/CaptchaProtectedViewModel.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Microsoft.Maui.Authentication;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public abstract class CaptchaProtectedViewModel : BaseViewModel
|
||||
{
|
||||
protected abstract II18nService i18nService { get; }
|
||||
protected abstract IEnvironmentService environmentService { get; }
|
||||
protected abstract IDeviceActionService deviceActionService { get; }
|
||||
protected abstract IPlatformUtilsService platformUtilsService { get; }
|
||||
protected string _captchaToken = null;
|
||||
|
||||
protected async Task<bool> HandleCaptchaAsync(string captchaSiteKey, bool needsCaptcha, Func<Task> onSuccess)
|
||||
{
|
||||
if (!needsCaptcha)
|
||||
{
|
||||
_captchaToken = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await HandleCaptchaAsync(captchaSiteKey))
|
||||
{
|
||||
await onSuccess();
|
||||
_captchaToken = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async Task<bool> HandleCaptchaAsync(string CaptchaSiteKey)
|
||||
{
|
||||
var callbackUri = "bitwarden://captcha-callback";
|
||||
var data = AppHelpers.EncodeDataParameter(new
|
||||
{
|
||||
siteKey = CaptchaSiteKey,
|
||||
locale = i18nService.Culture.TwoLetterISOLanguageName,
|
||||
callbackUri = callbackUri,
|
||||
captchaRequiredText = AppResources.CaptchaRequired,
|
||||
});
|
||||
|
||||
var url = environmentService.GetWebVaultUrl() +
|
||||
"/captcha-mobile-connector.html?" +
|
||||
"data=" + data +
|
||||
"&parent=" + Uri.EscapeDataString(callbackUri) +
|
||||
"&v=1";
|
||||
|
||||
WebAuthenticatorResult authResult = null;
|
||||
bool cancelled = false;
|
||||
try
|
||||
{
|
||||
// PrefersEphemeralWebBrowserSession should be false to allow access to the hCaptcha accessibility
|
||||
// cookie set in the default browser
|
||||
// https://www.hcaptcha.com/accessibility
|
||||
var options = new WebAuthenticatorOptions
|
||||
{
|
||||
Url = new Uri(url),
|
||||
CallbackUrl = new Uri(callbackUri),
|
||||
PrefersEphemeralWebBrowserSession = false,
|
||||
};
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(options);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
await deviceActionService.HideLoadingAsync();
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
if (cancelled == false && authResult != null &&
|
||||
authResult.Properties.TryGetValue("token", out _captchaToken))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
await platformUtilsService.ShowDialogAsync(AppResources.CaptchaFailed,
|
||||
AppResources.CaptchaRequired);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Core/Pages/CollectionViewModel.cs
Normal file
16
src/Core/Pages/CollectionViewModel.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class CollectionViewModel : ExtendedViewModel
|
||||
{
|
||||
private bool _checked;
|
||||
|
||||
public Core.Models.View.CollectionView Collection { get; set; }
|
||||
public bool Checked
|
||||
{
|
||||
get => _checked;
|
||||
set => SetProperty(ref _checked, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/Core/Pages/Generator/GeneratorHistoryPage.xaml
Normal file
102
src/Core/Pages/Generator/GeneratorHistoryPage.xaml
Normal file
@@ -0,0 +1,102 @@
|
||||
<?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.GeneratorHistoryPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:domain="clr-namespace:Bit.Core.Models.Domain"
|
||||
xmlns:core="clr-namespace:Bit.Core"
|
||||
x:DataType="pages:GeneratorHistoryPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:GeneratorHistoryPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:DateTimeConverter x:Key="dateTime" />
|
||||
<u:ColoredPasswordConverter x:Key="coloredPassword" />
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Clear}"
|
||||
Clicked="Clear_Clicked"
|
||||
Order="Secondary"
|
||||
x:Name="_clearItem"
|
||||
x:Key="clearItem"
|
||||
AutomationId="ClearPasswordList" />
|
||||
<ToolbarItem IconImageSource="more_vert.png"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Options}"
|
||||
Clicked="More_Clicked"
|
||||
x:Name="_moreItem"
|
||||
x:Key="moreItem" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<StackLayout x:Name="_mainLayout">
|
||||
<Label IsVisible="{Binding ShowNoData}"
|
||||
Text="{u:I18n NoPasswordsToList}"
|
||||
Margin="20, 0"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoPasswordsDisplayedLabel"></Label>
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding History}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Generator History Page">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="domain:GeneratedPasswordHistory">
|
||||
<Grid
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="10"
|
||||
AutomationId="GeneratedPasswordRow">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<controls:MonoLabel LineBreakMode="CharacterWrap"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform, text-html"
|
||||
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}"
|
||||
AutomationId="GeneratedPasswordValue" />
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}"
|
||||
AutomationId="GeneratedPasswordDateLabel" />
|
||||
<controls:IconButton
|
||||
StyleClass="list-row-button, list-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Paste}}"
|
||||
Command="{Binding BindingContext.CopyCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n CopyPassword}"
|
||||
AutomationId="CopyPasswordValueButton" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
76
src/Core/Pages/Generator/GeneratorHistoryPage.xaml.cs
Normal file
76
src/Core/Pages/Generator/GeneratorHistoryPage.xaml.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Styles;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class GeneratorHistoryPage : BaseContentPage, IThemeDirtablePage
|
||||
{
|
||||
private GeneratorHistoryPageViewModel _vm;
|
||||
|
||||
public GeneratorHistoryPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
SetActivityIndicator();
|
||||
_vm = BindingContext as GeneratorHistoryPageViewModel;
|
||||
_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)
|
||||
{
|
||||
ToolbarItems.Add(_closeItem);
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolbarItems.Add(_clearItem);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
await LoadOnAppearedAsync(_mainLayout, true, async () =>
|
||||
{
|
||||
await _vm.InitAsync();
|
||||
});
|
||||
}
|
||||
|
||||
private async void Clear_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
await _vm.ClearAsync();
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
null, AppResources.Clear);
|
||||
if (selection == AppResources.Clear)
|
||||
{
|
||||
await _vm.ClearAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task UpdateOnThemeChanged()
|
||||
{
|
||||
await base.UpdateOnThemeChanged();
|
||||
|
||||
await _vm?.UpdateOnThemeChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/Core/Pages/Generator/GeneratorHistoryPageViewModel.cs
Normal file
79
src/Core/Pages/Generator/GeneratorHistoryPageViewModel.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class GeneratorHistoryPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private bool _showNoData;
|
||||
|
||||
public GeneratorHistoryPageViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
PageTitle = AppResources.PasswordHistory;
|
||||
History = new ExtendedObservableCollection<GeneratedPasswordHistory>();
|
||||
CopyCommand = new Command<GeneratedPasswordHistory>(CopyAsync);
|
||||
}
|
||||
|
||||
public Command CopyCommand { get; set; }
|
||||
public ExtendedObservableCollection<GeneratedPasswordHistory> History { get; set; }
|
||||
|
||||
public bool ShowNoData
|
||||
{
|
||||
get => _showNoData;
|
||||
set => SetProperty(ref _showNoData, value);
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var history = await _passwordGenerationService.GetHistoryAsync();
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
History.ResetWithRange(history ?? new List<GeneratedPasswordHistory>());
|
||||
ShowNoData = History.Count == 0;
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ClearAsync()
|
||||
{
|
||||
History.ResetWithRange(new List<GeneratedPasswordHistory>());
|
||||
ShowNoData = true;
|
||||
await _passwordGenerationService.ClearAsync();
|
||||
}
|
||||
|
||||
private async void CopyAsync(GeneratedPasswordHistory ph)
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(ph.Password);
|
||||
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
|
||||
}
|
||||
|
||||
public async Task UpdateOnThemeChanged()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Device.InvokeOnMainThreadAsync(() => History.ResetWithRange(new List<GeneratedPasswordHistory>()));
|
||||
|
||||
await InitAsync();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
549
src/Core/Pages/Generator/GeneratorPage.xaml
Normal file
549
src/Core/Pages/Generator/GeneratorPage.xaml
Normal file
@@ -0,0 +1,549 @@
|
||||
<?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.GeneratorPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:core="clr-namespace:Bit.Core"
|
||||
xmlns:enums="clr-namespace:Bit.Core.Enums"
|
||||
x:DataType="pages:GeneratorPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:GeneratorPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:LocalizableEnumConverter x:Key="localizableEnum" />
|
||||
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
|
||||
<xct:EnumToBoolConverter x:Key="enumToBool"/>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Select}"
|
||||
Clicked="Select_Clicked"
|
||||
Order="Primary"
|
||||
x:Name="_selectItem"
|
||||
x:Key="selectItem" />
|
||||
<ToolbarItem Text="{u:I18n PasswordHistory}"
|
||||
Clicked="History_Clicked"
|
||||
Order="Secondary"
|
||||
x:Name="_historyItem"
|
||||
x:Key="historyItem" />
|
||||
<ToolbarItem IconImageSource="more_vert.png"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Options}"
|
||||
Clicked="More_Clicked"
|
||||
x:Name="_moreItem"
|
||||
x:Key="moreItem" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<!--WORKAROUND: Wrapped in a ContentView to fix bottom screen but when using large font size on Android.
|
||||
Check when https://github.com/xamarin/Xamarin.Forms/pull/15076 is released that may fix this without wrapping
|
||||
in ContentView.-->
|
||||
<ContentView>
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Spacing="0"
|
||||
Padding="10,0">
|
||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||
Margin="0, 12, 0, 0"
|
||||
Padding="10,0"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="PasswordGeneratorPolicyInEffectLabel" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-row"
|
||||
RowDefinitions="Auto"
|
||||
ColumnDefinitions="*,Auto,Auto">
|
||||
<controls:MonoLabel
|
||||
x:Name="lblPassword"
|
||||
StyleClass="text-lg, text-html"
|
||||
Text="{Binding ColoredPassword, Mode=OneWay}"
|
||||
Margin="0, 20"
|
||||
AutomationId="GeneratedPasswordLabel" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n CopyPassword}"
|
||||
AutomationId="CopyValueButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
||||
Command="{Binding RegenerateCommand}"
|
||||
Grid.Column="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n GeneratePassword}"
|
||||
AutomationId="RegenerateValueButton" />
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsUsername}"
|
||||
StyleClass="box-row"
|
||||
RowDefinitions="Auto"
|
||||
ColumnDefinitions="*,Auto,Auto">
|
||||
<controls:MonoLabel
|
||||
x:Name="lblUsername"
|
||||
StyleClass="text-lg, text-html"
|
||||
Text="{Binding ColoredUsername, Mode=OneWay}"
|
||||
Margin="0, 20"
|
||||
HorizontalOptions="Start"
|
||||
AutomationId="GeneratedPasswordLabel" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n CopyUsername}"
|
||||
AutomationId="CopyValueButton" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
||||
Command="{Binding RegenerateUsernameCommand}"
|
||||
Grid.Column="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n GenerateUsername}"
|
||||
AutomationId="RegenerateValueButton" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"/>
|
||||
<StackLayout StyleClass="box"
|
||||
IsVisible="{Binding ShowTypePicker}"
|
||||
Padding="0,10">
|
||||
<Label
|
||||
Text="{u:I18n WhatWouldYouLikeToGenerate}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_typePicker"
|
||||
ItemsSource="{Binding GeneratorTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding GeneratorTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="GeneratorTypePicker" />
|
||||
</StackLayout>
|
||||
<Label Text="{u:I18n Options, Header=True}"
|
||||
StyleClass="box-header, box-header-platform"
|
||||
Margin="0,10,0,0"/>
|
||||
<!--USERNAME OPTIONS-->
|
||||
<StackLayout IsVisible="{Binding IsUsername}">
|
||||
<StackLayout Orientation="Horizontal">
|
||||
<Label
|
||||
Text="{u:I18n UsernameType}"
|
||||
StyleClass="box-label"
|
||||
VerticalOptions="Center"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.QuestionCircle}}"
|
||||
Command="{Binding UsernameTypePromptHelpCommand}"
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n UsernamePromptHelpLink}"
|
||||
VerticalOptions="Center"/>
|
||||
</StackLayout>
|
||||
<Picker
|
||||
x:Name="_usernameTypePicker"
|
||||
ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding UsernameTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="UsernameTypePicker" />
|
||||
<Label
|
||||
StyleClass="box-footer-label"
|
||||
Text="{Binding UsernameTypeDescriptionLabel}" />
|
||||
<!--PLUS ADDRESSED EMAIL OPTIONS-->
|
||||
<StackLayout StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.PlusAddressedEmail}}">
|
||||
<Label Text="{u:I18n EmailRequiredParenthesis}"
|
||||
StyleClass="box-label" />
|
||||
<Entry x:Name="_plusAddressedEmailEntry"
|
||||
Text="{Binding PlusAddressedEmail}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="PlusAddressedEmailEntry" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n EmailType}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"/>
|
||||
<Picker IsVisible="{Binding ShowUsernameEmailType}"
|
||||
x:Name="_plusAddressedEmailTypePicker"
|
||||
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding PlusAddressedEmailTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n Website}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{Binding EmailWebsite}"
|
||||
StyleClass="box-value" />
|
||||
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
|
||||
StyleClass="box-row-separator"
|
||||
Margin="0,10,0,0" />
|
||||
</StackLayout>
|
||||
<!--CATCH-ALL EMAIL OPTIONS-->
|
||||
<StackLayout StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.CatchAllEmail}}">
|
||||
<Label
|
||||
Text="{u:I18n DomainNameRequiredParenthesis}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_catchAllEmailDomainNameEntry"
|
||||
Text="{Binding CatchAllEmailDomain}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="CatchAllEmailDomainEntry" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n EmailType}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"/>
|
||||
<Picker IsVisible="{Binding ShowUsernameEmailType}"
|
||||
x:Name="_catchallEmailTypePicker"
|
||||
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding CatchAllEmailTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n Website}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{Binding EmailWebsite}"
|
||||
StyleClass="box-value"/>
|
||||
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
|
||||
StyleClass="box-row-separator"
|
||||
Margin="0,10,0,0"/>
|
||||
</StackLayout>
|
||||
<!--FORWARDED EMAIL OPTIONS-->
|
||||
<StackLayout StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.ForwardedEmailAlias}}">
|
||||
<Label
|
||||
Text="{u:I18n Service}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_serviceTypePicker"
|
||||
ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding ForwardedEmailServiceSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="ServiceTypePicker" />
|
||||
<Grid
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Margin="0,10,0,0"
|
||||
Text="{Binding ForwardedEmailApiSecretLabel}"
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
Text="{Binding ForwardedEmailApiSecret}"
|
||||
IsPassword="{Binding ShowForwardedEmailApiSecret, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
AutomationId="ForwardedEmailApiSecretEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowForwardedEmailApiSecret, Converter={StaticResource inverseBool, iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
AutomationId="ShowForwardedEmailApiSecretButton" />
|
||||
</Grid>
|
||||
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||
Text="{u:I18n DomainNameRequiredParenthesis}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"/>
|
||||
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||
x:Name="_anonAddyDomainNameEntry"
|
||||
Text="{Binding AddyIoDomainName}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="AnonAddyDomainNameEntry" />
|
||||
</StackLayout>
|
||||
<!--RANDOM WORD OPTIONS-->
|
||||
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
|
||||
<Label
|
||||
Text="{u:I18n Capitalize}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding CapitalizeRandomWordUsername}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationId="CapitalizeRandomWordUsernameToggle" />
|
||||
</Grid>
|
||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
||||
StyleClass="box-row-separator" />
|
||||
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
|
||||
<Label
|
||||
Text="{u:I18n IncludeNumber}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding IncludeNumberRandomWordUsername}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationId="IncludeNumberRandomWordUsernameToggle" />
|
||||
</Grid>
|
||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
||||
StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
<!--PASSWORD OPTIONS-->
|
||||
<StackLayout IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
Text="{u:I18n PasswordType}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_passwordTypePicker"
|
||||
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding PasswordTypeSelectedIndex}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="PasswordTypePicker" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0"
|
||||
Padding="0"
|
||||
IsVisible="{Binding IsPassword, Converter={StaticResource inverseBool}}">
|
||||
<StackLayout StyleClass="box-row, box-row-stepper">
|
||||
<Label
|
||||
Text="{u:I18n NumberOfWords}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
<Label
|
||||
Text="{Binding NumWords}"
|
||||
StyleClass="box-sub-label"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="NumberOfWordsLabel" />
|
||||
<controls:ExtendedStepper
|
||||
Value="{Binding NumWords}"
|
||||
Maximum="20"
|
||||
Minimum="3"
|
||||
Increment="1"
|
||||
AutomationId="NumberOfWordsStepper" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
Text="{u:I18n WordSeparator}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
Text="{Binding WordSeparator}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
StyleClass="box-value"
|
||||
AutomationId="WordSeparatorEntry">
|
||||
<Entry.Effects>
|
||||
<effects:NoEmojiKeyboardEffect />
|
||||
</Entry.Effects>
|
||||
</Entry>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n Capitalize}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Capitalize}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.Capitalize,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationId="CapitalizePassphraseToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n IncludeNumber}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding IncludeNumber}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.IncludeNumber,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationId="IncludeNumbersToggle" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding IsPassword}">
|
||||
<StackLayout StyleClass="box-row, box-row-slider">
|
||||
<Label
|
||||
Text="{u:I18n Length}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="CenterAndExpand" />
|
||||
<Label
|
||||
Text="{Binding Length}"
|
||||
StyleClass="box-sub-label"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
WidthRequest="50"
|
||||
AutomationId="PasswordLengthLabel" />
|
||||
<controls:ExtendedSlider
|
||||
DragCompleted="LengthSlider_DragCompleted"
|
||||
Value="{Binding Length}"
|
||||
AutomationProperties.HelpText="{Binding Length}"
|
||||
StyleClass="box-value"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
Maximum="128"
|
||||
Minimum="5"
|
||||
AutomationId="PasswordLengthSlider" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="A-Z"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n UppercaseAtoZ}"/>
|
||||
<Switch
|
||||
IsToggled="{Binding Uppercase}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseUppercase,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n UppercaseAtoZ}"
|
||||
AutomationId="UppercaseAtoZToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="a-z"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n LowercaseAtoZ}"/>
|
||||
<Switch
|
||||
IsToggled="{Binding Lowercase}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseLowercase,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n LowercaseAtoZ}"
|
||||
AutomationId="LowercaseAtoZToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="0-9"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n NumbersZeroToNine}"/>
|
||||
<Switch
|
||||
IsToggled="{Binding Number}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseNumbers,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n NumbersZeroToNine}"
|
||||
AutomationId="NumbersZeroToNineToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="!@#$%^&*"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n SpecialCharacters}"/>
|
||||
<Switch
|
||||
IsToggled="{Binding Special}"
|
||||
IsEnabled="{Binding EnforcedPolicyOptions.UseSpecial,
|
||||
Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n SpecialCharacters}"
|
||||
AutomationId="SpecialCharactersToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-stepper">
|
||||
<Label
|
||||
Text="{u:I18n MinNumbers}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
<Label
|
||||
Text="{Binding MinNumber}"
|
||||
StyleClass="box-sub-label"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="MinNumberValueLabel" />
|
||||
<controls:ExtendedStepper
|
||||
Value="{Binding MinNumber}"
|
||||
Maximum="5"
|
||||
Minimum="0"
|
||||
Increment="1"
|
||||
AutomationId="MinNumberStepper" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-stepper">
|
||||
<Label
|
||||
Text="{u:I18n MinSpecial}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
<Label
|
||||
Text="{Binding MinSpecial}"
|
||||
StyleClass="box-sub-label"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="MinSpecialValueLabel" />
|
||||
<controls:ExtendedStepper
|
||||
Value="{Binding MinSpecial}"
|
||||
Maximum="5"
|
||||
Minimum="0"
|
||||
Increment="1"
|
||||
AutomationId="MinSpecialStepper" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n AvoidAmbiguousCharacters}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding AvoidAmbiguousChars}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End"
|
||||
AutomationId="AvoidAmbiguousCharsToggle" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</ContentView>
|
||||
</pages:BaseContentPage>
|
||||
163
src/Core/Pages/Generator/GeneratorPage.xaml.cs
Normal file
163
src/Core/Pages/Generator/GeneratorPage.xaml.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Styles;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class GeneratorPage : BaseContentPage, IThemeDirtablePage
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
|
||||
private GeneratorPageViewModel _vm;
|
||||
private readonly bool _fromTabPage;
|
||||
private readonly Action<string> _selectAction;
|
||||
private readonly TabsPage _tabsPage;
|
||||
|
||||
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false, AppOptions appOptions = null)
|
||||
{
|
||||
_tabsPage = tabsPage;
|
||||
InitializeComponent();
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
||||
_vm = BindingContext as GeneratorPageViewModel;
|
||||
_vm.Page = this;
|
||||
_fromTabPage = fromTabPage;
|
||||
_selectAction = selectAction;
|
||||
_vm.ShowTypePicker = fromTabPage;
|
||||
_vm.IsUsername = isUsernameGenerator;
|
||||
_vm.EmailWebsite = emailWebsite;
|
||||
_vm.EditMode = editMode;
|
||||
_vm.IosExtension = appOptions?.IosExtension ?? false;
|
||||
// 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
|
||||
var isIos = Device.RuntimePlatform == Device.iOS;
|
||||
if (selectAction != null)
|
||||
{
|
||||
if (isIos)
|
||||
{
|
||||
ToolbarItems.Add(_closeItem);
|
||||
}
|
||||
ToolbarItems.Add(_selectItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isIos)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolbarItems.Add(_historyItem);
|
||||
}
|
||||
}
|
||||
_typePicker.On<Microsoft.Maui.Controls.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_passwordTypePicker.On<Microsoft.Maui.Controls.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_usernameTypePicker.On<Microsoft.Maui.Controls.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_serviceTypePicker.On<Microsoft.Maui.Controls.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_plusAddressedEmailTypePicker.On<Microsoft.Maui.Controls.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_catchallEmailTypePicker.On<Microsoft.Maui.Controls.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
lblPassword.IsVisible = true;
|
||||
|
||||
if (!_fromTabPage)
|
||||
{
|
||||
await InitAsync();
|
||||
}
|
||||
|
||||
_broadcasterService.Subscribe(nameof(GeneratorPage), (message) =>
|
||||
{
|
||||
if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY)
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => _vm.RedrawPassword());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
|
||||
lblPassword.IsVisible = false;
|
||||
|
||||
_broadcasterService.Unsubscribe(nameof(GeneratorPage));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
null, AppResources.PasswordHistory);
|
||||
if (selection == AppResources.PasswordHistory)
|
||||
{
|
||||
var page = new GeneratorHistoryPage();
|
||||
await Navigation.PushModalAsync(new Microsoft.Maui.Controls.NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
private void Select_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
_selectAction?.Invoke(_vm.IsUsername ? _vm.Username : _vm.Password);
|
||||
}
|
||||
|
||||
private async void History_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
var page = new GeneratorHistoryPage();
|
||||
await Navigation.PushModalAsync(new Microsoft.Maui.Controls.NavigationPage(page));
|
||||
}
|
||||
|
||||
private async void LengthSlider_DragCompleted(object sender, EventArgs e)
|
||||
{
|
||||
await _vm.SliderChangedAsync();
|
||||
}
|
||||
|
||||
public override async Task UpdateOnThemeChanged()
|
||||
{
|
||||
await base.UpdateOnThemeChanged();
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
if (_vm != null)
|
||||
{
|
||||
if (_vm.IsUsername)
|
||||
{
|
||||
_vm.RedrawUsername();
|
||||
}
|
||||
else
|
||||
{
|
||||
_vm.RedrawPassword();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
864
src/Core/Pages/Generator/GeneratorPageViewModel.cs
Normal file
864
src/Core/Pages/Generator/GeneratorPageViewModel.cs
Normal file
@@ -0,0 +1,864 @@
|
||||
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.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class GeneratorPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IUsernameGenerationService _usernameGenerationService;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
|
||||
private PasswordGenerationOptions _options;
|
||||
private UsernameGenerationOptions _usernameOptions;
|
||||
private PasswordGeneratorPolicyOptions _enforcedPolicyOptions;
|
||||
private string _password;
|
||||
private bool _isPassword;
|
||||
private bool _isUsername;
|
||||
private bool _uppercase;
|
||||
private bool _lowercase;
|
||||
private bool _number;
|
||||
private bool _special;
|
||||
private bool _allowAmbiguousChars;
|
||||
private int _minNumber;
|
||||
private int _minSpecial;
|
||||
private int _length = 5;
|
||||
private int _numWords = 3;
|
||||
private string _wordSeparator;
|
||||
private bool _capitalize;
|
||||
private bool _includeNumber;
|
||||
private string _username;
|
||||
private GeneratorType _generatorTypeSelected;
|
||||
private int _passwordTypeSelectedIndex;
|
||||
private bool _doneIniting;
|
||||
private bool _showTypePicker;
|
||||
private string _emailWebsite;
|
||||
private bool _showForwardedEmailApiSecret;
|
||||
private bool _editMode;
|
||||
|
||||
public GeneratorPageViewModel()
|
||||
{
|
||||
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>();
|
||||
_usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>();
|
||||
_tokenService = ServiceContainer.Resolve<ITokenService>();
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
|
||||
PageTitle = AppResources.Generator;
|
||||
GeneratorTypeOptions = new List<GeneratorType> {
|
||||
GeneratorType.Password,
|
||||
GeneratorType.Username
|
||||
};
|
||||
PasswordTypeOptions = new List<string> { AppResources.Password, AppResources.Passphrase };
|
||||
|
||||
UsernameTypeOptions = new List<UsernameType> {
|
||||
UsernameType.PlusAddressedEmail,
|
||||
UsernameType.CatchAllEmail,
|
||||
UsernameType.ForwardedEmailAlias,
|
||||
UsernameType.RandomWord
|
||||
};
|
||||
|
||||
ForwardedEmailServiceTypeOptions = new List<ForwardedEmailServiceType> {
|
||||
ForwardedEmailServiceType.AnonAddy,
|
||||
ForwardedEmailServiceType.DuckDuckGo,
|
||||
ForwardedEmailServiceType.Fastmail,
|
||||
ForwardedEmailServiceType.FirefoxRelay,
|
||||
ForwardedEmailServiceType.SimpleLogin
|
||||
};
|
||||
|
||||
UsernameEmailTypeOptions = new List<UsernameEmailType>
|
||||
{
|
||||
UsernameEmailType.Random,
|
||||
UsernameEmailType.Website
|
||||
};
|
||||
|
||||
UsernameTypePromptHelpCommand = new Command(UsernameTypePromptHelp);
|
||||
RegenerateCommand = new AsyncCommand(RegenerateAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
|
||||
RegenerateUsernameCommand = new AsyncCommand(RegenerateUsernameAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
|
||||
ToggleForwardedEmailHiddenValueCommand = new Command(() => ShowForwardedEmailApiSecret = !ShowForwardedEmailApiSecret);
|
||||
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
|
||||
CloseCommand = new AsyncCommand(CloseAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public List<GeneratorType> GeneratorTypeOptions { get; set; }
|
||||
public List<string> PasswordTypeOptions { get; set; }
|
||||
public List<UsernameType> UsernameTypeOptions { get; set; }
|
||||
public List<ForwardedEmailServiceType> ForwardedEmailServiceTypeOptions { get; set; }
|
||||
public List<UsernameEmailType> UsernameEmailTypeOptions { get; set; }
|
||||
|
||||
public Command UsernameTypePromptHelpCommand { get; set; }
|
||||
public ICommand RegenerateCommand { get; set; }
|
||||
public ICommand RegenerateUsernameCommand { get; set; }
|
||||
public ICommand ToggleForwardedEmailHiddenValueCommand { get; set; }
|
||||
public ICommand CopyCommand { get; set; }
|
||||
public ICommand CloseCommand { get; set; }
|
||||
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
set => SetProperty(ref _password, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ColoredPassword)
|
||||
});
|
||||
}
|
||||
|
||||
public string Username
|
||||
{
|
||||
get => _username;
|
||||
set => SetProperty(ref _username, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ColoredUsername)
|
||||
});
|
||||
}
|
||||
|
||||
public string ColoredPassword => GeneratedValueFormatter.Format(Password);
|
||||
public string ColoredUsername => GeneratedValueFormatter.Format(Username);
|
||||
|
||||
public bool IsPassword
|
||||
{
|
||||
get => _isPassword;
|
||||
set => SetProperty(ref _isPassword, value);
|
||||
}
|
||||
|
||||
public bool IsUsername
|
||||
{
|
||||
get => _isUsername;
|
||||
set => SetProperty(ref _isUsername, value);
|
||||
}
|
||||
|
||||
public bool IosExtension { get; set; }
|
||||
|
||||
public bool ShowTypePicker
|
||||
{
|
||||
get => _showTypePicker;
|
||||
set => SetProperty(ref _showTypePicker, value);
|
||||
}
|
||||
|
||||
public bool EditMode
|
||||
{
|
||||
get => _editMode;
|
||||
set => SetProperty(ref _editMode, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowUsernameEmailType)
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShowUsernameEmailType
|
||||
{
|
||||
get => !string.IsNullOrWhiteSpace(EmailWebsite);
|
||||
}
|
||||
|
||||
public int Length
|
||||
{
|
||||
get => _length;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _length, value))
|
||||
{
|
||||
_options.Length = value;
|
||||
var task = SliderInputAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Uppercase
|
||||
{
|
||||
get => _uppercase;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _uppercase, value))
|
||||
{
|
||||
_options.Uppercase = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Lowercase
|
||||
{
|
||||
get => _lowercase;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _lowercase, value))
|
||||
{
|
||||
_options.Lowercase = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Number
|
||||
{
|
||||
get => _number;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _number, value))
|
||||
{
|
||||
_options.Number = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Special
|
||||
{
|
||||
get => _special;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _special, value))
|
||||
{
|
||||
_options.Special = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AllowAmbiguousChars
|
||||
{
|
||||
get => _allowAmbiguousChars;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _allowAmbiguousChars, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(AvoidAmbiguousChars)
|
||||
}))
|
||||
{
|
||||
_options.AllowAmbiguousChar = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AvoidAmbiguousChars
|
||||
{
|
||||
get => !AllowAmbiguousChars;
|
||||
set => AllowAmbiguousChars = !value;
|
||||
}
|
||||
|
||||
public int MinNumber
|
||||
{
|
||||
get => _minNumber;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _minNumber, value))
|
||||
{
|
||||
_options.MinNumber = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int MinSpecial
|
||||
{
|
||||
get => _minSpecial;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _minSpecial, value))
|
||||
{
|
||||
_options.MinSpecial = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int NumWords
|
||||
{
|
||||
get => _numWords;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _numWords, value))
|
||||
{
|
||||
_options.NumWords = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string WordSeparator
|
||||
{
|
||||
get => _wordSeparator;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var val = value.Trim();
|
||||
if (SetProperty(ref _wordSeparator, val))
|
||||
{
|
||||
_options.WordSeparator = val;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Capitalize
|
||||
{
|
||||
get => _capitalize;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _capitalize, value))
|
||||
{
|
||||
_options.Capitalize = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IncludeNumber
|
||||
{
|
||||
get => _includeNumber;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _includeNumber, value))
|
||||
{
|
||||
_options.Number = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string PlusAddressedEmail
|
||||
{
|
||||
get => _usernameOptions.PlusAddressedEmail;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.PlusAddressedEmail != value)
|
||||
{
|
||||
_usernameOptions.PlusAddressedEmail = value;
|
||||
TriggerPropertyChanged(nameof(PlusAddressedEmail));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PasswordGeneratorPolicyOptions EnforcedPolicyOptions
|
||||
{
|
||||
get => _enforcedPolicyOptions;
|
||||
set => SetProperty(ref _enforcedPolicyOptions, value,
|
||||
additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(IsPolicyInEffect)
|
||||
});
|
||||
}
|
||||
|
||||
public bool IsPolicyInEffect => _enforcedPolicyOptions.InEffect();
|
||||
|
||||
public GeneratorType GeneratorTypeSelected
|
||||
{
|
||||
get => _generatorTypeSelected;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _generatorTypeSelected, value))
|
||||
{
|
||||
IsUsername = value == GeneratorType.Username;
|
||||
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
|
||||
SaveOptionsAsync().FireAndForget();
|
||||
SaveUsernameOptionsAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int PasswordTypeSelectedIndex
|
||||
{
|
||||
get => _passwordTypeSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _passwordTypeSelectedIndex, value))
|
||||
{
|
||||
IsPassword = value == 0;
|
||||
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
|
||||
SaveOptionsAsync().FireAndForget();
|
||||
SaveUsernameOptionsAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UsernameType UsernameTypeSelected
|
||||
{
|
||||
get => _usernameOptions.Type;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.Type != value)
|
||||
{
|
||||
_usernameOptions.Type = value;
|
||||
Username = Constants.DefaultUsernameGenerated;
|
||||
TriggerPropertyChanged(nameof(UsernameTypeSelected), new string[] { nameof(UsernameTypeDescriptionLabel) });
|
||||
SaveUsernameOptionsAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string UsernameTypeDescriptionLabel => GetUsernameTypeLabelDescription(UsernameTypeSelected);
|
||||
|
||||
public ForwardedEmailServiceType ForwardedEmailServiceSelected
|
||||
{
|
||||
get => _usernameOptions.ServiceType;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.ServiceType != value)
|
||||
{
|
||||
_usernameOptions.ServiceType = value;
|
||||
Username = Constants.DefaultUsernameGenerated;
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected), new string[]
|
||||
{
|
||||
nameof(ForwardedEmailApiSecret),
|
||||
nameof(ForwardedEmailApiSecretLabel)
|
||||
});
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string CatchAllEmailDomain
|
||||
{
|
||||
get => _usernameOptions.CatchAllEmailDomain;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.CatchAllEmailDomain != value)
|
||||
{
|
||||
_usernameOptions.CatchAllEmailDomain = value;
|
||||
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ForwardedEmailApiSecret
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (ForwardedEmailServiceSelected)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
return _usernameOptions.AnonAddyApiAccessToken;
|
||||
case ForwardedEmailServiceType.DuckDuckGo:
|
||||
return _usernameOptions.DuckDuckGoApiKey;
|
||||
case ForwardedEmailServiceType.Fastmail:
|
||||
return _usernameOptions.FastMailApiKey;
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
return _usernameOptions.FirefoxRelayApiAccessToken;
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
return _usernameOptions.SimpleLoginApiKey;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
bool changed = false;
|
||||
switch (ForwardedEmailServiceSelected)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
if (_usernameOptions.AnonAddyApiAccessToken != value)
|
||||
{
|
||||
_usernameOptions.AnonAddyApiAccessToken = value;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case ForwardedEmailServiceType.DuckDuckGo:
|
||||
if (_usernameOptions.DuckDuckGoApiKey != value)
|
||||
{
|
||||
_usernameOptions.DuckDuckGoApiKey = value;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case ForwardedEmailServiceType.Fastmail:
|
||||
if (_usernameOptions.FastMailApiKey != value)
|
||||
{
|
||||
_usernameOptions.FastMailApiKey = value;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
if (_usernameOptions.FirefoxRelayApiAccessToken != value)
|
||||
{
|
||||
_usernameOptions.FirefoxRelayApiAccessToken = value;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
if (_usernameOptions.SimpleLoginApiKey != value)
|
||||
{
|
||||
_usernameOptions.SimpleLoginApiKey = value;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailApiSecret));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ForwardedEmailApiSecretLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (ForwardedEmailServiceSelected)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
return AppResources.APIAccessToken;
|
||||
case ForwardedEmailServiceType.DuckDuckGo:
|
||||
case ForwardedEmailServiceType.Fastmail:
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
return AppResources.APIKeyRequiredParenthesis;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowForwardedEmailApiSecret
|
||||
{
|
||||
get
|
||||
{
|
||||
return _showForwardedEmailApiSecret;
|
||||
}
|
||||
set => SetProperty(ref _showForwardedEmailApiSecret, value);
|
||||
}
|
||||
|
||||
public string AddyIoDomainName
|
||||
{
|
||||
get => _usernameOptions.AnonAddyDomainName;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.AnonAddyDomainName != value)
|
||||
{
|
||||
_usernameOptions.AnonAddyDomainName = value;
|
||||
TriggerPropertyChanged(nameof(AddyIoDomainName));
|
||||
SaveUsernameOptionsAsync(false).FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CapitalizeRandomWordUsername
|
||||
{
|
||||
get => _usernameOptions.CapitalizeRandomWordUsername;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.CapitalizeRandomWordUsername != value)
|
||||
{
|
||||
_usernameOptions.CapitalizeRandomWordUsername = value;
|
||||
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
|
||||
SaveUsernameOptionsAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IncludeNumberRandomWordUsername
|
||||
{
|
||||
get => _usernameOptions.IncludeNumberRandomWordUsername;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.IncludeNumberRandomWordUsername != value)
|
||||
{
|
||||
_usernameOptions.IncludeNumberRandomWordUsername = value;
|
||||
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
|
||||
SaveUsernameOptionsAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UsernameEmailType PlusAddressedEmailTypeSelected
|
||||
{
|
||||
get => _usernameOptions.PlusAddressedEmailType;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.PlusAddressedEmailType != value)
|
||||
{
|
||||
_usernameOptions.PlusAddressedEmailType = value;
|
||||
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
|
||||
SaveUsernameOptionsAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UsernameEmailType CatchAllEmailTypeSelected
|
||||
{
|
||||
get => _usernameOptions.CatchAllEmailType;
|
||||
set
|
||||
{
|
||||
if (_usernameOptions.CatchAllEmailType != value)
|
||||
{
|
||||
_usernameOptions.CatchAllEmailType = value;
|
||||
TriggerPropertyChanged(nameof(CatchAllEmailTypeSelected));
|
||||
SaveUsernameOptionsAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string EmailWebsite
|
||||
{
|
||||
get => _emailWebsite;
|
||||
set => SetProperty(ref _emailWebsite, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowUsernameEmailType)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
(_options, EnforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync();
|
||||
LoadFromOptions();
|
||||
|
||||
_usernameOptions = await _usernameGenerationService.GetOptionsAsync();
|
||||
await _tokenService.PrepareTokenForDecodingAsync();
|
||||
_usernameOptions.PlusAddressedEmail = _tokenService.GetEmail();
|
||||
_usernameOptions.EmailWebsite = EmailWebsite;
|
||||
_usernameOptions.CatchAllEmailType = _usernameOptions.PlusAddressedEmailType = string.IsNullOrWhiteSpace(EmailWebsite) || !EditMode ? UsernameEmailType.Random : UsernameEmailType.Website;
|
||||
|
||||
if (!IsUsername)
|
||||
{
|
||||
await RegenerateAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (UsernameTypeSelected != UsernameType.ForwardedEmailAlias)
|
||||
{
|
||||
await RegenerateUsernameAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Username = Constants.DefaultUsernameGenerated;
|
||||
}
|
||||
}
|
||||
TriggerUsernamePropertiesChanged();
|
||||
|
||||
_doneIniting = true;
|
||||
}
|
||||
|
||||
public async Task RegenerateAsync()
|
||||
{
|
||||
Password = await _passwordGenerationService.GeneratePasswordAsync(_options);
|
||||
await _passwordGenerationService.AddHistoryAsync(Password);
|
||||
}
|
||||
|
||||
public async Task RegenerateUsernameAsync()
|
||||
{
|
||||
Username = await _usernameGenerationService.GenerateAsync(_usernameOptions);
|
||||
}
|
||||
|
||||
public void RedrawPassword()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_password))
|
||||
{
|
||||
TriggerPropertyChanged(nameof(ColoredPassword));
|
||||
}
|
||||
}
|
||||
|
||||
public void RedrawUsername()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_username))
|
||||
{
|
||||
TriggerPropertyChanged(nameof(ColoredUsername));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveOptionsAsync(bool regenerate = true)
|
||||
{
|
||||
if (!_doneIniting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SetOptions();
|
||||
_passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
|
||||
await _passwordGenerationService.SaveOptionsAsync(_options);
|
||||
|
||||
LoadFromOptions();
|
||||
if (regenerate)
|
||||
{
|
||||
await RegenerateAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveUsernameOptionsAsync(bool regenerate = true)
|
||||
{
|
||||
if (!_doneIniting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_usernameOptions.EmailWebsite = EmailWebsite;
|
||||
await _usernameGenerationService.SaveOptionsAsync(_usernameOptions);
|
||||
|
||||
if (regenerate && UsernameTypeSelected != UsernameType.ForwardedEmailAlias)
|
||||
{
|
||||
await RegenerateUsernameAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Username = Constants.DefaultUsernameGenerated;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SliderChangedAsync()
|
||||
{
|
||||
await SaveOptionsAsync(false);
|
||||
await _passwordGenerationService.AddHistoryAsync(Password);
|
||||
}
|
||||
|
||||
public async Task SliderInputAsync()
|
||||
{
|
||||
if (!_doneIniting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SetOptions();
|
||||
_passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
|
||||
Password = await _passwordGenerationService.GeneratePasswordAsync(_options);
|
||||
}
|
||||
|
||||
public async Task CopyAsync()
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(IsUsername ? Username : Password);
|
||||
_platformUtilsService.ShowToastForCopiedValue(IsUsername ? AppResources.Username : AppResources.Password);
|
||||
}
|
||||
|
||||
public void UsernameTypePromptHelp()
|
||||
{
|
||||
try
|
||||
{
|
||||
_platformUtilsService.LaunchUri("https://bitwarden.com/help/generator/#username-types");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
if (IosExtension)
|
||||
{
|
||||
_deviceActionService.CloseExtensionPopUp();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Page.Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadFromOptions()
|
||||
{
|
||||
AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault();
|
||||
PasswordTypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
|
||||
IsPassword = PasswordTypeSelectedIndex == 0;
|
||||
MinNumber = _options.MinNumber.GetValueOrDefault();
|
||||
MinSpecial = _options.MinSpecial.GetValueOrDefault();
|
||||
Special = _options.Special.GetValueOrDefault();
|
||||
Number = _options.Number.GetValueOrDefault();
|
||||
NumWords = _options.NumWords.GetValueOrDefault();
|
||||
WordSeparator = _options.WordSeparator;
|
||||
Uppercase = _options.Uppercase.GetValueOrDefault();
|
||||
Lowercase = _options.Lowercase.GetValueOrDefault();
|
||||
Length = _options.Length.GetValueOrDefault(5);
|
||||
Capitalize = _options.Capitalize.GetValueOrDefault();
|
||||
IncludeNumber = _options.IncludeNumber.GetValueOrDefault();
|
||||
}
|
||||
|
||||
private void TriggerUsernamePropertiesChanged()
|
||||
{
|
||||
TriggerPropertyChanged(nameof(CatchAllEmailTypeSelected));
|
||||
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
|
||||
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
|
||||
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailApiSecret));
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailApiSecretLabel));
|
||||
TriggerPropertyChanged(nameof(AddyIoDomainName));
|
||||
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
|
||||
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
|
||||
TriggerPropertyChanged(nameof(UsernameTypeSelected));
|
||||
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
|
||||
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
|
||||
TriggerPropertyChanged(nameof(PlusAddressedEmail));
|
||||
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
|
||||
TriggerPropertyChanged(nameof(UsernameTypeDescriptionLabel));
|
||||
TriggerPropertyChanged(nameof(EmailWebsite));
|
||||
}
|
||||
|
||||
private void SetOptions()
|
||||
{
|
||||
_options.AllowAmbiguousChar = AllowAmbiguousChars;
|
||||
_options.Type = PasswordTypeSelectedIndex == 1 ? PasswordGenerationOptions.TYPE_PASSPHRASE : PasswordGenerationOptions.TYPE_PASSWORD;
|
||||
_options.MinNumber = MinNumber;
|
||||
_options.MinSpecial = MinSpecial;
|
||||
_options.Special = Special;
|
||||
_options.NumWords = NumWords;
|
||||
_options.Number = Number;
|
||||
_options.WordSeparator = WordSeparator;
|
||||
_options.Uppercase = Uppercase;
|
||||
_options.Lowercase = Lowercase;
|
||||
_options.Length = Length;
|
||||
_options.Capitalize = Capitalize;
|
||||
_options.IncludeNumber = IncludeNumber;
|
||||
}
|
||||
|
||||
private async void OnSubmitException(Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
|
||||
string message = AppResources.GenericErrorMessage;
|
||||
|
||||
if (IsUsername && UsernameTypeSelected == UsernameType.ForwardedEmailAlias)
|
||||
{
|
||||
if (ex is ForwardedEmailInvalidSecretException)
|
||||
{
|
||||
message = ForwardedEmailServiceSelected == ForwardedEmailServiceType.AnonAddy || ForwardedEmailServiceSelected == ForwardedEmailServiceType.FirefoxRelay
|
||||
? AppResources.InvalidAPIToken
|
||||
: AppResources.InvalidAPIKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
message = string.Format(AppResources.UnknownXErrorMessage, ForwardedEmailServiceSelected);
|
||||
}
|
||||
}
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, message, AppResources.Ok));
|
||||
}
|
||||
|
||||
private string GetUsernameTypeLabelDescription(UsernameType value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case UsernameType.PlusAddressedEmail:
|
||||
return AppResources.PlusAddressedEmailDescription;
|
||||
case UsernameType.CatchAllEmail:
|
||||
return AppResources.CatchAllEmailDescription;
|
||||
case UsernameType.ForwardedEmailAlias:
|
||||
return AppResources.ForwardedEmailDescription;
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/Core/Pages/PickerViewModel.cs
Normal file
125
src/Core/Pages/PickerViewModel.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class PickerViewModel<TKey> : ExtendedViewModel
|
||||
{
|
||||
const string SELECTED_CHARACTER = "✓";
|
||||
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Func<TKey, Task<bool>> _onSelectionChangingAsync;
|
||||
private readonly string _title;
|
||||
|
||||
public Dictionary<TKey, string> _items;
|
||||
private TKey _selectedKey;
|
||||
private TKey _defaultSelectedKeyIfFailsToFind;
|
||||
private Func<TKey, Task> _afterSelectionChangedAsync;
|
||||
|
||||
public PickerViewModel(IDeviceActionService deviceActionService,
|
||||
ILogger logger,
|
||||
Func<TKey, Task<bool>> onSelectionChangingAsync,
|
||||
string title,
|
||||
Func<bool> canExecuteSelectOptionCommand = null,
|
||||
Action<Exception> onSelectOptionCommandException = null)
|
||||
{
|
||||
_deviceActionService = deviceActionService;
|
||||
_logger = logger;
|
||||
_onSelectionChangingAsync = onSelectionChangingAsync;
|
||||
_title = title;
|
||||
|
||||
SelectOptionCommand = new AsyncCommand(SelectOptionAsync, canExecuteSelectOptionCommand, onSelectOptionCommandException, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public AsyncCommand SelectOptionCommand { get; }
|
||||
|
||||
public TKey SelectedKey => _selectedKey;
|
||||
|
||||
public string SelectedValue
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_items.TryGetValue(_selectedKey, out var option))
|
||||
{
|
||||
return option;
|
||||
}
|
||||
|
||||
_selectedKey = _defaultSelectedKeyIfFailsToFind;
|
||||
return _items[_selectedKey];
|
||||
}
|
||||
}
|
||||
|
||||
public void Init(Dictionary<TKey, string> items, TKey currentSelectedKey, TKey defaultSelectedKeyIfFailsToFind, bool logIfKeyNotFound = true)
|
||||
{
|
||||
_items = items;
|
||||
_defaultSelectedKeyIfFailsToFind = defaultSelectedKeyIfFailsToFind;
|
||||
|
||||
Select(currentSelectedKey, logIfKeyNotFound);
|
||||
}
|
||||
|
||||
public void Select(TKey key, bool logIfKeyNotFound = true)
|
||||
{
|
||||
if (!_items.ContainsKey(key))
|
||||
{
|
||||
if (logIfKeyNotFound)
|
||||
{
|
||||
_logger.Error($"There is no {_title} options for key: {key}");
|
||||
}
|
||||
key = _defaultSelectedKeyIfFailsToFind;
|
||||
}
|
||||
|
||||
_selectedKey = key;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(SelectedValue)));
|
||||
}
|
||||
|
||||
private async Task SelectOptionAsync()
|
||||
{
|
||||
var selection = await _deviceActionService.DisplayActionSheetAsync(_title,
|
||||
AppResources.Cancel,
|
||||
null,
|
||||
_items.Select(o => CreateSelectableOption(o.Value, EqualityComparer<TKey>.Default.Equals(o.Key, _selectedKey)))
|
||||
.ToArray()
|
||||
);
|
||||
|
||||
if (selection == null || selection == AppResources.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sanitizedSelection = selection.Replace($"{SELECTED_CHARACTER} ", string.Empty);
|
||||
var optionKey = _items.First(o => o.Value == sanitizedSelection).Key;
|
||||
|
||||
if (EqualityComparer<TKey>.Default.Equals(optionKey, _selectedKey)
|
||||
||
|
||||
!await _onSelectionChangingAsync(optionKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedKey = optionKey;
|
||||
TriggerPropertyChanged(nameof(SelectedValue));
|
||||
|
||||
if (_afterSelectionChangedAsync != null)
|
||||
{
|
||||
await _afterSelectionChangedAsync(_selectedKey);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAfterSelectionChanged(Func<TKey, Task> afterSelectionChangedAsync) => _afterSelectionChangedAsync = afterSelectionChangedAsync;
|
||||
|
||||
private string CreateSelectableOption(string option, bool selected) => selected ? ToSelectedOption(option) : option;
|
||||
|
||||
private string ToSelectedOption(string option) => $"{SELECTED_CHARACTER} {option}";
|
||||
}
|
||||
}
|
||||
539
src/Core/Pages/Send/SendAddEditPage.xaml
Normal file
539
src/Core/Pages/Send/SendAddEditPage.xaml
Normal file
@@ -0,0 +1,539 @@
|
||||
<?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.SendAddEditPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:core="clr-namespace:Bit.Core"
|
||||
x:DataType="pages:SendAddEditPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SendAddEditPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/>
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:IsNullConverter x:Key="null" />
|
||||
<u:IsNotNullConverter x:Key="notNull" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Key="closeItem" x:Name="_closeItem" />
|
||||
<ToolbarItem IconImageSource="more_vert.png" Clicked="More_Clicked" Order="Primary" x:Name="_moreItem"
|
||||
x:Key="moreItem"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Options}" />
|
||||
<ToolbarItem Text="{u:I18n RemovePassword}"
|
||||
Clicked="RemovePassword_Clicked"
|
||||
Order="Secondary"
|
||||
IsDestructive="True"
|
||||
x:Name="_removePassword"
|
||||
x:Key="removePassword" />
|
||||
<ToolbarItem Text="{u:I18n CopyLink}"
|
||||
Clicked="CopyLink_Clicked"
|
||||
Order="Secondary"
|
||||
IsDestructive="True"
|
||||
x:Name="_copyLink"
|
||||
x:Key="copyLink" />
|
||||
<ToolbarItem Text="{u:I18n ShareLink}"
|
||||
Clicked="ShareLink_Clicked"
|
||||
Order="Secondary"
|
||||
IsDestructive="True"
|
||||
x:Name="_shareLink"
|
||||
x:Key="shareLink" />
|
||||
<ToolbarItem Text="{u:I18n Delete}"
|
||||
Clicked="Delete_Clicked"
|
||||
Order="Secondary"
|
||||
IsDestructive="True"
|
||||
x:Name="_deleteItem"
|
||||
x:Key="deleteItem" />
|
||||
|
||||
<ScrollView x:Key="scrollView" x:Name="_scrollView">
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<Frame
|
||||
IsVisible="{Binding SendEnabled, Converter={StaticResource inverseBool}}"
|
||||
Padding="10"
|
||||
Margin="0, 12, 0, 0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{u:I18n SendDisabledWarning}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="SendDisabledWarningMessageLabel" />
|
||||
</Frame>
|
||||
<Frame
|
||||
IsVisible="{Binding SendOptionsPolicyInEffect}"
|
||||
Padding="10"
|
||||
Margin="0, 12, 0, 0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{u:I18n SendOptionsPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="SendOptionsPolicyInEffectLabel" />
|
||||
</Frame>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n Name}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_nameEntry"
|
||||
Text="{Binding Send.Name}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
AutomationId="SendNameEntry" />
|
||||
<Label
|
||||
Text="{u:I18n NameInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding ShowTypeButtons}">
|
||||
<Label
|
||||
Text="{u:I18n Type}"
|
||||
StyleClass="box-label" />
|
||||
<Grid
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0"
|
||||
Margin="{Binding SegmentedButtonMargins}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Text="{u:I18n TypeFile}"
|
||||
StyleClass="segmented-button"
|
||||
IsEnabled="{Binding IsText}"
|
||||
HeightRequest="{Binding SegmentedButtonHeight}"
|
||||
FontSize="{Binding SegmentedButtonFontSize}"
|
||||
Clicked="FileType_Clicked"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n File}"
|
||||
AutomationProperties.HelpText="{Binding FileTypeAccessibilityLabel}"
|
||||
AutomationId="SendFileButton"
|
||||
Grid.Column="0">
|
||||
</Button>
|
||||
<Button
|
||||
Text="{u:I18n TypeText}"
|
||||
StyleClass="segmented-button"
|
||||
IsEnabled="{Binding IsFile}"
|
||||
HeightRequest="{Binding SegmentedButtonHeight}"
|
||||
FontSize="{Binding SegmentedButtonFontSize}"
|
||||
Clicked="TextType_Clicked"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Text}"
|
||||
AutomationProperties.HelpText="{Binding TextTypeAccessibilityLabel}"
|
||||
AutomationId="SendTextButton"
|
||||
Grid.Column="1">
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding IsFile}">
|
||||
<Label
|
||||
Text="{u:I18n TypeFile}"
|
||||
StyleClass="box-label" />
|
||||
<StackLayout
|
||||
IsVisible="{Binding EditMode}"
|
||||
Orientation="Horizontal">
|
||||
<Label
|
||||
Text="{Binding Send.File.FileName, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
AutomationId="SendFileNameLabel" />
|
||||
<Label
|
||||
Text="{Binding Send.File.SizeName, Mode=OneWay}"
|
||||
StyleClass="box-sub-label"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="SendFileSizeLabel" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-row">
|
||||
<Label
|
||||
IsVisible="{Binding FileName, Converter={StaticResource null}}"
|
||||
Text="{u:I18n NoFileChosen}"
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Start"
|
||||
AutomationId="SendNoFileChosenLabel" />
|
||||
<Label
|
||||
IsVisible="{Binding FileName, Converter={StaticResource notNull}}"
|
||||
Text="{Binding FileName}"
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Start"
|
||||
AutomationId="SendCurrentFileNameLabel" />
|
||||
<Button
|
||||
Text="{u:I18n ChooseFile}"
|
||||
IsVisible="{Binding IsAddFromShare, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-button-row"
|
||||
Clicked="ChooseFile_Clicked"
|
||||
AutomationId="SendChooseFileButton" />
|
||||
<Label
|
||||
Margin="0, 5, 0, 0"
|
||||
Text="{u:I18n MaxFileSize}"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Start" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n TypeFileInfo}"
|
||||
IsVisible="{Binding ShowTypeButtons}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding IsText}">
|
||||
<Label
|
||||
Text="{u:I18n TypeText}"
|
||||
StyleClass="box-label" />
|
||||
<Editor
|
||||
x:Name="_textEditor"
|
||||
AutoSize="TextChanges"
|
||||
Text="{Binding Send.Text.Text}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Margin="{Binding EditorMargins}"
|
||||
AutomationId="SendTextContentEntry"
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
|
||||
<Editor.Behaviors>
|
||||
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
|
||||
</Editor.Behaviors>
|
||||
<Editor.Effects>
|
||||
<effects:ScrollEnabledEffect />
|
||||
</Editor.Effects>
|
||||
</Editor>
|
||||
<BoxView
|
||||
StyleClass="box-row-separator"
|
||||
IsVisible="{Binding ShowEditorSeparators}" />
|
||||
<Label
|
||||
Text="{u:I18n TypeTextInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,10" />
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
Margin="0,10,0,0">
|
||||
<Label
|
||||
Text="{u:I18n HideTextByDefault}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Send.Text.Hidden}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendHideTextByDefaultToggle" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<!--TODO: [MAUI-Migration] xct:TouchEffect.Command="{Binding ToggleOptionsCommand}" for below-->
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Spacing="0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{Binding OptionsAccessilibityText}">
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding ToggleOptionsCommand}" />
|
||||
</StackLayout.GestureRecognizers>
|
||||
<Button
|
||||
Text="{u:I18n Options}"
|
||||
x:Name="_btnOptions"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
Margin="0"
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="SendShowHideOptionsButton" />
|
||||
<controls:IconButton
|
||||
x:Name="_btnOptionsUp"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ChevronUp}}"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
IsVisible="{Binding ShowOptions}"
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="SendOptionsDisplayed" />
|
||||
<controls:IconButton
|
||||
x:Name="_btnOptionsDown"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.AngleDown}}"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
IsVisible="{Binding ShowOptions, Converter={StaticResource inverseBool}}"
|
||||
AutomationProperties.IsInAccessibleTree="False"
|
||||
AutomationId="SendOptionsHidden" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding ShowOptions}">
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,10,0,0">
|
||||
<Label
|
||||
Text="{u:I18n DeletionDate}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_deletionDateTypePicker"
|
||||
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding DeletionTypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding DeletionDateTypeSelectedIndex}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n DeletionTime}"
|
||||
AutomationId="SendDeletionOptionsPicker" />
|
||||
<Grid
|
||||
IsVisible="{Binding ShowDeletionCustomPickers}"
|
||||
Margin="0,5,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<controls:ExtendedDatePicker
|
||||
NullableDate="{Binding DeletionDateTimeViewModel.Date, Mode=TwoWay}"
|
||||
Format="d"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n DeletionDate}"
|
||||
Grid.Column="0"
|
||||
AutomationId="SendCustomDeletionDatePicker" />
|
||||
<controls:ExtendedTimePicker
|
||||
NullableTime="{Binding DeletionDateTimeViewModel.Time, Mode=TwoWay}"
|
||||
Format="t"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n DeletionTime}"
|
||||
Grid.Column="1"
|
||||
AutomationId="SendCustomDeletionTimePicker" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n DeletionDateInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row" Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n ExpirationDate}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_expirationDateTypePicker"
|
||||
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding ExpirationTypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding ExpirationDateTypeSelectedIndex}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ExpirationTime}"
|
||||
AutomationId="SendExpirationOptionsPicker" />
|
||||
<Grid
|
||||
IsVisible="{Binding ShowExpirationCustomPickers}"
|
||||
Margin="0,5,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<controls:ExtendedDatePicker
|
||||
NullableDate="{Binding ExpirationDateTimeViewModel.Date, Mode=TwoWay}"
|
||||
PlaceHolder="mm/dd/yyyy"
|
||||
Format="d"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ExpirationDate}"
|
||||
Grid.Column="0"
|
||||
AutomationId="SendCustomExpirationDatePicker" />
|
||||
<controls:ExtendedTimePicker
|
||||
NullableTime="{Binding ExpirationDateTimeViewModel.Time, Mode=TwoWay}"
|
||||
PlaceHolder="--:-- --"
|
||||
Format="t"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ExpirationTime}"
|
||||
Grid.Column="1"
|
||||
AutomationId="SendCustomExpirationTimePicker" />
|
||||
</Grid>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n ExpirationDateInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Button
|
||||
Text="{u:I18n Clear}"
|
||||
IsVisible="{Binding EditMode}"
|
||||
WidthRequest="110"
|
||||
HeightRequest="{Binding SegmentedButtonHeight}"
|
||||
FontSize="{Binding SegmentedButtonFontSize}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-row-button"
|
||||
Clicked="ClearExpirationDate_Clicked"
|
||||
AutomationId="SendClearExpirationDateButton" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n MaximumAccessCount}"
|
||||
StyleClass="box-label" />
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Orientation="Horizontal">
|
||||
<Entry
|
||||
Text="{Binding MaxAccessCount}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Keyboard="Numeric"
|
||||
MaxLength="9"
|
||||
TextChanged="OnMaxAccessCountTextChanged"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="SendMaxAccessCountEntry" />
|
||||
<controls:ExtendedStepper
|
||||
x:Name="_maxAccessCountStepper"
|
||||
Value="{Binding MaxAccessCount}"
|
||||
Maximum="999999999"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendMaxAccessCountStepper" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MaximumAccessCountInfo}"
|
||||
StyleClass="box-footer-label" />
|
||||
<StackLayout
|
||||
IsVisible="{Binding EditMode}"
|
||||
StyleClass="box-row"
|
||||
Orientation="Horizontal">
|
||||
<Label
|
||||
Text="{u:I18n CurrentAccessCount}"
|
||||
StyleClass="box-footer-label"
|
||||
VerticalTextAlignment="Center" />
|
||||
<Label
|
||||
Text=": "
|
||||
StyleClass="box-footer-label"
|
||||
VerticalTextAlignment="Center" />
|
||||
<Label
|
||||
Text="{Binding Send.AccessCount, Mode=OneWay}"
|
||||
StyleClass="box-label"
|
||||
VerticalTextAlignment="Center"
|
||||
AutomationId="SendCurrentAccessCountLabel" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n NewPassword}"
|
||||
StyleClass="box-label" />
|
||||
<StackLayout Orientation="Horizontal">
|
||||
<Entry
|
||||
Text="{Binding NewPassword}"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="SendNewPasswordEntry" />
|
||||
<controls:IconButton
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Margin="10,0,0,0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
AutomationId="SendShowHidePasswordButton" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n PasswordInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n Notes}"
|
||||
StyleClass="box-label" />
|
||||
<Editor
|
||||
AutoSize="TextChanges"
|
||||
Text="{Binding Send.Notes}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Margin="{Binding EditorMargins}"
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false"
|
||||
AutomationId="SendNotesEntry">
|
||||
<Editor.Behaviors>
|
||||
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
|
||||
</Editor.Behaviors>
|
||||
<Editor.Effects>
|
||||
<effects:ScrollEnabledEffect />
|
||||
</Editor.Effects>
|
||||
</Editor>
|
||||
<BoxView
|
||||
StyleClass="box-row-separator"
|
||||
IsVisible="{Binding ShowEditorSeparators}" />
|
||||
<Label
|
||||
Text="{u:I18n NotesInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n HideEmail}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Send.HideEmail}"
|
||||
IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendHideEmailSwitch" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n DisableSend}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Send.Disabled}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0"
|
||||
AutomationId="SendDeactivateSwitch" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
351
src/Core/Pages/Send/SendAddEditPage.xaml.cs
Normal file
351
src/Core/Pages/Send/SendAddEditPage.xaml.cs
Normal file
@@ -0,0 +1,351 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls.PlatformConfiguration;
|
||||
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SendAddEditPage : BaseContentPage
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
private AppOptions _appOptions;
|
||||
private SendAddEditPageViewModel _vm;
|
||||
|
||||
public Action AfterSubmit { get; set; }
|
||||
|
||||
public SendAddEditPage(
|
||||
AppOptions appOptions = null,
|
||||
string sendId = null,
|
||||
SendType? type = null)
|
||||
{
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SendAddEditPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.SendId = sendId;
|
||||
_vm.Type = appOptions?.CreateSend?.Item1 ?? type;
|
||||
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 (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
if (_vm.EditMode)
|
||||
{
|
||||
ToolbarItems.Add(_removePassword);
|
||||
ToolbarItems.Add(_copyLink);
|
||||
ToolbarItems.Add(_shareLink);
|
||||
ToolbarItems.Add(_deleteItem);
|
||||
}
|
||||
_vm.SegmentedButtonHeight = 36;
|
||||
_vm.SegmentedButtonFontSize = 13;
|
||||
_vm.SegmentedButtonMargins = new Thickness(0, 10, 0, 0);
|
||||
_vm.EditorMargins = new Thickness(0, 5, 0, 0);
|
||||
_btnOptions.WidthRequest = 70;
|
||||
_btnOptionsDown.WidthRequest = 30;
|
||||
_btnOptionsUp.WidthRequest = 30;
|
||||
}
|
||||
else if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ToolbarItems.Add(_closeItem);
|
||||
if (_vm.EditMode)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
_vm.SegmentedButtonHeight = 30;
|
||||
_vm.SegmentedButtonFontSize = 15;
|
||||
_vm.SegmentedButtonMargins = new Thickness(0, 5, 0, 0);
|
||||
_vm.ShowEditorSeparators = true;
|
||||
_vm.EditorMargins = new Thickness(0, 10, 0, 5);
|
||||
_deletionDateTypePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_expirationDateTypePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
|
||||
_deletionDateTypePicker.ItemDisplayBinding = new Binding("Key");
|
||||
_expirationDateTypePicker.ItemDisplayBinding = new Binding("Key");
|
||||
|
||||
if (_vm.IsText)
|
||||
{
|
||||
_nameEntry.ReturnType = ReturnType.Next;
|
||||
_nameEntry.ReturnCommand = new Command(() => _textEditor.Focus());
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
if (!await AppHelpers.IsVaultTimeoutImmediateAsync())
|
||||
{
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
}
|
||||
if (await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _vm.InitAsync();
|
||||
_broadcasterService.Subscribe(nameof(SendAddEditPage), message =>
|
||||
{
|
||||
if (message.Command == "selectFileResult")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
var data = message.Data as Tuple<byte[], string>;
|
||||
_vm.FileData = data.Item1;
|
||||
_vm.FileName = data.Item2;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await LoadOnAppearedAsync(_scrollView, true, async () =>
|
||||
{
|
||||
var success = await _vm.LoadAsync();
|
||||
if (!success)
|
||||
{
|
||||
await CloseAsync();
|
||||
return;
|
||||
}
|
||||
await HandleCreateRequest();
|
||||
if (!_vm.EditMode && string.IsNullOrWhiteSpace(_vm.Send?.Name))
|
||||
{
|
||||
RequestFocus(_nameEntry);
|
||||
}
|
||||
AdjustToolbar();
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
await CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CloseAsync()
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
|
||||
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 (_vm.IsAddFromShare && Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
_appOptions.CreateSend = null;
|
||||
}
|
||||
return base.OnBackButtonPressed();
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
// 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)
|
||||
{
|
||||
_broadcasterService.Unsubscribe(nameof(SendAddEditPage));
|
||||
}
|
||||
}
|
||||
|
||||
private async void TextType_Clicked(object sender, EventArgs eventArgs)
|
||||
{
|
||||
await _vm.TypeChangedAsync(SendType.Text);
|
||||
_nameEntry.ReturnType = ReturnType.Next;
|
||||
_nameEntry.ReturnCommand = new Command(() => _textEditor.Focus());
|
||||
if (string.IsNullOrWhiteSpace(_vm.Send.Name))
|
||||
{
|
||||
RequestFocus(_nameEntry);
|
||||
}
|
||||
}
|
||||
|
||||
private async void FileType_Clicked(object sender, EventArgs eventArgs)
|
||||
{
|
||||
await _vm.TypeChangedAsync(SendType.File);
|
||||
_nameEntry.ReturnType = ReturnType.Done;
|
||||
_nameEntry.ReturnCommand = null;
|
||||
if (string.IsNullOrWhiteSpace(_vm.Send.Name))
|
||||
{
|
||||
RequestFocus(_nameEntry);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMaxAccessCountTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(e.NewTextValue))
|
||||
{
|
||||
_vm.MaxAccessCount = null;
|
||||
_maxAccessCountStepper.Value = 0;
|
||||
return;
|
||||
}
|
||||
// accept only digits
|
||||
if (!int.TryParse(e.NewTextValue, out int _))
|
||||
{
|
||||
((Microsoft.Maui.Controls.Entry)sender).Text = e.OldTextValue;
|
||||
}
|
||||
}
|
||||
|
||||
private async void ChooseFile_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.ChooseFileAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearExpirationDate_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.ClearExpirationDate();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Save_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var submitted = await _vm.SubmitAsync();
|
||||
if (submitted)
|
||||
{
|
||||
AfterSubmit?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void RemovePassword_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.RemovePasswordAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void CopyLink_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.CopyLinkAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void ShareLink_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.ShareLinkAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Delete_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
if (await _vm.DeleteAsync())
|
||||
{
|
||||
await CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var options = new List<string>();
|
||||
if (_vm.SendEnabled && _vm.EditMode)
|
||||
{
|
||||
if (_vm.Send.HasPassword)
|
||||
{
|
||||
options.Add(AppResources.RemovePassword);
|
||||
}
|
||||
options.Add(AppResources.CopyLink);
|
||||
options.Add(AppResources.ShareLink);
|
||||
}
|
||||
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
_vm.EditMode ? AppResources.Delete : null, options.ToArray());
|
||||
if (selection == AppResources.RemovePassword)
|
||||
{
|
||||
await _vm.RemovePasswordAsync();
|
||||
}
|
||||
else if (selection == AppResources.CopyLink)
|
||||
{
|
||||
await _vm.CopyLinkAsync();
|
||||
}
|
||||
else if (selection == AppResources.ShareLink)
|
||||
{
|
||||
await _vm.ShareLinkAsync();
|
||||
}
|
||||
else if (selection == AppResources.Delete)
|
||||
{
|
||||
if (await _vm.DeleteAsync())
|
||||
{
|
||||
await CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void AdjustToolbar()
|
||||
{
|
||||
_saveItem.IsEnabled = _vm.SendEnabled;
|
||||
// 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.SendEnabled && _vm.EditMode && Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ToolbarItems.Remove(_removePassword);
|
||||
ToolbarItems.Remove(_copyLink);
|
||||
ToolbarItems.Remove(_shareLink);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCreateRequest()
|
||||
{
|
||||
if (_appOptions?.CreateSend == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_vm.IsAddFromShare = true;
|
||||
_vm.CopyInsteadOfShareAfterSaving = _appOptions.CopyInsteadOfShareAfterSaving;
|
||||
|
||||
var name = _appOptions.CreateSend.Item2;
|
||||
_vm.Send.Name = name;
|
||||
|
||||
var type = _appOptions.CreateSend.Item1;
|
||||
if (type == SendType.File)
|
||||
{
|
||||
_vm.FileData = _appOptions.CreateSend.Item3;
|
||||
_vm.FileName = name;
|
||||
FileType_Clicked(null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var text = _appOptions.CreateSend.Item4;
|
||||
_vm.Send.Text.Text = text;
|
||||
TextType_Clicked(null, null);
|
||||
}
|
||||
_appOptions.CreateSend = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
636
src/Core/Pages/Send/SendAddEditPageViewModel.cs
Normal file
636
src/Core/Pages/Send/SendAddEditPageViewModel.cs
Normal file
@@ -0,0 +1,636 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Networking;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SendAddEditPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ISendService _sendService;
|
||||
private readonly ILogger _logger;
|
||||
private bool _sendEnabled = true;
|
||||
private bool _canAccessPremium;
|
||||
private bool _emailVerified;
|
||||
private SendView _send;
|
||||
private string _fileName;
|
||||
private bool _showOptions;
|
||||
private bool _showPassword;
|
||||
private int _deletionDateTypeSelectedIndex;
|
||||
private int _expirationDateTypeSelectedIndex;
|
||||
private DateTime _simpleDeletionDateTime;
|
||||
private DateTime? _simpleExpirationDateTime;
|
||||
private bool _isOverridingPickers;
|
||||
private int? _maxAccessCount;
|
||||
private string[] _additionalSendProperties = new[]
|
||||
{
|
||||
nameof(IsText),
|
||||
nameof(IsFile),
|
||||
nameof(FileTypeAccessibilityLabel),
|
||||
nameof(TextTypeAccessibilityLabel)
|
||||
};
|
||||
private bool _disableHideEmail;
|
||||
private bool _sendOptionsPolicyInEffect;
|
||||
private bool _copyInsteadOfShareAfterSaving;
|
||||
|
||||
public SendAddEditPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_sendService = ServiceContainer.Resolve<ISendService>("sendService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleOptionsCommand = new Command(ToggleOptions);
|
||||
|
||||
TypeOptions = new List<KeyValuePair<string, SendType>>
|
||||
{
|
||||
new KeyValuePair<string, SendType>(AppResources.TypeText, SendType.Text),
|
||||
new KeyValuePair<string, SendType>(AppResources.TypeFile, SendType.File),
|
||||
};
|
||||
DeletionTypeOptions = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(AppResources.OneHour, AppResources.OneHour),
|
||||
new KeyValuePair<string, string>(AppResources.OneDay, AppResources.OneDay),
|
||||
new KeyValuePair<string, string>(AppResources.TwoDays, AppResources.TwoDays),
|
||||
new KeyValuePair<string, string>(AppResources.ThreeDays, AppResources.ThreeDays),
|
||||
new KeyValuePair<string, string>(AppResources.SevenDays, AppResources.SevenDays),
|
||||
new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays),
|
||||
new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom),
|
||||
};
|
||||
ExpirationTypeOptions = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(AppResources.Never, AppResources.Never),
|
||||
new KeyValuePair<string, string>(AppResources.OneHour, AppResources.OneHour),
|
||||
new KeyValuePair<string, string>(AppResources.OneDay, AppResources.OneDay),
|
||||
new KeyValuePair<string, string>(AppResources.TwoDays, AppResources.TwoDays),
|
||||
new KeyValuePair<string, string>(AppResources.ThreeDays, AppResources.ThreeDays),
|
||||
new KeyValuePair<string, string>(AppResources.SevenDays, AppResources.SevenDays),
|
||||
new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays),
|
||||
new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom),
|
||||
};
|
||||
|
||||
DeletionDateTimeViewModel = new DateTimeViewModel(AppResources.DeletionDate, AppResources.DeletionTime);
|
||||
ExpirationDateTimeViewModel = new DateTimeViewModel(AppResources.ExpirationDate, AppResources.ExpirationTime)
|
||||
{
|
||||
OnDateChanged = date =>
|
||||
{
|
||||
if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Time.HasValue)
|
||||
{
|
||||
// auto-set time to current time upon setting date
|
||||
ExpirationDateTimeViewModel.Time = DateTimeNow().TimeOfDay;
|
||||
}
|
||||
},
|
||||
OnTimeChanged = time =>
|
||||
{
|
||||
if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Date.HasValue)
|
||||
{
|
||||
// auto-set date to current date upon setting time
|
||||
ExpirationDateTimeViewModel.Date = DateTime.Today;
|
||||
}
|
||||
},
|
||||
DatePlaceholder = "mm/dd/yyyy",
|
||||
TimePlaceholder = "--:-- --"
|
||||
};
|
||||
|
||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger);
|
||||
}
|
||||
|
||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||
public Command TogglePasswordCommand { get; set; }
|
||||
public Command ToggleOptionsCommand { get; set; }
|
||||
public string SendId { get; set; }
|
||||
public int SegmentedButtonHeight { get; set; }
|
||||
public int SegmentedButtonFontSize { get; set; }
|
||||
public Thickness SegmentedButtonMargins { get; set; }
|
||||
public bool ShowEditorSeparators { get; set; }
|
||||
public Thickness EditorMargins { get; set; }
|
||||
public SendType? Type { get; set; }
|
||||
public byte[] FileData { get; set; }
|
||||
public string NewPassword { get; set; }
|
||||
public bool DisableHideEmailControl { get; set; }
|
||||
public bool IsAddFromShare { get; set; }
|
||||
public bool CopyInsteadOfShareAfterSaving { get; set; }
|
||||
public string OptionsAccessilibityText => ShowOptions ? AppResources.OptionsExpanded : AppResources.OptionsCollapsed;
|
||||
public List<KeyValuePair<string, SendType>> TypeOptions { get; }
|
||||
public List<KeyValuePair<string, string>> DeletionTypeOptions { get; }
|
||||
public List<KeyValuePair<string, string>> ExpirationTypeOptions { get; }
|
||||
public bool SendEnabled
|
||||
{
|
||||
get => _sendEnabled;
|
||||
set => SetProperty(ref _sendEnabled, value);
|
||||
}
|
||||
public int DeletionDateTypeSelectedIndex
|
||||
{
|
||||
get => _deletionDateTypeSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _deletionDateTypeSelectedIndex, value))
|
||||
{
|
||||
DeletionTypeChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool ShowOptions
|
||||
{
|
||||
get => _showOptions;
|
||||
set => SetProperty(ref _showOptions, value,
|
||||
additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(OptionsAccessilibityText),
|
||||
nameof(OptionsShowHideIcon)
|
||||
});
|
||||
}
|
||||
public int ExpirationDateTypeSelectedIndex
|
||||
{
|
||||
get => _expirationDateTypeSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _expirationDateTypeSelectedIndex, value))
|
||||
{
|
||||
ExpirationTypeChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int? MaxAccessCount
|
||||
{
|
||||
get => _maxAccessCount;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _maxAccessCount, value))
|
||||
{
|
||||
MaxAccessCountChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
public SendView Send
|
||||
{
|
||||
get => _send;
|
||||
set => SetProperty(ref _send, value, additionalPropertyNames: _additionalSendProperties);
|
||||
}
|
||||
public string FileName
|
||||
{
|
||||
get => _fileName ?? AppResources.NoFileChosen;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _fileName, value))
|
||||
{
|
||||
Send.File.FileName = _fileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(ShowPasswordIcon),
|
||||
nameof(PasswordVisibilityAccessibilityText)
|
||||
});
|
||||
}
|
||||
public bool DisableHideEmail
|
||||
{
|
||||
get => _disableHideEmail;
|
||||
set => SetProperty(ref _disableHideEmail, value);
|
||||
}
|
||||
public bool SendOptionsPolicyInEffect
|
||||
{
|
||||
get => _sendOptionsPolicyInEffect;
|
||||
set => SetProperty(ref _sendOptionsPolicyInEffect, value);
|
||||
}
|
||||
public bool ShowTypeButtons => !EditMode && !IsAddFromShare;
|
||||
public bool EditMode => !string.IsNullOrWhiteSpace(SendId);
|
||||
public bool IsText => Send?.Type == SendType.Text;
|
||||
public bool IsFile => Send?.Type == SendType.File;
|
||||
public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6;
|
||||
public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7;
|
||||
public DateTimeViewModel DeletionDateTimeViewModel { get; }
|
||||
public DateTimeViewModel ExpirationDateTimeViewModel { get; }
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string FileTypeAccessibilityLabel => IsFile ? AppResources.FileTypeIsSelected : AppResources.FileTypeIsNotSelected;
|
||||
public string TextTypeAccessibilityLabel => IsText ? AppResources.TextTypeIsSelected : AppResources.TextTypeIsNotSelected;
|
||||
public string OptionsShowHideIcon => ShowOptions ? BitwardenIcons.ChevronUp : BitwardenIcons.AngleDown;
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
PageTitle = EditMode ? AppResources.EditSend : AppResources.AddSend;
|
||||
_canAccessPremium = await _stateService.CanAccessPremiumAsync();
|
||||
_emailVerified = await _stateService.GetEmailVerifiedAsync();
|
||||
SendEnabled = !await AppHelpers.IsSendDisabledByPolicyAsync();
|
||||
DisableHideEmail = await AppHelpers.IsHideEmailDisabledByPolicyAsync();
|
||||
SendOptionsPolicyInEffect = SendEnabled && DisableHideEmail;
|
||||
}
|
||||
|
||||
public async Task<bool> LoadAsync()
|
||||
{
|
||||
if (Send == null)
|
||||
{
|
||||
_isOverridingPickers = true;
|
||||
if (EditMode)
|
||||
{
|
||||
var send = await _sendService.GetAsync(SendId);
|
||||
if (send == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Send = await send.DecryptAsync();
|
||||
DeletionDateTimeViewModel.DateTime = Send.DeletionDate.ToLocalTime();
|
||||
ExpirationDateTimeViewModel.DateTime = Send.ExpirationDate?.ToLocalTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
var defaultType = _canAccessPremium && _emailVerified ? SendType.File : SendType.Text;
|
||||
Send = new SendView
|
||||
{
|
||||
Type = Type.GetValueOrDefault(defaultType),
|
||||
};
|
||||
DeletionDateTimeViewModel.DateTime = DateTimeNow().AddDays(7);
|
||||
DeletionDateTypeSelectedIndex = 4;
|
||||
ExpirationDateTypeSelectedIndex = 0;
|
||||
}
|
||||
|
||||
MaxAccessCount = Send.MaxAccessCount;
|
||||
_isOverridingPickers = false;
|
||||
}
|
||||
|
||||
DisableHideEmailControl = !SendEnabled ||
|
||||
(!EditMode && DisableHideEmail) ||
|
||||
(EditMode && DisableHideEmail && !Send.HideEmail);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task ChooseFileAsync()
|
||||
{
|
||||
await _fileService.SelectFileAsync();
|
||||
}
|
||||
|
||||
public void ClearExpirationDate()
|
||||
{
|
||||
_isOverridingPickers = true;
|
||||
ExpirationDateTimeViewModel.DateTime = null;
|
||||
_isOverridingPickers = false;
|
||||
}
|
||||
|
||||
private void UpdateSendData()
|
||||
{
|
||||
// filename
|
||||
if (Send.File != null && _fileName != null)
|
||||
{
|
||||
Send.File.FileName = _fileName;
|
||||
}
|
||||
|
||||
// deletion date
|
||||
if (ShowDeletionCustomPickers)
|
||||
{
|
||||
Send.DeletionDate = DeletionDateTimeViewModel.DateTime.Value.ToUniversalTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
Send.DeletionDate = _simpleDeletionDateTime.ToUniversalTime();
|
||||
}
|
||||
|
||||
// expiration date
|
||||
if (ShowExpirationCustomPickers && ExpirationDateTimeViewModel.DateTime.HasValue)
|
||||
{
|
||||
Send.ExpirationDate = ExpirationDateTimeViewModel.DateTime.Value.ToUniversalTime();
|
||||
}
|
||||
else if (_simpleExpirationDateTime.HasValue)
|
||||
{
|
||||
Send.ExpirationDate = _simpleExpirationDateTime.Value.ToUniversalTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
Send.ExpirationDate = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SubmitAsync()
|
||||
{
|
||||
if (Send == null || !SendEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Send.Name))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.Name),
|
||||
AppResources.Ok);
|
||||
return false;
|
||||
}
|
||||
if (IsFile)
|
||||
{
|
||||
if (!_canAccessPremium)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.SendFilePremiumRequired);
|
||||
return false;
|
||||
}
|
||||
if (!_emailVerified)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.SendFileEmailVerificationRequired);
|
||||
return false;
|
||||
}
|
||||
if (!EditMode)
|
||||
{
|
||||
if (FileData == null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.File),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
return false;
|
||||
}
|
||||
if (FileData.Length > 104857600) // 100 MB
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.MaxFileSize,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpdateSendData();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(NewPassword))
|
||||
{
|
||||
NewPassword = null;
|
||||
}
|
||||
|
||||
var (send, encryptedFileData) = await _sendService.EncryptAsync(Send, FileData, NewPassword);
|
||||
if (send == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||
var sendId = await _sendService.SaveWithServerAsync(send, encryptedFileData);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
// 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 && IsFile)
|
||||
{
|
||||
// Workaround for https://github.com/xamarin/Xamarin.Forms/issues/5418
|
||||
// Exiting and returning (file picker) calls OnAppearing on list page instead of this modal, and
|
||||
// it doesn't get called again when the model is dismissed, so the list isn't updated.
|
||||
_messagingService.Send("sendUpdated");
|
||||
}
|
||||
|
||||
if (!CopyInsteadOfShareAfterSaving)
|
||||
{
|
||||
await CloseAsync();
|
||||
}
|
||||
|
||||
var savedSend = await _sendService.GetAsync(sendId);
|
||||
if (savedSend != null)
|
||||
{
|
||||
var savedSendView = await savedSend.DecryptAsync();
|
||||
if (CopyInsteadOfShareAfterSaving)
|
||||
{
|
||||
await AppHelpers.CopySendUrlAsync(savedSendView);
|
||||
|
||||
// wait so that the user sees the message before the view gets dismissed
|
||||
await Task.Delay(1300);
|
||||
}
|
||||
else
|
||||
{
|
||||
await AppHelpers.ShareSendUrlAsync(savedSendView);
|
||||
}
|
||||
}
|
||||
|
||||
if (CopyInsteadOfShareAfterSaving)
|
||||
{
|
||||
await CloseAsync();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_logger.Exception(ex);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task CloseAsync()
|
||||
{
|
||||
if (IsAddFromShare)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
_deviceActionService.CloseMainApp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Page is SendAddOnlyPage sendPage && sendPage.OnClose != null)
|
||||
{
|
||||
sendPage.OnClose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await Page.Navigation.PopModalAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> RemovePasswordAsync()
|
||||
{
|
||||
return await AppHelpers.RemoveSendPasswordAsync(SendId);
|
||||
}
|
||||
|
||||
public async Task CopyLinkAsync()
|
||||
{
|
||||
await AppHelpers.CopySendUrlAsync(Send);
|
||||
}
|
||||
|
||||
public async Task ShareLinkAsync()
|
||||
{
|
||||
await AppHelpers.ShareSendUrlAsync(Send);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync()
|
||||
{
|
||||
return await AppHelpers.DeleteSendAsync(SendId);
|
||||
}
|
||||
|
||||
public async Task TypeChangedAsync(SendType type)
|
||||
{
|
||||
if (!SendEnabled)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.SendDisabledWarning);
|
||||
await CloseAsync();
|
||||
return;
|
||||
}
|
||||
if (Send != null)
|
||||
{
|
||||
if (!EditMode && type == SendType.File && (!_canAccessPremium || !_emailVerified))
|
||||
{
|
||||
if (!_canAccessPremium)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.SendFilePremiumRequired);
|
||||
}
|
||||
else if (!_emailVerified)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.SendFileEmailVerificationRequired);
|
||||
}
|
||||
|
||||
// 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 (IsAddFromShare && Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
_deviceActionService.CloseMainApp();
|
||||
return;
|
||||
}
|
||||
type = SendType.Text;
|
||||
}
|
||||
Send.Type = type;
|
||||
TriggerPropertyChanged(nameof(Send), _additionalSendProperties);
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleOptions()
|
||||
{
|
||||
ShowOptions = !ShowOptions;
|
||||
}
|
||||
|
||||
private void DeletionTypeChanged()
|
||||
{
|
||||
if (Send != null && DeletionDateTypeSelectedIndex > -1)
|
||||
{
|
||||
_isOverridingPickers = true;
|
||||
switch (DeletionDateTypeSelectedIndex)
|
||||
{
|
||||
case 0:
|
||||
_simpleDeletionDateTime = DateTimeNow().AddHours(1);
|
||||
break;
|
||||
case 1:
|
||||
_simpleDeletionDateTime = DateTimeNow().AddDays(1);
|
||||
break;
|
||||
case 2:
|
||||
_simpleDeletionDateTime = DateTimeNow().AddDays(2);
|
||||
break;
|
||||
case 3:
|
||||
_simpleDeletionDateTime = DateTimeNow().AddDays(3);
|
||||
break;
|
||||
case 4:
|
||||
_simpleDeletionDateTime = DateTimeNow().AddDays(7);
|
||||
break;
|
||||
case 5:
|
||||
_simpleDeletionDateTime = DateTimeNow().AddDays(30);
|
||||
break;
|
||||
case 6:
|
||||
// custom option, initial values already set elsewhere
|
||||
break;
|
||||
}
|
||||
_isOverridingPickers = false;
|
||||
TriggerPropertyChanged(nameof(ShowDeletionCustomPickers));
|
||||
}
|
||||
}
|
||||
|
||||
private void ExpirationTypeChanged()
|
||||
{
|
||||
if (Send != null && ExpirationDateTypeSelectedIndex > -1)
|
||||
{
|
||||
_isOverridingPickers = true;
|
||||
switch (ExpirationDateTypeSelectedIndex)
|
||||
{
|
||||
case 0:
|
||||
_simpleExpirationDateTime = null;
|
||||
break;
|
||||
case 1:
|
||||
_simpleExpirationDateTime = DateTimeNow().AddHours(1);
|
||||
break;
|
||||
case 2:
|
||||
_simpleExpirationDateTime = DateTimeNow().AddDays(1);
|
||||
break;
|
||||
case 3:
|
||||
_simpleExpirationDateTime = DateTimeNow().AddDays(2);
|
||||
break;
|
||||
case 4:
|
||||
_simpleExpirationDateTime = DateTimeNow().AddDays(3);
|
||||
break;
|
||||
case 5:
|
||||
_simpleExpirationDateTime = DateTimeNow().AddDays(7);
|
||||
break;
|
||||
case 6:
|
||||
_simpleExpirationDateTime = DateTimeNow().AddDays(30);
|
||||
break;
|
||||
case 7:
|
||||
// custom option, clear all expiration values
|
||||
_simpleExpirationDateTime = null;
|
||||
ClearExpirationDate();
|
||||
break;
|
||||
}
|
||||
_isOverridingPickers = false;
|
||||
TriggerPropertyChanged(nameof(ShowExpirationCustomPickers));
|
||||
}
|
||||
}
|
||||
|
||||
private void MaxAccessCountChanged()
|
||||
{
|
||||
Send.MaxAccessCount = _maxAccessCount;
|
||||
}
|
||||
|
||||
private void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
}
|
||||
|
||||
private DateTime DateTimeNow()
|
||||
{
|
||||
var dtn = DateTime.Now;
|
||||
return new DateTime(
|
||||
dtn.Year,
|
||||
dtn.Month,
|
||||
dtn.Day,
|
||||
dtn.Hour,
|
||||
dtn.Minute,
|
||||
0,
|
||||
DateTimeKind.Local
|
||||
);
|
||||
}
|
||||
|
||||
internal void TriggerSendTextPropertyChanged()
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(Send)));
|
||||
}
|
||||
}
|
||||
}
|
||||
183
src/Core/Pages/Send/SendAddOnlyOptionsView.xaml
Normal file
183
src/Core/Pages/Send/SendAddOnlyOptionsView.xaml
Normal file
@@ -0,0 +1,183 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<ContentView
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"
|
||||
x:DataType="pages:SendAddEditPageViewModel"
|
||||
x:Class="Bit.App.Pages.SendAddOnlyOptionsView">
|
||||
<ContentView.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentView.Resources>
|
||||
<ContentView.Content>
|
||||
<StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,10,0,0">
|
||||
<Label
|
||||
Text="{u:I18n DeletionDate}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_deletionDateTypePicker"
|
||||
ItemsSource="{Binding DeletionTypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding DeletionDateTypeSelectedIndex}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
ItemDisplayBinding="{Binding Key}"
|
||||
ios:Picker.UpdateMode="WhenFinished"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n DeletionTime}" />
|
||||
<controls:LazyDateTimePicker
|
||||
x:Name="_lazyDeletionDateTimePicker"
|
||||
BindingContext="{Binding DeletionDateTimeViewModel}"
|
||||
IsVisible="{Binding ShowDeletionCustomPickers}"
|
||||
Margin="0,5,0,0" />
|
||||
<Label
|
||||
Text="{u:I18n DeletionDateInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row" Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n ExpirationDate}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_expirationDateTypePicker"
|
||||
ItemsSource="{Binding ExpirationTypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding ExpirationDateTypeSelectedIndex}"
|
||||
ItemDisplayBinding="{Binding Key}"
|
||||
ios:Picker.UpdateMode="WhenFinished"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ExpirationTime}" />
|
||||
<controls:LazyDateTimePicker
|
||||
x:Name="_lazyExpirationDateTimePicker"
|
||||
BindingContext="{Binding ExpirationDateTimeViewModel}"
|
||||
IsVisible="{Binding ShowExpirationCustomPickers}"
|
||||
Margin="0,5,0,0" />
|
||||
<Label
|
||||
Text="{u:I18n ExpirationDateInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n MaximumAccessCount}"
|
||||
StyleClass="box-label" />
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Orientation="Horizontal">
|
||||
<Entry
|
||||
Text="{Binding MaxAccessCount}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Keyboard="Numeric"
|
||||
MaxLength="9"
|
||||
TextChanged="OnMaxAccessCountTextChanged"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<controls:ExtendedStepper
|
||||
x:Name="_maxAccessCountStepper"
|
||||
Value="{Binding MaxAccessCount}"
|
||||
Maximum="999999999"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MaximumAccessCountInfo}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n NewPassword}"
|
||||
StyleClass="box-label" />
|
||||
<StackLayout Orientation="Horizontal">
|
||||
<Entry
|
||||
Text="{Binding NewPassword}"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
<controls:IconButton
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Margin="10,0,0,0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n PasswordInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n Notes}"
|
||||
StyleClass="box-label" />
|
||||
<Editor
|
||||
x:Name="_notesEditor"
|
||||
AutoSize="TextChanges"
|
||||
Text="{Binding Send.Notes}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Margin="0,10,0,5"
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
|
||||
<Editor.Effects>
|
||||
<effects:ScrollEnabledEffect />
|
||||
</Editor.Effects>
|
||||
</Editor>
|
||||
<BoxView
|
||||
StyleClass="box-row-separator" />
|
||||
<Label
|
||||
Text="{u:I18n NotesInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n HideEmail}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Send.HideEmail}"
|
||||
IsEnabled="{Binding DisableHideEmailControl, Converter={StaticResource inverseBool}}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
Margin="0,5,0,0">
|
||||
<Label
|
||||
Text="{u:I18n DisableSend}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Send.Disabled}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ContentView.Content>
|
||||
</ContentView>
|
||||
97
src/Core/Pages/Send/SendAddOnlyOptionsView.xaml.cs
Normal file
97
src/Core/Pages/Send/SendAddOnlyOptionsView.xaml.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Behaviors;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using CommunityToolkit.Maui.Converters;
|
||||
using CommunityToolkit.Maui.ImageSources;
|
||||
using CommunityToolkit.Maui;
|
||||
using CommunityToolkit.Maui.Core;
|
||||
using CommunityToolkit.Maui.Layouts;
|
||||
using CommunityToolkit.Maui.Views;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SendAddOnlyOptionsView : ContentView
|
||||
{
|
||||
public SendAddOnlyOptionsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private SendAddEditPageViewModel ViewModel => BindingContext as SendAddEditPageViewModel;
|
||||
|
||||
public void SetMainScrollView(ScrollView scrollView)
|
||||
{
|
||||
_notesEditor.Behaviors.Add(new EditorPreventAutoBottomScrollingOnFocusedBehavior { ParentScrollView = scrollView });
|
||||
}
|
||||
|
||||
private void OnMaxAccessCountTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (ViewModel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(e.NewTextValue))
|
||||
{
|
||||
ViewModel.MaxAccessCount = null;
|
||||
_maxAccessCountStepper.Value = 0;
|
||||
return;
|
||||
}
|
||||
// accept only digits
|
||||
if (!int.TryParse(e.NewTextValue, out int _))
|
||||
{
|
||||
((Entry)sender).Text = e.OldTextValue;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
base.OnPropertyChanged(propertyName);
|
||||
|
||||
if (propertyName == nameof(BindingContext)
|
||||
&&
|
||||
ViewModel != null)
|
||||
{
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (!_lazyDeletionDateTimePicker.IsLoaded
|
||||
&&
|
||||
e.PropertyName == nameof(SendAddEditPageViewModel.ShowDeletionCustomPickers)
|
||||
&&
|
||||
ViewModel.ShowDeletionCustomPickers)
|
||||
{
|
||||
_lazyDeletionDateTimePicker.LoadViewAsync();
|
||||
}
|
||||
|
||||
if (!_lazyExpirationDateTimePicker.IsLoaded
|
||||
&&
|
||||
e.PropertyName == nameof(SendAddEditPageViewModel.ShowExpirationCustomPickers)
|
||||
&&
|
||||
ViewModel.ShowExpirationCustomPickers)
|
||||
{
|
||||
_lazyExpirationDateTimePicker.LoadViewAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SendAddOnlyOptionsLazyView : LazyView<SendAddOnlyOptionsView>
|
||||
{
|
||||
public ScrollView MainScrollView { get; set; }
|
||||
|
||||
public override async ValueTask LoadViewAsync()
|
||||
{
|
||||
await base.LoadViewAsync();
|
||||
|
||||
if (Content is SendAddOnlyOptionsView optionsView)
|
||||
{
|
||||
optionsView.SetMainScrollView(MainScrollView);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
178
src/Core/Pages/Send/SendAddOnlyPage.xaml
Normal file
178
src/Core/Pages/Send/SendAddOnlyPage.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"
|
||||
x:Class="Bit.App.Pages.SendAddOnlyPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
x:DataType="pages:SendAddEditPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SendAddEditPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<!--Order matters here or the avatar's image won't be updated correctly, check iOS CustomNavigationRenderer for more info-->
|
||||
<controls:ExtendedToolbarItem
|
||||
x:Name="_accountAvatar"
|
||||
IconImageSource="{Binding AvatarImageSource}"
|
||||
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
|
||||
Order="Primary"
|
||||
Priority="-2"
|
||||
UseOriginalImage="True"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Account}"
|
||||
AutomationId="AccountIconButton" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" x:Name="_closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" x:Name="_saveItem"/>
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<AbsoluteLayout>
|
||||
<ScrollView
|
||||
x:Name="_scrollView"
|
||||
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
|
||||
AbsoluteLayout.LayoutFlags="All">
|
||||
<StackLayout x:Name="_mainContainer" StyleClass="box">
|
||||
<Frame
|
||||
IsVisible="{Binding SendEnabled, Converter={StaticResource inverseBool}}"
|
||||
Padding="10"
|
||||
Margin="0, 12, 0, 0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{u:I18n SendDisabledWarning}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<Frame
|
||||
IsVisible="{Binding SendOptionsPolicyInEffect}"
|
||||
Padding="10"
|
||||
Margin="0, 12, 0, 0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{u:I18n SendOptionsPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n Name}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_nameEntry"
|
||||
Text="{Binding Send.Name}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value" />
|
||||
<Label
|
||||
Text="{u:I18n NameInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,0" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding IsFile}">
|
||||
<Label
|
||||
Text="{u:I18n TypeFile}"
|
||||
StyleClass="box-label" />
|
||||
<StackLayout
|
||||
StyleClass="box-row">
|
||||
<Label
|
||||
Text="{Binding FileName}"
|
||||
LineBreakMode="CharacterWrap"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Start" />
|
||||
<Label
|
||||
Margin="0, 5, 0, 0"
|
||||
Text="{u:I18n MaxFileSize}"
|
||||
StyleClass="text-sm, text-muted"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
HorizontalTextAlignment="Start" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
IsVisible="{Binding IsText}">
|
||||
<Label
|
||||
Text="{u:I18n TypeText}"
|
||||
StyleClass="box-label" />
|
||||
<Editor
|
||||
x:Name="_textEditor"
|
||||
AutoSize="TextChanges"
|
||||
Text="{Binding Send.Text.Text}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
StyleClass="box-value"
|
||||
Margin="{Binding EditorMargins}"
|
||||
effects:ScrollEnabledEffect.IsScrollEnabled="false" >
|
||||
<Editor.Behaviors>
|
||||
<behaviors:EditorPreventAutoBottomScrollingOnFocusedBehavior ParentScrollView="{x:Reference _scrollView}" />
|
||||
</Editor.Behaviors>
|
||||
<Editor.Effects>
|
||||
<effects:ScrollEnabledEffect />
|
||||
</Editor.Effects>
|
||||
</Editor>
|
||||
<BoxView
|
||||
StyleClass="box-row-separator" />
|
||||
<Label
|
||||
Text="{u:I18n TypeTextInfo}"
|
||||
StyleClass="box-footer-label"
|
||||
Margin="0,5,0,10" />
|
||||
<StackLayout
|
||||
StyleClass="box-row, box-row-switch"
|
||||
Margin="0,10,0,0">
|
||||
<Label
|
||||
Text="{u:I18n HideTextByDefault}"
|
||||
StyleClass="box-label-regular"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Send.Text.Hidden}"
|
||||
IsEnabled="{Binding SendEnabled}"
|
||||
HorizontalOptions="End"
|
||||
Margin="10,0,0,0" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Spacing="0"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{Binding OptionsAccessilibityText}">
|
||||
<StackLayout.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="OptionsHeader_Tapped" />
|
||||
</StackLayout.GestureRecognizers>
|
||||
<Label
|
||||
Text="{u:I18n Options}"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
Margin="0,0,5,0"
|
||||
AutomationProperties.IsInAccessibleTree="False"/>
|
||||
<controls:IconLabel
|
||||
Text="{Binding OptionsShowHideIcon}"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
AutomationProperties.IsInAccessibleTree="False"/>
|
||||
</StackLayout>
|
||||
<pages:SendAddOnlyOptionsLazyView x:Name="_lazyOptionsView" IsVisible="{Binding ShowOptions}" />
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
<controls:AccountSwitchingOverlayView
|
||||
x:Name="_accountListOverlay"
|
||||
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
|
||||
AbsoluteLayout.LayoutFlags="All"
|
||||
LongPressAccountEnabled="False"
|
||||
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
|
||||
</AbsoluteLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
179
src/Core/Pages/Send/SendAddOnlyPage.xaml.cs
Normal file
179
src/Core/Pages/Send/SendAddOnlyPage.xaml.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a version of <see cref="SendAddEditPage"/> that is reduced for adding only and adapted
|
||||
/// for performance for iOS Share extension.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should NOT be used in Android.
|
||||
/// </remarks>
|
||||
public partial class SendAddOnlyPage : BaseContentPage
|
||||
{
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
private AppOptions _appOptions;
|
||||
private SendAddEditPageViewModel _vm;
|
||||
|
||||
public Action OnClose { get; set; }
|
||||
public Action AfterSubmit { get; set; }
|
||||
|
||||
public SendAddOnlyPage(
|
||||
AppOptions appOptions = null,
|
||||
string sendId = null,
|
||||
SendType? type = null)
|
||||
{
|
||||
if (appOptions?.IosExtension != true)
|
||||
{
|
||||
throw new InvalidOperationException(nameof(SendAddOnlyPage) + " is only prepared to be used in iOS share extension");
|
||||
}
|
||||
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SendAddEditPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.SendId = sendId;
|
||||
_vm.Type = appOptions?.CreateSend?.Item1 ?? type;
|
||||
|
||||
if (_vm.IsText)
|
||||
{
|
||||
_nameEntry.ReturnType = ReturnType.Next;
|
||||
_nameEntry.ReturnCommand = new Command(() => _textEditor.Focus());
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
try
|
||||
{
|
||||
if (!await AppHelpers.IsVaultTimeoutImmediateAsync())
|
||||
{
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
}
|
||||
if (await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _vm.InitAsync();
|
||||
|
||||
if (!await _vm.LoadAsync())
|
||||
{
|
||||
await CloseAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_accountAvatar?.OnAppearing();
|
||||
await Device.InvokeOnMainThreadAsync(async () => _vm.AvatarImageSource = await GetAvatarImageSourceAsync());
|
||||
|
||||
await HandleCreateRequest();
|
||||
if (string.IsNullOrWhiteSpace(_vm.Send?.Name))
|
||||
{
|
||||
RequestFocus(_nameEntry);
|
||||
}
|
||||
AdjustToolbar();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
await CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_accountAvatar?.OnDisappearing();
|
||||
}
|
||||
|
||||
private async Task CloseAsync()
|
||||
{
|
||||
if (OnClose is null)
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnClose();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Save_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var submitted = await _vm.SubmitAsync();
|
||||
if (submitted)
|
||||
{
|
||||
AfterSubmit?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void AdjustToolbar()
|
||||
{
|
||||
_saveItem.IsEnabled = _vm.SendEnabled;
|
||||
}
|
||||
|
||||
private Task HandleCreateRequest()
|
||||
{
|
||||
if (_appOptions?.CreateSend == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_vm.IsAddFromShare = true;
|
||||
_vm.CopyInsteadOfShareAfterSaving = _appOptions.CopyInsteadOfShareAfterSaving;
|
||||
|
||||
var name = _appOptions.CreateSend.Item2;
|
||||
_vm.Send.Name = name;
|
||||
|
||||
var type = _appOptions.CreateSend.Item1;
|
||||
if (type == SendType.File)
|
||||
{
|
||||
_vm.FileData = _appOptions.CreateSend.Item3;
|
||||
_vm.FileName = name;
|
||||
}
|
||||
else
|
||||
{
|
||||
var text = _appOptions.CreateSend.Item4;
|
||||
_vm.Send.Text.Text = text;
|
||||
_vm.TriggerSendTextPropertyChanged();
|
||||
}
|
||||
_appOptions.CreateSend = null;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void OptionsHeader_Tapped(object sender, EventArgs e)
|
||||
{
|
||||
_vm.ToggleOptionsCommand.Execute(null);
|
||||
|
||||
if (!_lazyOptionsView.IsLoaded)
|
||||
{
|
||||
_lazyOptionsView.MainScrollView = _scrollView;
|
||||
_lazyOptionsView.LoadViewAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public interface ISendGroupingsPageListItem
|
||||
{
|
||||
}
|
||||
}
|
||||
176
src/Core/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml
Normal file
176
src/Core/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml
Normal file
@@ -0,0 +1,176 @@
|
||||
<?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.SendGroupingsPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
x:DataType="pages:SendGroupingsPageViewModel"
|
||||
Title="{Binding PageTitle}"
|
||||
x:Name="_page">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SendGroupingsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem IconImageSource="search.png" Clicked="Search_Clicked"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n Search}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
|
||||
<ToolbarItem x:Name="_aboutIconItem" x:Key="aboutIconItem" IconImageSource="info.png"
|
||||
Clicked="About_Clicked" Order="Primary" Priority="-1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n AboutSend}" />
|
||||
<ToolbarItem x:Name="_syncItem" x:Key="syncItem" Text="{u:I18n Sync}"
|
||||
Clicked="Sync_Clicked" Order="Secondary" />
|
||||
<ToolbarItem x:Name="_lockItem" x:Key="lockItem" Text="{u:I18n Lock}"
|
||||
Clicked="Lock_Clicked" Order="Secondary" />
|
||||
<ToolbarItem x:Name="_aboutTextItem" x:Key="aboutTextItem" Text="{u:I18n AboutSend}"
|
||||
Clicked="About_Clicked" Order="Secondary" />
|
||||
<ToolbarItem x:Name="_addItem" x:Key="addItem" IconImageSource="plus.png"
|
||||
Clicked="AddButton_Clicked" Order="Primary"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n AddItem}" />
|
||||
|
||||
<DataTemplate x:Key="sendTemplate"
|
||||
x:DataType="pages:SendGroupingsPageListItem">
|
||||
<controls:SendViewCell
|
||||
Send="{Binding Send}"
|
||||
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}"
|
||||
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}"
|
||||
AutomationId="SendCell" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="sendGroupTemplate"
|
||||
x:DataType="pages:SendGroupingsPageListItem">
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform"
|
||||
AutomationId="{Binding AutomationId}">
|
||||
<controls:IconLabel Text="{Binding Icon, Mode=OneWay}"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform"
|
||||
ShouldUpdateFontSizeDynamicallyForAccesibility="True">
|
||||
<controls:IconLabel.Effects>
|
||||
<effects:FixedSizeEffect />
|
||||
</controls:IconLabel.Effects>
|
||||
</controls:IconLabel>
|
||||
<Label Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="TailTruncation"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title"
|
||||
AutomationId="SendFilterNameLabel" />
|
||||
<Label Text="{Binding ItemCount, Mode=OneWay}"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
StyleClass="list-sub"
|
||||
AutomationId="SendFilterCountLabel" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate
|
||||
x:Key="headerTemplate"
|
||||
x:DataType="pages:SendGroupingsPageHeaderListItem">
|
||||
<StackLayout
|
||||
Spacing="0" Padding="0" VerticalOptions="FillAndExpand"
|
||||
StyleClass="list-row-header-container, list-row-header-container-platform">
|
||||
<BoxView
|
||||
StyleClass="list-section-separator-top, list-section-separator-top-platform" />
|
||||
<StackLayout StyleClass="list-row-header, list-row-header-platform">
|
||||
<Label
|
||||
Text="{Binding Title}"
|
||||
StyleClass="list-header, list-header-platform" />
|
||||
<Label
|
||||
Text="{Binding ItemCount}"
|
||||
StyleClass="list-header-sub" />
|
||||
</StackLayout>
|
||||
<BoxView
|
||||
StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" />
|
||||
</StackLayout>
|
||||
</DataTemplate>
|
||||
|
||||
<pages:SendGroupingsPageListItemSelector x:Key="sendListItemDataTemplateSelector"
|
||||
HeaderTemplate="{StaticResource headerTemplate}"
|
||||
SendTemplate="{StaticResource sendTemplate}"
|
||||
GroupTemplate="{StaticResource sendGroupTemplate}" />
|
||||
|
||||
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
|
||||
<StackLayout
|
||||
IsVisible="{Binding SendEnabled, Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box">
|
||||
<Frame
|
||||
Padding="10"
|
||||
Margin="0, 12, 0, 6"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="{DynamicResource PrimaryColor}">
|
||||
<Label
|
||||
Text="{u:I18n SendDisabledWarning}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Padding="20, 0"
|
||||
Spacing="20"
|
||||
IsVisible="{Binding ShowNoData}">
|
||||
<Label
|
||||
Text="{Binding NoDataText}"
|
||||
HorizontalTextAlignment="Center" />
|
||||
<Button
|
||||
Text="{u:I18n AddASend}"
|
||||
Clicked="AddButton_Clicked" />
|
||||
</StackLayout>
|
||||
|
||||
<RefreshView
|
||||
IsVisible="{Binding ShowList}"
|
||||
IsRefreshing="{Binding Refreshing}"
|
||||
Command="{Binding RefreshCommand}">
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding GroupedSends}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
ItemTemplate="{StaticResource sendListItemDataTemplateSelector}"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Send Groupings Page" />
|
||||
</RefreshView>
|
||||
</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" />
|
||||
<Button
|
||||
x:Name="_fab"
|
||||
ImageSource="plus.png"
|
||||
IsVisible="{Binding SendEnabled}"
|
||||
Clicked="AddButton_Clicked"
|
||||
Style="{StaticResource btn-fab}"
|
||||
AbsoluteLayout.LayoutFlags="PositionProportional"
|
||||
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
SemanticProperties.Description="{u:I18n AddItem}">
|
||||
<Button.Effects>
|
||||
<effects:FabShadowEffect />
|
||||
</Button.Effects>
|
||||
</Button>
|
||||
</AbsoluteLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
207
src/Core/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs
Normal file
207
src/Core/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Models;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SendGroupingsPage : BaseContentPage
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly ISendService _sendService;
|
||||
private readonly SendGroupingsPageViewModel _vm;
|
||||
private readonly string _pageName;
|
||||
|
||||
private AppOptions _appOptions;
|
||||
|
||||
public SendGroupingsPage(bool mainPage, SendType? type = null, string pageTitle = null,
|
||||
AppOptions appOptions = null)
|
||||
{
|
||||
_pageName = string.Concat(nameof(SendGroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||
InitializeComponent();
|
||||
SetActivityIndicator(_mainContent);
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_sendService = ServiceContainer.Resolve<ISendService>("sendService");
|
||||
_vm = BindingContext as SendGroupingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.MainPage = mainPage;
|
||||
_vm.Type = type;
|
||||
_appOptions = appOptions;
|
||||
if (pageTitle != null)
|
||||
{
|
||||
_vm.PageTitle = pageTitle;
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (type == null)
|
||||
{
|
||||
ToolbarItems.Add(_aboutIconItem);
|
||||
}
|
||||
ToolbarItems.Add(_addItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolbarItems.Add(_syncItem);
|
||||
ToolbarItems.Add(_lockItem);
|
||||
ToolbarItems.Add(_aboutTextItem);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
if (_syncService.SyncInProgress)
|
||||
{
|
||||
IsBusy = true;
|
||||
}
|
||||
|
||||
_broadcasterService.Subscribe(_pageName, async (message) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (message.Command == "syncStarted")
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() => IsBusy = true);
|
||||
}
|
||||
else if (message.Command == "syncCompleted" || message.Command == "sendUpdated")
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
IsBusy = false;
|
||||
if (_vm.LoadedOnce)
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
});
|
||||
|
||||
await LoadOnAppearedAsync(_mainLayout, false, async () =>
|
||||
{
|
||||
if (!_syncService.SyncInProgress || (await _sendService.GetAllAsync()).Any())
|
||||
{
|
||||
try
|
||||
{
|
||||
await _vm.LoadAsync();
|
||||
}
|
||||
catch (Exception e) when (e.Message.Contains("No key."))
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
await _vm.LoadAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
if (!_vm.Loaded)
|
||||
{
|
||||
await _vm.LoadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
AdjustToolbar();
|
||||
await CheckAddRequest();
|
||||
}, _mainContent);
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
IsBusy = false;
|
||||
_broadcasterService.Unsubscribe(_pageName);
|
||||
_vm.DisableRefreshing();
|
||||
}
|
||||
|
||||
private async Task CheckAddRequest()
|
||||
{
|
||||
if (_appOptions?.CreateSend != null)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var page = new SendAddEditPage(_appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is SendGroupingsPageListItem item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Send != null)
|
||||
{
|
||||
await _vm.SelectSendAsync(item.Send);
|
||||
}
|
||||
else if (item.Type != null)
|
||||
{
|
||||
await _vm.SelectTypeAsync(item.Type.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async void Search_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var page = new SendsPage(_vm.Filter, _vm.Type);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
private async void Sync_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
await _vm.SyncAsync();
|
||||
}
|
||||
|
||||
private async void Lock_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
await _vaultTimeoutService.LockAsync(true, true);
|
||||
}
|
||||
|
||||
private void About_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
_vm.ShowAbout();
|
||||
}
|
||||
|
||||
private async void AddButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var page = new SendAddEditPage(null, null, _vm.Type);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
private void AdjustToolbar()
|
||||
{
|
||||
_addItem.IsEnabled = _vm.SendEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SendGroupingsPageHeaderListItem : ISendGroupingsPageListItem
|
||||
{
|
||||
public SendGroupingsPageHeaderListItem(string title, string itemCount)
|
||||
{
|
||||
Title = title;
|
||||
ItemCount = itemCount;
|
||||
}
|
||||
|
||||
public string Title { get; }
|
||||
public string ItemCount { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SendGroupingsPageListGroup : List<SendGroupingsPageListItem>
|
||||
{
|
||||
public SendGroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false)
|
||||
: this(new List<SendGroupingsPageListItem>(), name, count, doUpper, first) { }
|
||||
|
||||
public SendGroupingsPageListGroup(List<SendGroupingsPageListItem> sendGroupItems, string name, int count,
|
||||
bool doUpper = true, bool first = false)
|
||||
{
|
||||
AddRange(sendGroupItems);
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
Name = "-";
|
||||
}
|
||||
else if (doUpper)
|
||||
{
|
||||
Name = name.ToUpperInvariant();
|
||||
}
|
||||
else
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
ItemCount = count > 0 ? count.ToString("N0") : "";
|
||||
First = first;
|
||||
}
|
||||
|
||||
public bool First { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string NameShort => string.IsNullOrWhiteSpace(Name) || Name.Length == 0 ? "-" : Name[0].ToString();
|
||||
public string ItemCount { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.View;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SendGroupingsPageListItem : ISendGroupingsPageListItem
|
||||
{
|
||||
private string _icon;
|
||||
private string _name;
|
||||
|
||||
public SendView Send { get; set; }
|
||||
public SendType? Type { get; set; }
|
||||
public string ItemCount { get; set; }
|
||||
public bool ShowOptions { get; set; }
|
||||
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_name != null)
|
||||
{
|
||||
return _name;
|
||||
}
|
||||
if (Type != null)
|
||||
{
|
||||
switch (Type.Value)
|
||||
{
|
||||
case SendType.Text:
|
||||
_name = AppResources.TypeText;
|
||||
break;
|
||||
case SendType.File:
|
||||
_name = AppResources.TypeFile;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return _name;
|
||||
}
|
||||
}
|
||||
|
||||
public string Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_icon != null)
|
||||
{
|
||||
return _icon;
|
||||
}
|
||||
if (Type != null)
|
||||
{
|
||||
switch (Type.Value)
|
||||
{
|
||||
case SendType.Text:
|
||||
_icon = BitwardenIcons.FileText;
|
||||
break;
|
||||
case SendType.File:
|
||||
_icon = BitwardenIcons.File;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return _icon;
|
||||
}
|
||||
}
|
||||
|
||||
public string AutomationId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_name != null)
|
||||
{
|
||||
return "SendItem";
|
||||
}
|
||||
if (Type != null)
|
||||
{
|
||||
switch (Type.Value)
|
||||
{
|
||||
case SendType.Text:
|
||||
return "SendTextFilter";
|
||||
case SendType.File:
|
||||
return "SendFileFilter";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SendGroupingsPageListItemSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate HeaderTemplate { get; set; }
|
||||
public DataTemplate SendTemplate { get; set; }
|
||||
public DataTemplate GroupTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
|
||||
{
|
||||
if (item is SendGroupingsPageHeaderListItem)
|
||||
{
|
||||
return HeaderTemplate;
|
||||
}
|
||||
|
||||
if (item is SendGroupingsPageListItem listItem)
|
||||
{
|
||||
return listItem.Send != null ? SendTemplate : GroupTemplate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
using DeviceType = Bit.Core.Enums.DeviceType;
|
||||
using Microsoft.Maui.Networking;
|
||||
using Microsoft.Maui.Devices;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SendGroupingsPageViewModel : BaseViewModel
|
||||
{
|
||||
private bool _sendEnabled;
|
||||
private bool _refreshing;
|
||||
private bool _doingLoad;
|
||||
private bool _loading;
|
||||
private bool _loaded;
|
||||
private bool _showNoData;
|
||||
private bool _showList;
|
||||
private bool _syncRefreshing;
|
||||
private string _noDataText;
|
||||
private List<SendView> _allSends;
|
||||
private Dictionary<SendType, int> _typeCounts = new Dictionary<SendType, int>();
|
||||
|
||||
private readonly ISendService _sendService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
|
||||
public SendGroupingsPageViewModel()
|
||||
{
|
||||
_sendService = ServiceContainer.Resolve<ISendService>("sendService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
|
||||
Loading = true;
|
||||
PageTitle = AppResources.Send;
|
||||
GroupedSends = new ObservableRangeCollection<ISendGroupingsPageListItem>();
|
||||
RefreshCommand = new Command(async () =>
|
||||
{
|
||||
Refreshing = true;
|
||||
await LoadAsync();
|
||||
});
|
||||
SendOptionsCommand = new Command<SendView>(SendOptionsAsync);
|
||||
}
|
||||
|
||||
public bool MainPage { get; set; }
|
||||
public SendType? Type { get; set; }
|
||||
public Func<SendView, bool> Filter { get; set; }
|
||||
public bool HasSends { get; set; }
|
||||
public List<SendView> Sends { get; set; }
|
||||
|
||||
public bool SendEnabled
|
||||
{
|
||||
get => _sendEnabled;
|
||||
set => SetProperty(ref _sendEnabled, value);
|
||||
}
|
||||
public bool Refreshing
|
||||
{
|
||||
get => _refreshing;
|
||||
set => SetProperty(ref _refreshing, value);
|
||||
}
|
||||
public bool SyncRefreshing
|
||||
{
|
||||
get => _syncRefreshing;
|
||||
set => SetProperty(ref _syncRefreshing, value);
|
||||
}
|
||||
public bool Loading
|
||||
{
|
||||
get => _loading;
|
||||
set => SetProperty(ref _loading, value);
|
||||
}
|
||||
public bool Loaded
|
||||
{
|
||||
get => _loaded;
|
||||
set => SetProperty(ref _loaded, value);
|
||||
}
|
||||
public bool ShowNoData
|
||||
{
|
||||
get => _showNoData;
|
||||
set => SetProperty(ref _showNoData, value);
|
||||
}
|
||||
public string NoDataText
|
||||
{
|
||||
get => _noDataText;
|
||||
set => SetProperty(ref _noDataText, value);
|
||||
}
|
||||
public bool ShowList
|
||||
{
|
||||
get => _showList;
|
||||
set => SetProperty(ref _showList, value);
|
||||
}
|
||||
public ObservableRangeCollection<ISendGroupingsPageListItem> GroupedSends { get; set; }
|
||||
public Command RefreshCommand { get; set; }
|
||||
public Command<SendView> SendOptionsCommand { get; set; }
|
||||
public bool LoadedOnce { get; set; }
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
if (_doingLoad)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var authed = await _stateService.IsAuthenticatedAsync();
|
||||
if (!authed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (await _stateService.GetSyncOnRefreshAsync() && Refreshing && !SyncRefreshing)
|
||||
{
|
||||
SyncRefreshing = true;
|
||||
await _syncService.FullSyncAsync(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_doingLoad = true;
|
||||
LoadedOnce = true;
|
||||
ShowNoData = false;
|
||||
Loading = true;
|
||||
ShowList = false;
|
||||
SendEnabled = !await AppHelpers.IsSendDisabledByPolicyAsync();
|
||||
var groupedSends = new List<SendGroupingsPageListGroup>();
|
||||
var page = Page as SendGroupingsPage;
|
||||
|
||||
try
|
||||
{
|
||||
await LoadDataAsync();
|
||||
|
||||
// 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
|
||||
var uppercaseGroupNames = Device.RuntimePlatform == Device.iOS;
|
||||
if (MainPage)
|
||||
{
|
||||
groupedSends.Add(new SendGroupingsPageListGroup(
|
||||
AppResources.Types, 0, uppercaseGroupNames, true)
|
||||
{
|
||||
new SendGroupingsPageListItem
|
||||
{
|
||||
Type = SendType.Text,
|
||||
ItemCount = (_typeCounts.ContainsKey(SendType.Text) ?
|
||||
_typeCounts[SendType.Text] : 0).ToString("N0")
|
||||
},
|
||||
new SendGroupingsPageListItem
|
||||
{
|
||||
Type = SendType.File,
|
||||
ItemCount = (_typeCounts.ContainsKey(SendType.File) ?
|
||||
_typeCounts[SendType.File] : 0).ToString("N0")
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (Sends?.Any() ?? false)
|
||||
{
|
||||
var sendsListItems = Sends.Select(s => new SendGroupingsPageListItem
|
||||
{
|
||||
Send = s,
|
||||
ShowOptions = SendEnabled
|
||||
}).ToList();
|
||||
groupedSends.Add(new SendGroupingsPageListGroup(sendsListItems,
|
||||
MainPage ? AppResources.AllSends : AppResources.Sends, sendsListItems.Count,
|
||||
uppercaseGroupNames, !MainPage));
|
||||
}
|
||||
|
||||
// TODO: refactor 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.Android
|
||||
||
|
||||
GroupedSends.Any())
|
||||
{
|
||||
var items = new List<ISendGroupingsPageListItem>();
|
||||
foreach (var itemGroup in groupedSends)
|
||||
{
|
||||
items.Add(new SendGroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount));
|
||||
items.AddRange(itemGroup);
|
||||
}
|
||||
|
||||
GroupedSends.ReplaceRange(items);
|
||||
}
|
||||
else
|
||||
{
|
||||
// HACK: we need this on iOS, so that it doesn't crash when adding coming from an empty list
|
||||
var first = true;
|
||||
var items = new List<ISendGroupingsPageListItem>();
|
||||
foreach (var itemGroup in groupedSends)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
items.Add(new SendGroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount));
|
||||
}
|
||||
else
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
items.AddRange(itemGroup);
|
||||
}
|
||||
|
||||
if (groupedSends.Any())
|
||||
{
|
||||
GroupedSends.ReplaceRange(new List<ISendGroupingsPageListItem> { new SendGroupingsPageHeaderListItem(groupedSends[0].Name, groupedSends[0].ItemCount) });
|
||||
GroupedSends.AddRange(items);
|
||||
}
|
||||
else
|
||||
{
|
||||
GroupedSends.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_doingLoad = false;
|
||||
Loaded = true;
|
||||
Loading = false;
|
||||
ShowNoData = (MainPage && !HasSends) || !groupedSends.Any();
|
||||
ShowList = !ShowNoData;
|
||||
DisableRefreshing();
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableRefreshing()
|
||||
{
|
||||
Refreshing = false;
|
||||
SyncRefreshing = false;
|
||||
}
|
||||
|
||||
public async Task SelectSendAsync(SendView send)
|
||||
{
|
||||
var page = new SendAddEditPage(null, send.Id);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
public async Task SelectTypeAsync(SendType type)
|
||||
{
|
||||
string title = null;
|
||||
switch (type)
|
||||
{
|
||||
case SendType.Text:
|
||||
title = AppResources.TypeText;
|
||||
break;
|
||||
case SendType.File:
|
||||
title = AppResources.TypeFile;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
var page = new SendGroupingsPage(false, type, title);
|
||||
await Page.Navigation.PushAsync(page);
|
||||
}
|
||||
|
||||
public async Task SyncAsync()
|
||||
{
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
|
||||
try
|
||||
{
|
||||
await _syncService.FullSyncAsync(false, true);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("error", null, AppResources.SyncingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowAbout()
|
||||
{
|
||||
_platformUtilsService.LaunchUri("https://bitwarden.com/products/send/");
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
NoDataText = AppResources.NoSends;
|
||||
_allSends = await _sendService.GetAllDecryptedAsync();
|
||||
HasSends = _allSends.Any();
|
||||
_typeCounts.Clear();
|
||||
Filter = null;
|
||||
|
||||
if (MainPage)
|
||||
{
|
||||
Sends = _allSends;
|
||||
foreach (var c in _allSends)
|
||||
{
|
||||
if (_typeCounts.ContainsKey(c.Type))
|
||||
{
|
||||
_typeCounts[c.Type] = _typeCounts[c.Type] + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
_typeCounts.Add(c.Type, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Type != null)
|
||||
{
|
||||
Filter = c => c.Type == Type.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
PageTitle = AppResources.AllSends;
|
||||
}
|
||||
Sends = Filter != null ? _allSends.Where(Filter).ToList() : _allSends;
|
||||
}
|
||||
}
|
||||
|
||||
private async void SendOptionsAsync(SendView send)
|
||||
{
|
||||
if ((Page as BaseContentPage).DoOnce())
|
||||
{
|
||||
var selection = await AppHelpers.SendListOptions(Page, send);
|
||||
if (selection == AppResources.RemovePassword || selection == AppResources.Delete)
|
||||
{
|
||||
await LoadAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/Core/Pages/Send/SendsPage.xaml
Normal file
85
src/Core/Pages/Send/SendsPage.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"
|
||||
x:Class="Bit.App.Pages.SendsPage"
|
||||
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:core="clr-namespace:Bit.Core"
|
||||
x:DataType="pages:SendsPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SendsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
Spacing="0"
|
||||
Padding="0"
|
||||
x:Name="_titleLayout"
|
||||
x:Key="titleLayout">
|
||||
<controls:MiButton
|
||||
StyleClass="btn-title, btn-title-platform"
|
||||
Text=""
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Clicked="BackButton_Clicked"
|
||||
x:Name="_backButton" />
|
||||
<controls:ExtendedSearchBar
|
||||
x:Name="_searchBar"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
TextChanged="SearchBar_TextChanged"
|
||||
SearchButtonPressed="SearchBar_SearchButtonPressed"
|
||||
Placeholder="{Binding PageTitle}" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform"
|
||||
x:Name="_separator" x:Key="separator" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<StackLayout x:Name="_mainLayout" Spacing="0" Padding="0">
|
||||
<controls:IconLabel IsVisible="{Binding ShowSearchDirection}"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Search}}"
|
||||
StyleClass="text-muted"
|
||||
FontSize="50"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
<Label IsVisible="{Binding ShowNoData}"
|
||||
Text="{u:I18n NoItemsToList}"
|
||||
Margin="20, 0"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center"
|
||||
AutomationId="NoSendDisplayedLabel" />
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowList}"
|
||||
ItemsSource="{Binding Sends}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform"
|
||||
ExtraDataForLogging="Sends Page"
|
||||
AutomationId="SendCellList">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:SendView">
|
||||
<controls:SendViewCell
|
||||
Send="{Binding .}"
|
||||
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}"
|
||||
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}"
|
||||
AutomationId="SendCell" />
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
122
src/Core/Pages/Send/SendsPage.xaml.cs
Normal file
122
src/Core/Pages/Send/SendsPage.xaml.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.View;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SendsPage : BaseContentPage
|
||||
{
|
||||
private SendsPageViewModel _vm;
|
||||
private bool _hasFocused;
|
||||
|
||||
public SendsPage(Func<SendView, bool> filter, SendType? type = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SendsPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.Filter = filter;
|
||||
if (type != null)
|
||||
{
|
||||
if (type == SendType.File)
|
||||
{
|
||||
_vm.PageTitle = AppResources.SearchFileSends;
|
||||
}
|
||||
else if (type == SendType.Text)
|
||||
{
|
||||
_vm.PageTitle = AppResources.SearchTextSends;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_vm.PageTitle = AppResources.SearchSends;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
ToolbarItems.Add(_closeItem);
|
||||
_searchBar.Placeholder = AppResources.Search;
|
||||
_mainLayout.Children.Insert(0, _searchBar);
|
||||
_mainLayout.Children.Insert(1, _separator);
|
||||
ShowModalAnimationDelay = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
NavigationPage.SetTitleView(this, _titleLayout);
|
||||
}
|
||||
}
|
||||
|
||||
public SearchBar SearchBar => _searchBar;
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
if (!_hasFocused)
|
||||
{
|
||||
_hasFocused = true;
|
||||
RequestFocus(_searchBar);
|
||||
}
|
||||
}
|
||||
|
||||
private void SearchBar_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
var oldLength = e.OldTextValue?.Length ?? 0;
|
||||
var newLength = e.NewTextValue?.Length ?? 0;
|
||||
if (oldLength < 2 && newLength < 2 && oldLength < newLength)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_vm.Search(e.NewTextValue, 200);
|
||||
}
|
||||
|
||||
private void SearchBar_SearchButtonPressed(object sender, EventArgs e)
|
||||
{
|
||||
_vm.Search((sender as SearchBar).Text);
|
||||
}
|
||||
|
||||
private void BackButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
GoBack();
|
||||
}
|
||||
|
||||
protected override bool OnBackButtonPressed()
|
||||
{
|
||||
GoBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void GoBack()
|
||||
{
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
Navigation.PopModalAsync(false);
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.CurrentSelection?.FirstOrDefault() is SendView send)
|
||||
{
|
||||
await _vm.SelectSendAsync(send);
|
||||
}
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
GoBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
129
src/Core/Pages/Send/SendsPageViewModel.cs
Normal file
129
src/Core/Pages/Send/SendsPageViewModel.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SendsPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly ISearchService _searchService;
|
||||
|
||||
private CancellationTokenSource _searchCancellationTokenSource;
|
||||
private bool _sendEnabled;
|
||||
private bool _showNoData;
|
||||
private bool _showList;
|
||||
|
||||
public SendsPageViewModel()
|
||||
{
|
||||
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||
Sends = new ExtendedObservableCollection<SendView>();
|
||||
SendOptionsCommand = new Command<SendView>(SendOptionsAsync);
|
||||
}
|
||||
|
||||
public Command SendOptionsCommand { get; set; }
|
||||
public ExtendedObservableCollection<SendView> Sends { get; set; }
|
||||
public Func<SendView, bool> Filter { get; set; }
|
||||
|
||||
public bool SendEnabled
|
||||
{
|
||||
get => _sendEnabled;
|
||||
set => SetProperty(ref _sendEnabled, value);
|
||||
}
|
||||
|
||||
public bool ShowNoData
|
||||
{
|
||||
get => _showNoData;
|
||||
set => SetProperty(ref _showNoData, value, additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(ShowSearchDirection)
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShowList
|
||||
{
|
||||
get => _showList;
|
||||
set => SetProperty(ref _showList, value, additionalPropertyNames: new[]
|
||||
{
|
||||
nameof(ShowSearchDirection)
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShowSearchDirection => !ShowList && !ShowNoData;
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
SendEnabled = !await AppHelpers.IsSendDisabledByPolicyAsync();
|
||||
if (!string.IsNullOrWhiteSpace((Page as SendsPage).SearchBar.Text))
|
||||
{
|
||||
Search((Page as SendsPage).SearchBar.Text, 200);
|
||||
}
|
||||
}
|
||||
|
||||
public void Search(string searchText, int? timeout = null)
|
||||
{
|
||||
var previousCts = _searchCancellationTokenSource;
|
||||
var cts = new CancellationTokenSource();
|
||||
Task.Run(async () =>
|
||||
{
|
||||
List<SendView> sends = null;
|
||||
var searchable = !string.IsNullOrWhiteSpace(searchText) && searchText.Length > 1;
|
||||
if (searchable)
|
||||
{
|
||||
if (timeout != null)
|
||||
{
|
||||
await Task.Delay(timeout.Value);
|
||||
}
|
||||
if (searchText != (Page as SendsPage).SearchBar.Text)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
previousCts?.Cancel();
|
||||
}
|
||||
try
|
||||
{
|
||||
sends = await _searchService.SearchSendsAsync(searchText, Filter, null, cts.Token);
|
||||
cts.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (sends == null)
|
||||
{
|
||||
sends = new List<SendView>();
|
||||
}
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
Sends.ResetWithRange(sends);
|
||||
ShowNoData = searchable && Sends.Count == 0;
|
||||
ShowList = searchable && !ShowNoData;
|
||||
});
|
||||
}, cts.Token);
|
||||
_searchCancellationTokenSource = cts;
|
||||
}
|
||||
|
||||
public async Task SelectSendAsync(SendView send)
|
||||
{
|
||||
var page = new SendAddEditPage(null, send.Id);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async void SendOptionsAsync(SendView send)
|
||||
{
|
||||
if ((Page as BaseContentPage).DoOnce())
|
||||
{
|
||||
await AppHelpers.SendListOptions(Page, send);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user