1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-29 14:43:50 +00:00

PM-3349 PM-3350 MAUI Migration Initial

This commit is contained in:
Federico Maccaroni
2023-09-29 11:02:19 -03:00
parent bbef0f8c93
commit 8ef9443b1e
717 changed files with 5367 additions and 4702 deletions

View File

@@ -0,0 +1,43 @@
using System;
using Bit.Core.Resources.Localization;
namespace Bit.App.Utilities
{
public static class A11yExtensions
{
public enum TimeSpanVerbalizationMode
{
HoursAndMinutes,
Hours
}
public static string Verbalize(this TimeSpan timeSpan, TimeSpanVerbalizationMode mode)
{
if (mode == TimeSpanVerbalizationMode.Hours)
{
if (timeSpan.TotalHours == 1)
{
return AppResources.OneHour;
}
return string.Format(AppResources.XHours, timeSpan.TotalHours);
}
if (timeSpan.Hours == 1)
{
if (timeSpan.Minutes == 1)
{
return AppResources.OneHourAndOneMinute;
}
return string.Format(AppResources.OneHourAndXMinute, timeSpan.Minutes);
}
if (timeSpan.Minutes == 1)
{
return string.Format(AppResources.XHoursAndOneMinute, timeSpan.Hours);
}
return string.Format(AppResources.XHoursAndYMinutes, timeSpan.Hours, timeSpan.Minutes);
}
}
}

View File

@@ -0,0 +1,268 @@
using System;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.Core.Resources.Localization;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities.AccountManagement
{
public class AccountsManager : IAccountsManager
{
private readonly IBroadcasterService _broadcasterService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IStorageService _secureStorageService;
private readonly IStateService _stateService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuthService _authService;
private readonly ILogger _logger;
private readonly IMessagingService _messagingService;
private readonly IWatchDeviceService _watchDeviceService;
private readonly IConditionedAwaiterManager _conditionedAwaiterManager;
Func<AppOptions> _getOptionsFunc;
private IAccountsManagerHost _accountsManagerHost;
public AccountsManager(IBroadcasterService broadcasterService,
IVaultTimeoutService vaultTimeoutService,
IStorageService secureStorageService,
IStateService stateService,
IPlatformUtilsService platformUtilsService,
IAuthService authService,
ILogger logger,
IMessagingService messagingService,
IWatchDeviceService watchDeviceService,
IConditionedAwaiterManager conditionedAwaiterManager)
{
_broadcasterService = broadcasterService;
_vaultTimeoutService = vaultTimeoutService;
_secureStorageService = secureStorageService;
_stateService = stateService;
_platformUtilsService = platformUtilsService;
_authService = authService;
_logger = logger;
_messagingService = messagingService;
_watchDeviceService = watchDeviceService;
_conditionedAwaiterManager = conditionedAwaiterManager;
}
private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true };
public void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost)
{
_getOptionsFunc = getOptionsFunc;
_accountsManagerHost = accountsManagerHost;
_broadcasterService.Subscribe(nameof(AccountsManager), OnMessage);
}
public async Task StartDefaultNavigationFlowAsync(Action<AppOptions> appOptionsAction)
{
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
appOptionsAction(Options);
await NavigateOnAccountChangeAsync();
}
public async Task NavigateOnAccountChangeAsync(bool? isAuthed = null)
{
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
// TODO: this could be improved by doing chain of responsability pattern
// but for now it may be an overkill, if logic gets more complex consider refactoring it
var authed = isAuthed ?? await _stateService.IsAuthenticatedAsync();
if (authed)
{
if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() ||
await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
{
// TODO implement orgIdentifier flow to SSO Login page, same as email flow below
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
var email = await _stateService.GetEmailAsync();
Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null;
_accountsManagerHost.Navigate(NavigationTarget.Login, new LoginNavigationParams(email));
}
else if (await _vaultTimeoutService.IsLockedAsync() ||
await _vaultTimeoutService.ShouldLockAsync())
{
_accountsManagerHost.Navigate(NavigationTarget.Lock);
}
else if (Options.FromAutofillFramework && Options.SaveType.HasValue)
{
_accountsManagerHost.Navigate(NavigationTarget.AddEditCipher);
}
else if (Options.Uri != null)
{
_accountsManagerHost.Navigate(NavigationTarget.AutofillCiphers);
}
else if (Options.OtpData != null)
{
_accountsManagerHost.Navigate(NavigationTarget.OtpCipherSelection);
}
else if (Options.CreateSend != null)
{
_accountsManagerHost.Navigate(NavigationTarget.SendAddEdit);
}
else
{
_accountsManagerHost.Navigate(NavigationTarget.Home);
}
}
else
{
Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null;
if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() ||
await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
{
// TODO implement orgIdentifier flow to SSO Login page, same as email flow below
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
var email = await _stateService.GetEmailAsync();
await _stateService.SetRememberedEmailAsync(email);
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin);
}
else
{
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin);
}
}
}
private async void OnMessage(Message message)
{
try
{
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
switch (message.Command)
{
case AccountsManagerMessageCommands.LOCKED:
await Device.InvokeOnMainThreadAsync(() => LockedAsync(message.Data as Tuple<string, bool>));
break;
case AccountsManagerMessageCommands.LOCK_VAULT:
await _vaultTimeoutService.LockAsync(true);
break;
case AccountsManagerMessageCommands.LOGOUT:
var extras = message.Data as Tuple<string, bool, bool>;
var userId = extras?.Item1;
var userInitiated = extras?.Item2 ?? true;
var expired = extras?.Item3 ?? false;
await Device.InvokeOnMainThreadAsync(() => LogOutAsync(userId, userInitiated, expired));
break;
case AccountsManagerMessageCommands.LOGGED_OUT:
// Clean up old migrated key if they ever log out.
await _secureStorageService.RemoveAsync("oldKey");
break;
case AccountsManagerMessageCommands.ADD_ACCOUNT:
await AddAccountAsync();
break;
case AccountsManagerMessageCommands.ACCOUNT_ADDED:
await _accountsManagerHost.UpdateThemeAsync();
break;
case AccountsManagerMessageCommands.SWITCHED_ACCOUNT:
await SwitchedAccountAsync();
break;
}
}
catch (Exception ex)
{
_logger.Exception(ex);
}
}
private async Task LockedAsync(Tuple<string, bool> extras)
{
var userId = extras?.Item1;
var userInitiated = extras?.Item2 ?? false;
if (!await _stateService.IsActiveAccountAsync(userId))
{
_platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully);
return;
}
var autoPromptBiometric = !userInitiated;
// 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 (autoPromptBiometric && Device.RuntimePlatform == Device.iOS)
{
var vaultTimeout = await _stateService.GetVaultTimeoutAsync();
if (vaultTimeout == 0)
{
autoPromptBiometric = false;
}
}
await _accountsManagerHost.SetPreviousPageInfoAsync();
await Device.InvokeOnMainThreadAsync(() => _accountsManagerHost.Navigate(NavigationTarget.Lock, new LockNavigationParams(autoPromptBiometric)));
}
private async Task AddAccountAsync()
{
await AppHelpers.ClearServiceCacheAsync();
await Device.InvokeOnMainThreadAsync(() =>
{
Options.HideAccountSwitcher = false;
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin);
});
}
public async Task LogOutAsync(string userId, bool userInitiated, bool expired)
{
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
await AppHelpers.LogOutAsync(userId, userInitiated);
await NavigateOnAccountChangeAsync();
_authService.LogOut(() =>
{
if (expired)
{
_platformUtilsService.ShowToast("warning", null, AppResources.LoginExpired);
}
});
}
private async Task SwitchedAccountAsync()
{
await AppHelpers.OnAccountSwitchAsync();
await Device.InvokeOnMainThreadAsync(async () =>
{
if (await _vaultTimeoutService.ShouldTimeoutAsync())
{
await _vaultTimeoutService.ExecuteTimeoutActionAsync();
}
else
{
await NavigateOnAccountChangeAsync();
}
await Task.Delay(50);
await _accountsManagerHost.UpdateThemeAsync();
_watchDeviceService.SyncDataToWatchAsync().FireAndForget();
_messagingService.Send(AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED);
});
}
public async Task PromptToSwitchToExistingAccountAsync(string userId)
{
var switchToAccount = await _platformUtilsService.ShowDialogAsync(
AppResources.SwitchToAlreadyAddedAccountConfirmation,
AppResources.AccountAlreadyAdded, AppResources.Yes, AppResources.Cancel);
if (switchToAccount)
{
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
await _stateService.SetActiveUserAsync(userId);
_messagingService.Send("switchedAccount");
}
}
}
}

