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:
43
src/Core/Utilities/A11yExtensions.cs
Normal file
43
src/Core/Utilities/A11yExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
268
src/Core/Utilities/AccountManagement/AccountsManager.cs
Normal file
268
src/Core/Utilities/AccountManagement/AccountsManager.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Core/Utilities/AccountManagement/LockNavigationParams.cs
Normal file
14
src/Core/Utilities/AccountManagement/LockNavigationParams.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
592
src/Core/Utilities/AppHelpers.cs
Normal file
592
src/Core/Utilities/AppHelpers.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/Core/Utilities/AppSetup.cs
Normal file
26
src/Core/Utilities/AppSetup.cs
Normal 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>()));
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/Core/Utilities/AsyncCommand.cs
Normal file
70
src/Core/Utilities/AsyncCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
23
src/Core/Utilities/Automation/AutomationIdsHelper.cs
Normal file
23
src/Core/Utilities/Automation/AutomationIdsHelper.cs
Normal 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)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
src/Core/Utilities/Automation/SuffixType.cs
Normal file
13
src/Core/Utilities/Automation/SuffixType.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Bit.App.Utilities.Automation
|
||||
{
|
||||
public enum SuffixType
|
||||
{
|
||||
Cell,
|
||||
SettingValue,
|
||||
Header,
|
||||
ListGroup,
|
||||
ListItem,
|
||||
Filter
|
||||
}
|
||||
}
|
||||
|
||||
25
src/Core/Utilities/BoxRowVsBoxRowInputPaddingConverter.cs
Normal file
25
src/Core/Utilities/BoxRowVsBoxRowInputPaddingConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Core/Utilities/ColoredPasswordConverter.cs
Normal file
29
src/Core/Utilities/ColoredPasswordConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
41
src/Core/Utilities/DateTimeConverter.cs
Normal file
41
src/Core/Utilities/DateTimeConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/Core/Utilities/EnumHelper.cs
Normal file
33
src/Core/Utilities/EnumHelper.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/Core/Utilities/GeneratedValueFormatter.cs
Normal file
126
src/Core/Utilities/GeneratedValueFormatter.cs
Normal 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 += "​";
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/Core/Utilities/I18nExtension.cs
Normal file
38
src/Core/Utilities/I18nExtension.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Core/Utilities/IPasswordPromptable.cs
Normal file
9
src/Core/Utilities/IPasswordPromptable.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.App.Utilities
|
||||
{
|
||||
public interface IPasswordPromptable
|
||||
{
|
||||
Task<bool> PromptPasswordAsync();
|
||||
}
|
||||
}
|
||||
33
src/Core/Utilities/IconGlyphConverter.cs
Normal file
33
src/Core/Utilities/IconGlyphConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/Core/Utilities/IconGlyphExtensions.cs
Normal file
62
src/Core/Utilities/IconGlyphExtensions.cs
Normal 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
|
||||
}
|
||||
}
|
||||
87
src/Core/Utilities/IconImageConverter.cs
Normal file
87
src/Core/Utilities/IconImageConverter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Core/Utilities/InverseBoolConverter.cs
Normal file
25
src/Core/Utilities/InverseBoolConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Core/Utilities/IsNotNullConverter.cs
Normal file
25
src/Core/Utilities/IsNotNullConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Core/Utilities/IsNullConverter.cs
Normal file
25
src/Core/Utilities/IsNullConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Core/Utilities/LocalizableEnumConverter.cs
Normal file
24
src/Core/Utilities/LocalizableEnumConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
168
src/Core/Utilities/ObservableRangeCollection.cs
Normal file
168
src/Core/Utilities/ObservableRangeCollection.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/Core/Utilities/PageExtensions.cs
Normal file
54
src/Core/Utilities/PageExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/Core/Utilities/PermissionManager.cs
Normal file
21
src/Core/Utilities/PermissionManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Core/Utilities/ProgressBarExtensions.cs
Normal file
23
src/Core/Utilities/ProgressBarExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
src/Core/Utilities/Prompts/ValidatablePromptConfig.cs
Normal file
29
src/Core/Utilities/Prompts/ValidatablePromptConfig.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
38
src/Core/Utilities/SendIconGlyphConverter.cs
Normal file
38
src/Core/Utilities/SendIconGlyphConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/Core/Utilities/StringHasValueConverter.cs
Normal file
32
src/Core/Utilities/StringHasValueConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
201
src/Core/Utilities/ThemeManager.cs
Normal file
201
src/Core/Utilities/ThemeManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/Core/Utilities/TimerTask.cs
Normal file
73
src/Core/Utilities/TimerTask.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/Core/Utilities/TotpHelper.cs
Normal file
58
src/Core/Utilities/TotpHelper.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Core/Utilities/UpperCaseConverter.cs
Normal file
30
src/Core/Utilities/UpperCaseConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/Core/Utilities/VerificationActionsFlowHelper.cs
Normal file
158
src/Core/Utilities/VerificationActionsFlowHelper.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user