View File

@@ -0,0 +1,14 @@
using Bit.App.Abstractions;
namespace Bit.App.Utilities.AccountManagement
{
public class LockNavigationParams : INavigationParams
{
public LockNavigationParams(bool autoPromptBiometric = true)
{
AutoPromptBiometric = autoPromptBiometric;
}
public bool AutoPromptBiometric { get; }
}
}

View File

@@ -0,0 +1,14 @@
using Bit.App.Abstractions;
namespace Bit.App.Utilities.AccountManagement
{
public class LoginNavigationParams : INavigationParams
{
public LoginNavigationParams(string email)
{
Email = email;
}
public string Email { get; }
}
}

View File

@@ -0,0 +1,592 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Models;
using Bit.App.Pages;
using Bit.Core.Resources.Localization;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.View;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Newtonsoft.Json;
using Microsoft.Maui.Networking;
using Microsoft.Maui.ApplicationModel.DataTransfer;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public static class AppHelpers
{
public const string VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND = "vaultTimeoutActionChanged";
public const string RESUMED_MESSAGE_COMMAND = "resumed";
public static async Task<string> CipherListOptions(ContentPage page, CipherView cipher, IPasswordRepromptService passwordRepromptService)
{
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
var clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
var options = new List<string> { AppResources.View };
if (!cipher.IsDeleted)
{
options.Add(AppResources.Edit);
}
if (cipher.Type == Core.Enums.CipherType.Login)
{
if (!string.IsNullOrWhiteSpace(cipher.Login.Username))
{
options.Add(AppResources.CopyUsername);
}
if (!string.IsNullOrWhiteSpace(cipher.Login.Password) && cipher.ViewPassword)
{
options.Add(AppResources.CopyPassword);
}
if (!string.IsNullOrWhiteSpace(cipher.Login.Totp))
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var canAccessPremium = await stateService.CanAccessPremiumAsync();
if (canAccessPremium || cipher.OrganizationUseTotp)
{
options.Add(AppResources.CopyTotp);
}
}
if (cipher.Login.CanLaunch)
{
options.Add(AppResources.Launch);
}
}
else if (cipher.Type == Core.Enums.CipherType.Card)
{
if (!string.IsNullOrWhiteSpace(cipher.Card.Number))
{
options.Add(AppResources.CopyNumber);
}
if (!string.IsNullOrWhiteSpace(cipher.Card.Code))
{
options.Add(AppResources.CopySecurityCode);
}
}
else if (cipher.Type == Core.Enums.CipherType.SecureNote)
{
if (!string.IsNullOrWhiteSpace(cipher.Notes))
{
options.Add(AppResources.CopyNotes);
}
}
var selection = await page.DisplayActionSheet(cipher.Name, AppResources.Cancel, null, options.ToArray());
if (await vaultTimeoutService.IsLockedAsync())
{
platformUtilsService.ShowToast("info", null, AppResources.VaultIsLocked);
}
else if (selection == AppResources.View)
{
await page.Navigation.PushModalAsync(new NavigationPage(new CipherDetailsPage(cipher.Id)));
}
else if (selection == AppResources.Edit
&&
await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(cipher.Reprompt))
{
await page.Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(cipher.Id)));
}
else if (selection == AppResources.CopyUsername)
{
await clipboardService.CopyTextAsync(cipher.Login.Username);
platformUtilsService.ShowToastForCopiedValue(AppResources.Username);
}
else if (selection == AppResources.CopyPassword
&&
await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(cipher.Reprompt))
{
await clipboardService.CopyTextAsync(cipher.Login.Password);
platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedPassword, cipher.Id);
}
else if (selection == AppResources.CopyTotp
&&
await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(cipher.Reprompt))
{
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
if (!string.IsNullOrWhiteSpace(totp))
{
await clipboardService.CopyTextAsync(totp);
platformUtilsService.ShowToastForCopiedValue(AppResources.VerificationCodeTotp);
}
}
else if (selection == AppResources.Launch && cipher.CanLaunch)
{
platformUtilsService.LaunchUri(cipher.LaunchUri);
}
else if (selection == AppResources.CopyNumber
&&
await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(cipher.Reprompt))
{
await clipboardService.CopyTextAsync(cipher.Card.Number);
platformUtilsService.ShowToastForCopiedValue(AppResources.Number);
}
else if (selection == AppResources.CopySecurityCode
&&
await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(cipher.Reprompt))
{
await clipboardService.CopyTextAsync(cipher.Card.Code);
platformUtilsService.ShowToastForCopiedValue(AppResources.SecurityCode);
eventService.CollectAsync(EventType.Cipher_ClientCopiedCardCode, cipher.Id).FireAndForget();
}
else if (selection == AppResources.CopyNotes)
{
await clipboardService.CopyTextAsync(cipher.Notes);
platformUtilsService.ShowToastForCopiedValue(AppResources.Notes);
}
return selection;
}
public static async Task<string> SendListOptions(ContentPage page, SendView send)
{
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
var options = new List<string> { AppResources.Edit };
options.Add(AppResources.CopyLink);
options.Add(AppResources.ShareLink);
if (send.HasPassword)
{
options.Add(AppResources.RemovePassword);
}
var selection = await page.DisplayActionSheet(send.Name, AppResources.Cancel, AppResources.Delete,
options.ToArray());
if (await vaultTimeoutService.IsLockedAsync())
{
platformUtilsService.ShowToast("info", null, AppResources.VaultIsLocked);
}
else if (selection == AppResources.Edit)
{
await page.Navigation.PushModalAsync(new NavigationPage(new SendAddEditPage(null, send.Id)));
}
else if (selection == AppResources.CopyLink)
{
await CopySendUrlAsync(send);
}
else if (selection == AppResources.ShareLink)
{
await ShareSendUrlAsync(send);
}
else if (selection == AppResources.RemovePassword)
{
await RemoveSendPasswordAsync(send.Id);
}
else if (selection == AppResources.Delete)
{
await DeleteSendAsync(send.Id);
}
return selection;
}
public static async Task<string> AccountListOptions(ContentPage page, AccountViewCellViewModel accountViewCell)
{
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var userId = accountViewCell.AccountView.UserId;
List<string> options;
if (await vaultTimeoutService.IsLoggedOutByTimeoutAsync(userId) ||
await vaultTimeoutService.ShouldLogOutByTimeoutAsync(userId))
{
options = new List<string> { AppResources.RemoveAccount };
}
else if (await vaultTimeoutService.IsLockedAsync(userId) ||
await vaultTimeoutService.ShouldLockAsync(userId))
{
options = new List<string> { AppResources.LogOut };
}
else
{
options = new List<string> { AppResources.Lock, AppResources.LogOut };
}
var accountSummary = accountViewCell.AccountView.Email;
if (!string.IsNullOrWhiteSpace(accountViewCell.AccountView.Hostname))
{
accountSummary += "\n" + accountViewCell.AccountView.Hostname;
}
var selection = await page.DisplayActionSheet(accountSummary, AppResources.Cancel, null, options.ToArray());
if (selection == AppResources.Lock)
{
await vaultTimeoutService.LockAsync(true, true, userId);
}
else if (selection == AppResources.LogOut || selection == AppResources.RemoveAccount)
{
var title = selection == AppResources.LogOut ? AppResources.LogOut : AppResources.RemoveAccount;
var text = (selection == AppResources.LogOut ? AppResources.LogoutConfirmation
: AppResources.RemoveAccountConfirmation) + "\n\n" + accountSummary;
var confirmed =
await platformUtilsService.ShowDialogAsync(text, title, AppResources.Yes, AppResources.Cancel);
if (confirmed)
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
if (await stateService.IsActiveAccountAsync(userId))
{
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
messagingService.Send("logout");
return selection;
}
await LogOutAsync(userId, true);
}
}
return selection;
}
public static async Task CopySendUrlAsync(SendView send)
{
if (await IsSendDisabledByPolicyAsync())
{
return;
}
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
await clipboardService.CopyTextAsync(GetSendUrl(send));
platformUtilsService.ShowToastForCopiedValue(AppResources.SendLink);
}
public static async Task ShareSendUrlAsync(SendView send)
{
if (await IsSendDisabledByPolicyAsync())
{
return;
}
await Share.RequestAsync(new ShareTextRequest
{
Uri = new Uri(GetSendUrl(send)).ToString(),
Title = AppResources.Send + " " + send.Name,
Subject = send.Name
});
}
private static string GetSendUrl(SendView send)
{
var environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
return environmentService.GetWebSendUrl() + send.AccessId + "/" + send.UrlB64Key;
}
public static async Task<bool> RemoveSendPasswordAsync(string sendId)
{
if (await IsSendDisabledByPolicyAsync())
{
return false;
}
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
var sendService = ServiceContainer.Resolve<ISendService>("sendService");
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return false;
}
var confirmed = await platformUtilsService.ShowDialogAsync(
AppResources.AreYouSureRemoveSendPassword,
null, AppResources.Yes, AppResources.Cancel);
if (!confirmed)
{
return false;
}
try
{
await deviceActionService.ShowLoadingAsync(AppResources.RemovingSendPassword);
await sendService.RemovePasswordWithServerAsync(sendId);
await deviceActionService.HideLoadingAsync();
platformUtilsService.ShowToast("success", null, AppResources.SendPasswordRemoved);
return true;
}
catch (ApiException e)
{
await deviceActionService.HideLoadingAsync();
if (e?.Error != null)
{
await platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
AppResources.AnErrorHasOccurred);
}
}
return false;
}
public static async Task<bool> DeleteSendAsync(string sendId)
{
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
var sendService = ServiceContainer.Resolve<ISendService>("sendService");
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return false;
}
var confirmed = await platformUtilsService.ShowDialogAsync(
AppResources.AreYouSureDeleteSend,
null, AppResources.Yes, AppResources.Cancel);
if (!confirmed)
{
return false;
}
try
{
await deviceActionService.ShowLoadingAsync(AppResources.Deleting);
await sendService.DeleteWithServerAsync(sendId);
await deviceActionService.HideLoadingAsync();
platformUtilsService.ShowToast("success", null, AppResources.SendDeleted);
return true;
}
catch (ApiException e)
{
await deviceActionService.HideLoadingAsync();
if (e?.Error != null)
{
await platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
AppResources.AnErrorHasOccurred);
}
}
return false;
}
public static async Task<bool> IsSendDisabledByPolicyAsync()
{
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
return await policyService.PolicyAppliesToUser(PolicyType.DisableSend);
}
public static async Task<bool> IsHideEmailDisabledByPolicyAsync()
{
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
return await policyService.PolicyAppliesToUser(PolicyType.SendOptions,
policy => policy.Data.ContainsKey("disableHideEmail") && (bool)policy.Data["disableHideEmail"]);
}
public static async Task<bool> PerformUpdateTasksAsync(ISyncService syncService,
IDeviceActionService deviceActionService, IStateService stateService)
{
var currentBuild = deviceActionService.GetBuildNumber();
var lastBuild = await stateService.GetLastBuildAsync();
if (lastBuild == null || lastBuild != currentBuild)
{
// Updated
var tasks = Task.Run(() => syncService.FullSyncAsync(true));
await stateService.SetLastBuildAsync(currentBuild);
return true;
}
return false;
}
public static async Task SetPreconfiguredSettingsAsync(IDictionary<string, string> configSettings)
{
if (configSettings?.Any() != true)
{
return;
}
foreach (var setting in configSettings)
{
switch (setting.Key)
{
case "baseEnvironmentUrl":
var environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
var settingValue = string.IsNullOrWhiteSpace(setting.Value) ? null : setting.Value;
if (environmentService.BaseUrl != settingValue)
{
await environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
{
Base = settingValue,
Api = environmentService.ApiUrl,
Identity = environmentService.IdentityUrl,
WebVault = environmentService.WebVaultUrl,
Icons = environmentService.IconsUrl
});
}
return;
default:
break;
}
}
}
public static bool SetAlternateMainPage(AppOptions appOptions)
{
if (appOptions != null)
{
if (appOptions.FromAutofillFramework && appOptions.SaveType.HasValue)
{
Application.Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: appOptions));
return true;
}
if (appOptions.Uri != null
||
appOptions.OtpData != null)
{
Application.Current.MainPage = new NavigationPage(new CipherSelectionPage(appOptions));
return true;
}
if (appOptions.CreateSend != null)
{
Application.Current.MainPage = new NavigationPage(new SendAddEditPage(appOptions));
return true;
}
}
return false;
}
public static async Task<PreviousPageInfo> ClearPreviousPage()
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var previousPage = await stateService.GetPreviousPageInfoAsync();
if (previousPage != null)
{
await stateService.SetPreviousPageInfoAsync(null);
}
return previousPage;
}
public static async Task<int> IncrementInvalidUnlockAttemptsAsync()
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var invalidUnlockAttempts = await stateService.GetInvalidUnlockAttemptsAsync();
invalidUnlockAttempts++;
await stateService.SetInvalidUnlockAttemptsAsync(invalidUnlockAttempts);
return invalidUnlockAttempts;
}
public static async Task ResetInvalidUnlockAttemptsAsync()
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
await stateService.SetInvalidUnlockAttemptsAsync(null);
}
public static async Task<bool> IsVaultTimeoutImmediateAsync()
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var vaultTimeoutMinutes = await stateService.GetVaultTimeoutAsync();
if (vaultTimeoutMinutes.GetValueOrDefault(-1) == 0)
{
return true;
}
return false;
}
public static string EncodeDataParameter(object obj)
{
string EncodeMultibyte(Match match)
{
return Convert.ToChar(Convert.ToUInt32($"0x{match.Groups[1].Value}", 16)).ToString();
}
var escaped = Uri.EscapeDataString(JsonConvert.SerializeObject(obj));
var multiByteEscaped = Regex.Replace(escaped, "%([0-9A-F]{2})", EncodeMultibyte);
return WebUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(multiByteEscaped)));
}
public static async Task LogOutAsync(string userId, bool userInitiated = false)
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
var isActiveAccount = await stateService.IsActiveAccountAsync(userId);
var isAccountRemoval = await vaultTimeoutService.IsLoggedOutByTimeoutAsync(userId) ||
await vaultTimeoutService.ShouldLogOutByTimeoutAsync(userId);
if (userId == null)
{
userId = await stateService.GetActiveUserIdAsync();
}
await stateService.LogoutAccountAsync(userId, userInitiated);
if (isActiveAccount)
{
await ClearServiceCacheAsync();
}
if (!userInitiated)
{
return;
}
// check if we switched active accounts automatically
if (isActiveAccount && await stateService.GetActiveUserIdAsync() != null)
{
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
messagingService.Send("switchedAccount");
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
platformUtilsService.ShowToast("info", null, AppResources.AccountSwitchedAutomatically);
return;
}
// check if we logged out a non-active account
if (!isActiveAccount)
{
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
if (isAccountRemoval)
{
platformUtilsService.ShowToast("info", null, AppResources.AccountRemovedSuccessfully);
return;
}
platformUtilsService.ShowToast("info", null, AppResources.AccountLoggedOutSuccessfully);
}
}
public static async Task OnAccountSwitchAsync()
{
var environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
await environmentService.SetUrlsFromStorageAsync();
await ClearServiceCacheAsync();
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
await deviceActionService.OnAccountSwitchCompleteAsync();
}
public static async Task ClearServiceCacheAsync()
{
var tokenService = ServiceContainer.Resolve<ITokenService>("tokenService");
var cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
var settingsService = ServiceContainer.Resolve<ISettingsService>("settingsService");
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
var folderService = ServiceContainer.Resolve<IFolderService>("folderService");
var collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
var sendService = ServiceContainer.Resolve<ISendService>("sendService");
var passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
"passwordGenerationService");
var fileService = ServiceContainer.Resolve<IFileService>();
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
var searchService = ServiceContainer.Resolve<ISearchService>("searchService");
var usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>(
"usernameGenerationService");
await Task.WhenAll(
cipherService.ClearCacheAsync(),
fileService.ClearCacheAsync());
tokenService.ClearCache();
cryptoService.ClearCache();
settingsService.ClearCache();
folderService.ClearCache();
collectionService.ClearCache();
sendService.ClearCache();
passwordGenerationService.ClearCache();
policyService.ClearCache();
searchService.ClearIndex();
usernameGenerationService.ClearCache();
}
}
}

View File

@@ -0,0 +1,26 @@
using Bit.App.Abstractions;
using Bit.App.Lists.ItemViewModels.CustomFields;
using Bit.App.Services;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
namespace Bit.App.Utilities
{
public interface IAppSetup
{
void InitializeServicesLastChance();
}
public class AppSetup : IAppSetup
{
public void InitializeServicesLastChance()
{
var i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
// TODO: This could be further improved by Lazy Registration since it may not be needed at all
ServiceContainer.Register<ICustomFieldItemFactory>("customFieldItemFactory", new CustomFieldItemFactory(i18nService, eventService));
ServiceContainer.Register<IDeepLinkContext>(new DeepLinkContext(ServiceContainer.Resolve<IMessagingService>()));
}
}
}

View File

@@ -0,0 +1,70 @@
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
namespace Bit.App.Utilities
{
// TODO: [MAUI-Migration] DELETE WHEN MIGRATION IS DONE
/// <summary>
/// Wrapper of <see cref="AsyncRelayCommand"/> just to ease with the MAUI migration process.
/// After the process is done, remove this and use AsyncRelayCommand directly
/// </summary>
public class AsyncCommand : ICommand
{
readonly AsyncRelayCommand _relayCommand;
public AsyncCommand(Func<Task> execute, Func<bool> canExecute = null, Action<Exception> onException = null, bool allowsMultipleExecutions = true)
{
async Task doAsync()
{
try
{
await execute?.Invoke();
}
catch (Exception ex)
{
onException?.Invoke(ex);
}
}
_relayCommand = new AsyncRelayCommand(doAsync, canExecute, allowsMultipleExecutions ? AsyncRelayCommandOptions.AllowConcurrentExecutions : AsyncRelayCommandOptions.None);
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) => _relayCommand.CanExecute(parameter);
public void Execute(object parameter) => _relayCommand.Execute(parameter);
public void RaiseCanExecuteChanged() => _relayCommand.NotifyCanExecuteChanged();
}
/// Wrapper of <see cref="AsyncRelayCommand"/> just to ease with the MAUI migration process.
/// After the process is done, remove this and use AsyncRelayCommand directly
/// </summary>
public class AsyncCommand<T> : ICommand
{
readonly AsyncRelayCommand<T> _relayCommand;
public AsyncCommand(Func<T, Task> execute, Predicate<T?> canExecute = null, Action<Exception> onException = null, bool allowsMultipleExecutions = true)
{
async Task doAsync(T foo)
{
try
{
await execute?.Invoke(foo);
}
catch (Exception ex)
{
onException?.Invoke(ex);
}
}
_relayCommand = new AsyncRelayCommand<T>(doAsync, canExecute, allowsMultipleExecutions ? AsyncRelayCommandOptions.AllowConcurrentExecutions : AsyncRelayCommandOptions.None);
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) => _relayCommand.CanExecute(parameter);
public void Execute(object parameter) => _relayCommand.Execute(parameter);
public void RaiseCanExecuteChanged() => _relayCommand.NotifyCanExecuteChanged();
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Globalization;
namespace Bit.App.Utilities.Automation
{
public static class AutomationIdsHelper
{
public static string ToEnglishTitleCase(string name)
{
return new CultureInfo("en-US", false)
.TextInfo
.ToTitleCase(name)
.Replace(" ", String.Empty)
.Replace("-", String.Empty);
}
public static string AddSuffixFor(string text, SuffixType type)
{
return $"{text}{Enum.GetName(typeof(SuffixType), type)}";
}
}
}

View File

@@ -0,0 +1,13 @@
namespace Bit.App.Utilities.Automation
{
public enum SuffixType
{
Cell,
SettingValue,
Header,
ListGroup,
ListItem,
Filter
}
}

View File

@@ -0,0 +1,25 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class BooleanToBoxRowInputPaddingConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType == typeof(Thickness))
{
return ((bool?)value).GetValueOrDefault() ? new Thickness(0, 10, 0, 0) : new Thickness(0, 10);
}
throw new InvalidOperationException("The target must be a thickness.");
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class ColoredPasswordConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType != typeof(string))
{
throw new InvalidOperationException("The target must be a string.");
}
if (value == null)
{
return string.Empty;
}
return GeneratedValueFormatter.Format((string)value);
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Web;
using Bit.Core.Models.Domain;
using Bit.Core.Services;
using Newtonsoft.Json;
using Color = Microsoft.Maui.Graphics.Color;
namespace Bit.Core.Utilities
{
@@ -268,7 +269,7 @@ namespace Bit.Core.Utilities
{
if (new ColorConverter().ConvertFromString(hexColor) is Color bgColor)
{
var luminance = bgColor.R * 0.299 + bgColor.G * 0.587 + bgColor.B * 0.114;
var luminance = bgColor.Red * 0.299 + bgColor.Green * 0.587 + bgColor.Blue * 0.114;
return luminance > threshold ? "#ff000000" : "#ffffffff";
}

View File

@@ -0,0 +1,41 @@
using System;
using Bit.App.Abstractions;
using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class DateTimeConverter : IValueConverter
{
private readonly ILocalizeService _localizeService;
public DateTimeConverter()
{
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
}
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType != typeof(string))
{
throw new InvalidOperationException("The target must be a string.");
}
if (value == null)
{
return string.Empty;
}
var d = ((DateTime)value).ToLocalTime();
return string.Format("{0} {1}",
_localizeService.GetLocaleShortDate(d),
_localizeService.GetLocaleShortTime(d));
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Linq;
using System.Reflection;
using Bit.Core.Resources.Localization;
using Bit.Core.Attributes;
using CommunityToolkit.Maui.Converters;
namespace Bit.App.Utilities
{
public static class EnumHelper
{
public static string GetLocalizedValue<T>(T value)
{
return GetLocalizedValue(value, typeof(T));
}
public static string GetLocalizedValue(object value, Type type)
{
if (!type.GetTypeInfo().IsEnum)
{
throw new ArgumentException("type should be an enum");
}
var valueMemberInfo = type.GetMember(value.ToString())[0];
if (valueMemberInfo.GetCustomAttribute<LocalizableEnumAttribute>() is LocalizableEnumAttribute attr)
{
return AppResources.ResourceManager.GetString(attr.Key);
}
return value.ToString();
}
}
}

View File

@@ -0,0 +1,126 @@
using System;
using System.Web;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
/**
* Helper class to format a password/username with numeric encoding to separate
* normal text from numbers and special characters.
*/
class GeneratedValueFormatter
{
/**
* This enum is used for the state machine when building the colorized
* password/username string.
*/
private enum CharType
{
None,
Normal,
Number,
Special
}
public static string Format(string generatedValue)
{
if (generatedValue == null)
{
return string.Empty;
}
// First two digits of returned hex code contains the alpha,
// which is not supported in HTML color, so we need to cut those out.
var normalColor = $"<span style=\"color:#{ThemeManager.GetResourceColor("TextColor").ToHex().Substring(3)}\">";
var numberColor = $"<span style=\"color:#{ThemeManager.GetResourceColor("PasswordNumberColor").ToHex().Substring(3)}\">";
var specialColor = $"<span style=\"color:#{ThemeManager.GetResourceColor("PasswordSpecialColor").ToHex().Substring(3)}\">";
var result = string.Empty;
// iOS won't hide the zero-width space char without these div attrs, but Android won't respect
// display:inline-block and adds a newline after the password/username. Hence, only iOS gets the div.
// 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)
{
result += "<div style=\"display:inline-block; align-items:center; justify-content:center; text-align:center; word-break:break-all; white-space:pre-wrap; min-width:0\">";
}
// Start with an otherwise uncovered case so we will definitely enter the "something changed"
// state.
var currentType = CharType.None;
foreach (var c in generatedValue)
{
// First, identify what the current char is.
CharType charType;
if (char.IsLetter(c))
{
charType = CharType.Normal;
}
else if (char.IsDigit(c))
{
charType = CharType.Number;
}
else
{
charType = CharType.Special;
}
// If the char type changed, build a new span to append the text to.
if (charType != currentType)
{
// Close off previous span.
if (currentType != CharType.None)
{
result += "</span>";
}
currentType = charType;
// Switch the color if it is not a normal text. Otherwise leave the
// default value.
switch (currentType)
{
// Apply color style to span.
case CharType.Normal:
result += normalColor;
break;
case CharType.Number:
result += numberColor;
break;
case CharType.Special:
result += specialColor;
break;
}
}
if (currentType == CharType.Special)
{
result += HttpUtility.HtmlEncode(c);
}
else
{
result += c;
}
// Add zero-width space after every char so per-char wrapping works consistently
result += "&#8203;";
}
// Close off last span.
if (currentType != CharType.None)
{
result += "</span>";
}
// Close off iOS div
// 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)
{
result += "</div>";
}
return result;
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
[ContentProperty("Id")]
public class I18nExtension : IMarkupExtension
{
private II18nService _i18nService;
public I18nExtension()
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
}
public string Id { get; set; }
public string P1 { get; set; }
public string P2 { get; set; }
public string P3 { get; set; }
public bool Header { get; set; }
public object ProvideValue(IServiceProvider serviceProvider)
{
var val = _i18nService.T(Id, P1, P2, P3);
/*
if (Header && Device.RuntimePlatform == Device.iOS)
{
return val.ToUpper();
}
*/
return val;
}
}
}

View File

@@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Bit.App.Utilities
{
public interface IPasswordPromptable
{
Task<bool> PromptPasswordAsync();
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Globalization;
using Bit.Core.Models.View;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class IconGlyphConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is CipherView cipher)
{
return cipher.GetIcon();
}
if (value is bool boolVal
&&
parameter is BooleanGlyphType boolGlyphType)
{
return IconGlyphExtensions.GetBooleanIconGlyph(boolVal, boolGlyphType);
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,62 @@
using Bit.Core;
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.App.Utilities
{
public static class IconGlyphExtensions
{
public static string GetIcon(this CipherView cipher)
{
switch (cipher.Type)
{
case CipherType.Login:
return GetLoginIconGlyph(cipher);
case CipherType.SecureNote:
return BitwardenIcons.StickyNote;
case CipherType.Card:
return BitwardenIcons.CreditCard;
case CipherType.Identity:
return BitwardenIcons.IdCard;
}
return null;
}
static string GetLoginIconGlyph(CipherView cipher)
{
var icon = BitwardenIcons.Globe;
if (cipher.Login.Uri != null)
{
var hostnameUri = cipher.Login.Uri;
if (hostnameUri.StartsWith(Constants.AndroidAppProtocol))
{
icon = BitwardenIcons.Android;
}
else if (hostnameUri.StartsWith(Constants.iOSAppProtocol))
{
icon = BitwardenIcons.Apple;
}
}
return icon;
}
public static string GetBooleanIconGlyph(bool value, BooleanGlyphType type)
{
switch (type)
{
case BooleanGlyphType.Checkbox:
return value ? BitwardenIcons.CheckSquare : BitwardenIcons.Square;
case BooleanGlyphType.Eye:
return value ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
default:
return "";
}
}
}
public enum BooleanGlyphType
{
Checkbox,
Eye
}
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Globalization;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class IconImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var cipher = value as CipherView;
return IconImageHelper.GetIconImage(cipher);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public static class IconImageHelper
{
public static string GetIconImage(CipherView cipher)
{
if (cipher.Type != CipherType.Login)
{
return null;
}
return GetLoginIconImage(cipher);
}
public static string GetLoginIconImage(CipherView cipher)
{
string image = null;
if (cipher.Login.HasUris)
{
foreach (var uri in cipher.Login.Uris)
{
var hostnameUri = uri.Uri;
var isWebsite = false;
if (!hostnameUri.Contains("."))
{
continue;
}
if (!hostnameUri.Contains("://"))
{
hostnameUri = string.Concat("http://", hostnameUri);
}
isWebsite = hostnameUri.StartsWith("http");
if (isWebsite)
{
image = GetIconUrl(hostnameUri);
break;
}
}
}
return image;
}
private static string GetIconUrl(string hostnameUri)
{
IEnvironmentService _environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
var hostname = CoreHelpers.GetHostname(hostnameUri);
var iconsUrl = _environmentService.IconsUrl;
if (string.IsNullOrWhiteSpace(iconsUrl))
{
if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl))
{
iconsUrl = string.Format("{0}/icons", _environmentService.BaseUrl);
}
else
{
iconsUrl = "https://icons.bitwarden.net";
}
}
return string.Format("{0}/{1}/icon.png", iconsUrl, hostname);
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class InverseBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType == typeof(bool))
{
return !((bool?)value).GetValueOrDefault();
}
throw new InvalidOperationException("The target must be a boolean.");
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class IsNotNullConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType == typeof(bool))
{
return value != null;
}
throw new InvalidOperationException("The target must be a boolean.");
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class IsNullConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType == typeof(bool))
{
return value == null;
}
throw new InvalidOperationException("The target must be a boolean.");
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
/// <summary>
/// It localizes an enum value by using the <see cref="Core.Attributes.LocalizableEnumAttribute"/>
/// </summary>
public class LocalizableEnumConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
return EnumHelper.GetLocalizedValue(value, value.GetType());
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
#nullable enable
namespace Bit.Core.Utilities
{
// TODO: [MAUI-Migration] CHECK WHEN MIGRATION IS DONE
/// <summary>
/// Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ObservableRangeCollection<T> : ObservableCollection<T>
{
/// <summary>
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class.
/// </summary>
public ObservableRangeCollection()
: base()
{
}
/// <summary>
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class that contains elements copied from the specified collection.
/// </summary>
/// <param name="collection">collection: The collection from which the elements are copied.</param>
/// <exception cref="System.ArgumentNullException">The collection parameter cannot be null.</exception>
public ObservableRangeCollection(IEnumerable<T> collection)
: base(collection)
{
}
/// <summary>
/// Adds the elements of the specified collection to the end of the ObservableCollection(Of T).
/// </summary>
public void AddRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Add)
{
if (notificationMode != NotifyCollectionChangedAction.Add && notificationMode != NotifyCollectionChangedAction.Reset)
throw new ArgumentException("Mode must be either Add or Reset for AddRange.", nameof(notificationMode));
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
var startIndex = Count;
var itemsAdded = AddArrangeCore(collection);
if (!itemsAdded)
return;
if (notificationMode == NotifyCollectionChangedAction.Reset)
{
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
return;
}
var changedItems = collection is List<T>
? (List<T>)collection
: new List<T>(collection);
RaiseChangeNotificationEvents(
action: NotifyCollectionChangedAction.Add,
changedItems: changedItems,
startingIndex: startIndex);
}
/// <summary>
/// Removes the first occurence of each item in the specified collection from ObservableCollection(Of T). NOTE: with notificationMode = Remove, removed items starting index is not set because items are not guaranteed to be consecutive.
/// </summary>
public void RemoveRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Reset)
{
if (notificationMode != NotifyCollectionChangedAction.Remove && notificationMode != NotifyCollectionChangedAction.Reset)
throw new ArgumentException("Mode must be either Remove or Reset for RemoveRange.", nameof(notificationMode));
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
if (notificationMode == NotifyCollectionChangedAction.Reset)
{
var raiseEvents = false;
foreach (var item in collection)
{
Items.Remove(item);
raiseEvents = true;
}
if (raiseEvents)
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
return;
}
var changedItems = new List<T>(collection);
for (var i = 0; i < changedItems.Count; i++)
{
if (!Items.Remove(changedItems[i]))
{
changedItems.RemoveAt(i); // Can't use a foreach because changedItems is intended to be (carefully) modified
i--;
}
}
if (changedItems.Count == 0)
return;
RaiseChangeNotificationEvents(
action: NotifyCollectionChangedAction.Remove,
changedItems: changedItems);
}
/// <summary>
/// Clears the current collection and replaces it with the specified item.
/// </summary>
public void Replace(T item) => ReplaceRange(new T[] { item });
/// <summary>
/// Clears the current collection and replaces it with the specified collection.
/// </summary>
public void ReplaceRange(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
var previouslyEmpty = Items.Count == 0;
Items.Clear();
AddArrangeCore(collection);
var currentlyEmpty = Items.Count == 0;
if (previouslyEmpty && currentlyEmpty)
return;
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
}
bool AddArrangeCore(IEnumerable<T> collection)
{
var itemAdded = false;
foreach (var item in collection)
{
Items.Add(item);
itemAdded = true;
}
return itemAdded;
}
void RaiseChangeNotificationEvents(NotifyCollectionChangedAction action, List<T>? changedItems = null, int startingIndex = -1)
{
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
if (changedItems == null)
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action));
else
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, changedItems: changedItems, startingIndex: startingIndex));
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public static class PageExtensions
{
public static async Task TraverseNavigationRecursivelyAsync(this Page page, Func<Page, Task> actionOnPage)
{
if (page?.Navigation?.ModalStack != null)
{
foreach (var p in page.Navigation.ModalStack)
{
if (p is NavigationPage modalNavPage)
{
await TraverseNavigationStackRecursivelyAsync(modalNavPage.CurrentPage, actionOnPage);
}
else
{
await TraverseNavigationStackRecursivelyAsync(p, actionOnPage);
}
}
}
await TraverseNavigationStackRecursivelyAsync(page, actionOnPage);
}
private static async Task TraverseNavigationStackRecursivelyAsync(this Page page, Func<Page, Task> actionOnPage)
{
if (page is MultiPage<Page> multiPage && multiPage.Children != null)
{
foreach (var p in multiPage.Children)
{
await TraverseNavigationStackRecursivelyAsync(p, actionOnPage);
}
}
if (page is NavigationPage && page.Navigation != null)
{
if (page.Navigation.NavigationStack != null)
{
foreach (var p in page.Navigation.NavigationStack)
{
await TraverseNavigationStackRecursivelyAsync(p, actionOnPage);
}
}
}
await actionOnPage(page);
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using static Microsoft.Maui.ApplicationModel.Permissions;
using Microsoft.Maui.ApplicationModel;
namespace Bit.App.Utilities
{
public static class PermissionManager
{
public static async Task<PermissionStatus> CheckAndRequestPermissionAsync<T>(T permission)
where T : BasePermission
{
var status = await permission.CheckStatusAsync();
if (status != PermissionStatus.Granted)
{
status = await permission.RequestAsync();
}
return status;
}
}
}

View File

@@ -0,0 +1,23 @@
namespace Bit.App.Utilities
{
public static class ProgressBarExtensions
{
public static BindableProperty AnimatedProgressProperty =
BindableProperty.CreateAttached("AnimatedProgress",
typeof(double),
typeof(ProgressBar),
0.0d,
BindingMode.OneWay,
propertyChanged: (b, o, n) => ProgressBarProgressChanged((ProgressBar)b, (double)n));
public static double GetAnimatedProgress(BindableObject target) => (double)target.GetValue(AnimatedProgressProperty);
public static void SetAnimatedProgress(BindableObject target, double value) => target.SetValue(AnimatedProgressProperty, value);
private static void ProgressBarProgressChanged(ProgressBar progressBar, double progress)
{
Microsoft.Maui.Controls.ViewExtensions.CancelAnimations(progressBar);
progressBar.ProgressTo(progress, 500, Easing.SinIn);
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
namespace Bit.App.Utilities.Prompts
{
public class ValidatablePromptConfig
{
public string Title { get; set; }
public string Subtitle { get; set; }
public string Text { get; set; }
public Func<string, string> ValidateText { get; set; }
public string ValueSubInfo { get; set; }
public string OkButtonText { get; set; }
public string CancelButtonText { get; set; }
public string ThirdButtonText { get; set; }
public bool NumericKeyboard { get; set; }
}
public struct ValidatablePromptResponse
{
public ValidatablePromptResponse(string text, bool executeThirdAction)
{
Text = text;
ExecuteThirdAction = executeThirdAction;
}
public string Text { get; set; }
public bool ExecuteThirdAction { get; set; }
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Globalization;
using Bit.Core;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class SendIconGlyphConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var send = value as SendView;
if (send == null)
{
return null;
}
string icon = null;
switch (send.Type)
{
case SendType.Text:
icon = BitwardenIcons.FileText;
break;
case SendType.File:
icon = BitwardenIcons.File;
break;
}
return icon;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class StringHasValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType == typeof(bool))
{
if (value == null)
{
return false;
}
if (value.GetType() == typeof(string))
{
return !string.IsNullOrWhiteSpace((string)value);
}
}
throw new InvalidOperationException("The value must be a string with a boolean target.");
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,201 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Styles;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public static class ThemeManager
{
public const string UPDATED_THEME_MESSAGE_KEY = "updatedTheme";
public static bool UsingLightTheme = true;
public static Func<ResourceDictionary> Resources = () => null;
public static bool IsThemeDirty = false;
public const string Light = "light";
public const string Dark = "dark";
public const string Black = "black";
public const string Nord = "nord";
public const string SolarizedDark = "solarizeddark";
public static void SetThemeStyle(string name, string autoDarkName, ResourceDictionary resources)
{
try
{
Resources = () => resources;
var newTheme = NeedsThemeUpdate(name, autoDarkName, resources);
if (newTheme is null)
{
return;
}
var currentTheme = resources.MergedDictionaries.FirstOrDefault(md => md is IThemeResourceDictionary);
if (currentTheme != null)
{
resources.MergedDictionaries.Remove(currentTheme);
resources.MergedDictionaries.Add(newTheme);
UsingLightTheme = newTheme is Light;
IsThemeDirty = true;
return;
}
// Reset styles
resources.Clear();
resources.MergedDictionaries.Clear();
// Variables
resources.MergedDictionaries.Add(new Variables());
// Theme
resources.MergedDictionaries.Add(newTheme);
UsingLightTheme = newTheme is Light;
// Base styles
resources.MergedDictionaries.Add(new Base());
resources.MergedDictionaries.Add(new ControlTemplates());
// Platform styles
// 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)
{
resources.MergedDictionaries.Add(new Styles.Android());
}
else if (Device.RuntimePlatform == Device.iOS)
{
resources.MergedDictionaries.Add(new iOS());
}
}
catch (InvalidOperationException ioex) when (ioex.Message != null && ioex.Message.StartsWith("Collection was modified"))
{
// https://github.com/bitwarden/mobile/issues/1689 There are certain scenarios where this might cause "collection was modified; enumeration operation may not execute"
// the way I found to prevent this for now was to catch the exception here and move on.
// Because on the screens that I found it to happen, the screen is being closed while trying to apply the resources
// so we shouldn't be introducing any issues.
// TODO: Maybe something like this https://github.com/matteobortolazzo/HtmlLabelPlugin/pull/113 can be implemented to avoid this
// on html labels.
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
}
static ResourceDictionary CheckAndGetThemeForMergedDictionaries(Type themeType, ResourceDictionary resources)
{
return resources.MergedDictionaries.Any(rd => rd.GetType() == themeType)
? null
: Activator.CreateInstance(themeType) as ResourceDictionary;
}
static ResourceDictionary NeedsThemeUpdate(string themeName, string autoDarkThemeName, ResourceDictionary resources)
{
switch (themeName)
{
case Dark:
return CheckAndGetThemeForMergedDictionaries(typeof(Dark), resources);
case Black:
return CheckAndGetThemeForMergedDictionaries(typeof(Black), resources);
case Nord:
return CheckAndGetThemeForMergedDictionaries(typeof(Nord), resources);
case Light:
return CheckAndGetThemeForMergedDictionaries(typeof(Light), resources);
case SolarizedDark:
return CheckAndGetThemeForMergedDictionaries(typeof(SolarizedDark), resources);
default:
if (OsDarkModeEnabled())
{
switch (autoDarkThemeName)
{
case Black:
return CheckAndGetThemeForMergedDictionaries(typeof(Black), resources);
case Nord:
return CheckAndGetThemeForMergedDictionaries(typeof(Nord), resources);
case SolarizedDark:
return CheckAndGetThemeForMergedDictionaries(typeof(SolarizedDark), resources);
default:
return CheckAndGetThemeForMergedDictionaries(typeof(Dark), resources);
}
}
return CheckAndGetThemeForMergedDictionaries(typeof(Light), resources);
}
}
public static void SetTheme(ResourceDictionary resources)
{
SetThemeStyle(GetTheme(), GetAutoDarkTheme(), resources);
}
public static string GetTheme()
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
return stateService.GetThemeAsync().GetAwaiter().GetResult();
}
public static string GetAutoDarkTheme()
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
return stateService.GetAutoDarkThemeAsync().GetAwaiter().GetResult();
}
public static bool OsDarkModeEnabled()
{
if (Application.Current == null)
{
// called from iOS extension
var app = new App(new AppOptions { IosExtension = true });
return app.RequestedTheme == AppTheme.Dark;
}
return Application.Current.RequestedTheme == AppTheme.Dark;
}
public static void ApplyResourcesTo(VisualElement element)
{
foreach (var resourceDict in Resources().MergedDictionaries)
{
element.Resources.Add(resourceDict);
}
}
public static Color GetResourceColor(string color)
{
return (Color)Resources()[color];
}
public static async Task UpdateThemeOnPagesAsync()
{
try
{
if (IsThemeDirty)
{
IsThemeDirty = false;
await Application.Current.MainPage.TraverseNavigationRecursivelyAsync(async p =>
{
if (p is IThemeDirtablePage themeDirtablePage)
{
themeDirtablePage.IsThemeDirty = true;
if (p.IsVisible)
{
await themeDirtablePage.UpdateOnThemeChanged();
}
}
});
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class TimerTask
{
private readonly ILogger _logger;
private readonly Action _action;
private readonly Func<Task> _actionTask;
private readonly CancellationTokenSource _cancellationTokenSource;
public TimerTask(ILogger logger, Action action, CancellationTokenSource cancellationTokenSource)
{
_logger = logger;
_action = action ?? throw new ArgumentNullException(nameof(action));
_cancellationTokenSource = cancellationTokenSource;
}
public TimerTask(ILogger logger, Func<Task> actionTask, CancellationTokenSource cancellationTokenSource)
{
_logger = logger;
_actionTask = actionTask ?? throw new ArgumentNullException(nameof(actionTask));
_cancellationTokenSource = cancellationTokenSource;
}
public Task RunPeriodic(TimeSpan? interval = null)
{
interval = interval ?? TimeSpan.FromSeconds(1);
return Task.Run(async () =>
{
try
{
while (!_cancellationTokenSource.IsCancellationRequested)
{
await Device.InvokeOnMainThreadAsync(async () =>
{
if (!_cancellationTokenSource.IsCancellationRequested)
{
try
{
if (_action != null)
{
_action();
}
else if (_actionTask != null)
{
await _actionTask();
}
}
catch (Exception ex)
{
_cancellationTokenSource?.Cancel();
_logger?.Exception(ex);
}
}
});
await Task.Delay(interval.Value, _cancellationTokenSource.Token);
}
}
catch (TaskCanceledException) { }
catch (Exception ex)
{
_logger?.Exception(ex);
}
}, _cancellationTokenSource.Token);
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Threading.Tasks;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
namespace Bit.App.Utilities
{
public class TotpHelper
{
private ITotpService _totpService;
private CipherView _cipher;
public TotpHelper(CipherView cipher)
{
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
_cipher = cipher;
Interval = _totpService.GetTimeInterval(cipher?.Login?.Totp);
}
public string TotpSec { get; private set; }
public string TotpCodeFormatted { get; private set; }
public double Progress { get; private set; }
public double Interval { get; private set; } = Constants.TotpDefaultTimer;
public async Task GenerateNewTotpValues()
{
var epoc = CoreHelpers.EpocUtcNow() / 1000;
var mod = epoc % Interval;
var totpSec = Interval - mod;
TotpSec = totpSec.ToString();
Progress = totpSec * 100 / Interval;
if (mod == 0 || string.IsNullOrEmpty(TotpCodeFormatted))
{
TotpCodeFormatted = await TotpUpdateCodeAsync();
}
}
private async Task<string> TotpUpdateCodeAsync()
{
var totpCode = await _totpService.GetCodeAsync(_cipher?.Login?.Totp);
if (totpCode == null)
{
return null;
}
if (totpCode.Length <= 4)
{
return totpCode;
}
var half = (int)Math.Floor(totpCode.Length / 2M);
return string.Format("{0} {1}", totpCode.Substring(0, half),
totpCode.Substring(half));
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Globalization;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public class UpperCaseConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(string))
{
throw new InvalidOperationException("The target must be a string.");
}
if (value == null)
{
return string.Empty;
}
return parameter.ToString().ToUpper();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Pages;
using Bit.App.Pages.Accounts;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Utilities
{
public interface IVerificationActionsFlowHelper
{
IVerificationActionsFlowHelper Configure(VerificationFlowAction action,
IActionFlowParmeters parameters = null,
string verificatioCodeMainActionText = null,
bool isVerificationCodeMainActionStyleDanger = false);
IActionFlowParmeters GetParameters();
Task ValidateAndExecuteAsync();
Task ExecuteAsync(IActionFlowParmeters parameters);
}
public interface IActionFlowParmeters
{
VerificationType VerificationType { get; set; }
string Secret { get; set; }
}
public class DefaultActionFlowParameters : IActionFlowParmeters
{
public VerificationType VerificationType { get; set; }
public string Secret { get; set; }
}
public interface IActionFlowExecutioner
{
Task Execute(IActionFlowParmeters parameters);
}
public enum VerificationFlowAction
{
ExportVault,
DeleteAccount
}
/// <summary>
/// Verifies and execute actions on the corresponding order.
///
/// Use: From the caller
/// - Configure it on <see cref="Configure(VerificationFlowAction, IActionFlowParmeters)"/>
/// - Call <see cref="ValidateAndExecuteAsync"/>
///
/// For every <see cref="VerificationFlowAction"/> we need an implementation of <see cref="IActionFlowExecutioner"/>
/// and it to be configured in the inner dictionary.
/// Also, inherit from <see cref="DefaultActionFlowParameters"/> if more custom parameters are needed for the executioner.
/// </summary>
public class VerificationActionsFlowHelper : IVerificationActionsFlowHelper
{
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly ICryptoService _cryptoService;
private readonly IUserVerificationService _userVerificationService;
private VerificationFlowAction? _action;
private IActionFlowParmeters _parameters;
private string _verificationCodeMainActionText;
private bool _isVerificationCodeMainActionStyleDanger;
private readonly Dictionary<VerificationFlowAction, IActionFlowExecutioner> _actionExecutionerDictionary = new Dictionary<VerificationFlowAction, IActionFlowExecutioner>();
public VerificationActionsFlowHelper(
IPasswordRepromptService passwordRepromptService,
ICryptoService cryptoService,
IUserVerificationService userVerificationService)
{
_passwordRepromptService = passwordRepromptService;
_cryptoService = cryptoService;
_userVerificationService = userVerificationService;
_actionExecutionerDictionary.Add(VerificationFlowAction.DeleteAccount, ServiceContainer.Resolve<IDeleteAccountActionFlowExecutioner>("deleteAccountActionFlowExecutioner"));
}
public IVerificationActionsFlowHelper Configure(VerificationFlowAction action,
IActionFlowParmeters parameters = null,
string verificationCodeMainActionText = null,
bool isVerificationCodeMainActionStyleDanger = false)
{
_action = action;
_parameters = parameters;
_verificationCodeMainActionText = verificationCodeMainActionText;
_isVerificationCodeMainActionStyleDanger = isVerificationCodeMainActionStyleDanger;
return this;
}
public IActionFlowParmeters GetParameters()
{
if (_parameters is null)
{
_parameters = new DefaultActionFlowParameters();
}
return _parameters;
}
public async Task ValidateAndExecuteAsync()
{
var verificationType = await _userVerificationService.HasMasterPasswordAsync(true)
? VerificationType.MasterPassword
: VerificationType.OTP;
switch (verificationType)
{
case VerificationType.MasterPassword:
var (password, valid) = await _passwordRepromptService.ShowPasswordPromptAndGetItAsync();
if (!valid)
{
return;
}
var parameters = GetParameters();
parameters.Secret = await _cryptoService.HashMasterKeyAsync(password, null);
parameters.VerificationType = VerificationType.MasterPassword;
await ExecuteAsync(parameters);
break;
case VerificationType.OTP:
await Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(
new VerificationCodePage(_verificationCodeMainActionText, _isVerificationCodeMainActionStyleDanger)));
break;
default:
throw new NotImplementedException($"There is no implementation for {verificationType}");
}
}
/// <summary>
/// Executes the action with the given parameters after we have gotten the verification secret
/// </summary>
public async Task ExecuteAsync(IActionFlowParmeters parameters)
{
if (!_action.HasValue)
{
// this should never happen
throw new InvalidOperationException("A problem occurred while getting the action value after validation");
}
if (!_actionExecutionerDictionary.TryGetValue(_action.Value, out var executioner))
{
throw new InvalidOperationException($"There is no executioner for {_action}");
}
await executioner.Execute(GetParameters());
}
}
}