1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-17 00:33:20 +00:00

[SG-223] Mobile username generator (#2033)

* SG-223 - Changed page title and password title

* SG-223 - Refactored generated field
* Changed position of generated field
* Replaced buttons generate and copy for icons

* SG-223 - Refactor type to passwordType

* SG-223 - Added password or username selector
* Added string for label type selection

* SG-223 - Added logic for different types of username
* Added strings of new types

* [SG-223] - Added UI components for different username types
* Added static strings for new labels
* Added viewmodel properties to support username generation and their respective options

* [SG-223] Added control over type picker visibility

* [SG-223] Refactored username entry on add edit page and added generate icon
* Added GenerateUsername command

* [SG-223] - Implemented service for username generation

* [SG-223] - Added support for username generation for item creation flow
* Implemented cache for username options
* Added exception handling for api calls

* [SG-223] - Remove unused code

* [SG-223] - Added a new display field for username generated and respective command
* Added description label for each type of username
* Changed defautl value of username from string.Empty to -

* [SG-223] - Removed some StackLayouts and refactored some controls

* [SG-223] - Refactored properties name

* [SG-223] - Added visibility toggle icon for api keys of forwarded email username types

* [SG-223] - Refactored nested StackLayouts into grids.

* [SG-223] - Refactor and pr fixing

* [SG-223] - Removed string keys from Resolve
- Added static string to resources

* [SG-223] - Refactored Copy_Clicked as AsyncCommand
- Improved exception handling
- Refactored TypeSelected as GeneratorTypeSelected

* [SG-223] - Renamed PasswordFormatter

* [SG-223] - Refactored VM properties to use UsernameGenerationOptions
* Removed LoadUsernameOptions

* [SG-223] - Refactored added pickers to use SelectedItem instead SelectedIndex
* Deleted PickerIndexToBoolConverter as it isn't needed anymore

* [SG-223] -  Refactored and simplified Grid row and column definitions

* [SG-223] - Refactored Command into async command
* Added exception handling and feedback to the user

* [SG-223] - Refactored GeneratorType picker to use Enum GeneratorType instead of string

* [SG-223] - Changed some resource keys

* [SG-223] - Refactor method name

* [SG-223] - Refactored code and added logs for switch default cases

* [SG-223] - Added flag to control visibility when in edit mode

* [SG-223] - Added suffix Parenthesis to keys to prevent future conflicts

* [SG-223] - Refactored multiple methods into one, GetUsernameFromAsync
* Removed unused Extensions from enums

* [SG-223] - Added exception message

* [SG-223] - Added localizable enum values through LocalizableEnumConverter

* [SG-223] - Fixed space between controls

* [SG-223] - Removed unused code and refactored some variables and methods names

* [SG-223] - Removed unused code and refactored constant name to be more elucidative

* [SG-223] - Removed unused variable
This commit is contained in:
Carlos Gonçalves
2022-08-26 19:32:02 +01:00
committed by GitHub
parent 673ba9f3cc
commit b1fb867b6e
25 changed files with 1471 additions and 100 deletions

View File

@@ -1,10 +1,16 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -13,11 +19,15 @@ namespace Bit.App.Pages
private readonly IPasswordGenerationService _passwordGenerationService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IClipboardService _clipboardService;
private readonly IUsernameGenerationService _usernameGenerationService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private PasswordGenerationOptions _options;
private UsernameGenerationOptions _usernameOptions;
private PasswordGeneratorPolicyOptions _enforcedPolicyOptions;
private string _password;
private bool _isPassword;
private bool _isUsername;
private bool _uppercase;
private bool _lowercase;
private bool _number;
@@ -30,21 +40,70 @@ namespace Bit.App.Pages
private string _wordSeparator;
private bool _capitalize;
private bool _includeNumber;
private int _typeSelectedIndex;
private string _username;
private GeneratorType _generatorTypeSelected;
private int _passwordTypeSelectedIndex;
private bool _doneIniting;
private bool _showTypePicker;
private string _emailWebsite;
private bool _showFirefoxRelayApiAccessToken;
private bool _showAnonAddyApiAccessToken;
private bool _showSimpleLoginApiKey;
private UsernameEmailType _catchAllEmailTypeSelected;
private UsernameEmailType _plusAddressedEmailTypeSelected;
private bool _editMode;
public GeneratorPageViewModel()
{
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
"passwordGenerationService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_clipboardService = ServiceContainer.Resolve<IClipboardService>();
_usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>();
PageTitle = AppResources.PasswordGenerator;
TypeOptions = new List<string> { AppResources.Password, AppResources.Passphrase };
PageTitle = AppResources.Generator;
GeneratorTypeOptions = new List<GeneratorType> {
GeneratorType.Password,
GeneratorType.Username
};
PasswordTypeOptions = new List<string> { AppResources.Password, AppResources.Passphrase };
UsernameTypeOptions = new List<UsernameType> {
UsernameType.PlusAddressedEmail,
UsernameType.CatchAllEmail,
UsernameType.ForwardedEmailAlias,
UsernameType.RandomWord
};
ForwardedEmailServiceTypeOptions = new List<ForwardedEmailServiceType> {
ForwardedEmailServiceType.AnonAddy,
ForwardedEmailServiceType.FirefoxRelay,
ForwardedEmailServiceType.SimpleLogin
};
UsernameEmailTypeOptions = new List<UsernameEmailType>
{
UsernameEmailType.Random,
UsernameEmailType.Website
};
UsernameTypePromptHelpCommand = new Command(UsernameTypePromptHelp);
RegenerateCommand = new AsyncCommand(RegenerateAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
RegenerateUsernameCommand = new AsyncCommand(RegenerateUsernameAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
ToggleForwardedEmailHiddenValueCommand = new AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
}
public List<string> TypeOptions { get; set; }
public List<GeneratorType> GeneratorTypeOptions { get; set; }
public List<string> PasswordTypeOptions { get; set; }
public List<UsernameType> UsernameTypeOptions { get; set; }
public List<ForwardedEmailServiceType> ForwardedEmailServiceTypeOptions { get; set; }
public List<UsernameEmailType> UsernameEmailTypeOptions { get; set; }
public Command UsernameTypePromptHelpCommand { get; set; }
public ICommand RegenerateCommand { get; set; }
public ICommand RegenerateUsernameCommand { get; set; }
public ICommand ToggleForwardedEmailHiddenValueCommand { get; set; }
public ICommand CopyCommand { get; set; }
public string Password
{
@@ -56,7 +115,18 @@ namespace Bit.App.Pages
});
}
public string ColoredPassword => PasswordFormatter.FormatPassword(Password);
public string Username
{
get => _username;
set => SetProperty(ref _username, value,
additionalPropertyNames: new string[]
{
nameof(ColoredUsername)
});
}
public string ColoredPassword => GeneratedValueFormatter.Format(Password);
public string ColoredUsername => GeneratedValueFormatter.Format(Username);
public bool IsPassword
{
@@ -64,6 +134,32 @@ namespace Bit.App.Pages
set => SetProperty(ref _isPassword, value);
}
public bool IsUsername
{
get => _isUsername;
set => SetProperty(ref _isUsername, value);
}
public bool ShowTypePicker
{
get => _showTypePicker;
set => SetProperty(ref _showTypePicker, value);
}
public bool EditMode
{
get => _editMode;
set => SetProperty(ref _editMode, value, additionalPropertyNames: new string[]
{
nameof(ShowUsernameEmailType)
});
}
public bool ShowUsernameEmailType
{
get => !string.IsNullOrWhiteSpace(EmailWebsite) || EditMode;
}
public int Length
{
get => _length;
@@ -235,6 +331,20 @@ namespace Bit.App.Pages
}
}
public string PlusAddressedEmail
{
get => _usernameOptions.PlusAddressedEmail;
set
{
if (_usernameOptions.PlusAddressedEmail != value)
{
_usernameOptions.PlusAddressedEmail = value;
TriggerPropertyChanged(nameof(PlusAddressedEmail));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public PasswordGeneratorPolicyOptions EnforcedPolicyOptions
{
get => _enforcedPolicyOptions;
@@ -247,24 +357,261 @@ namespace Bit.App.Pages
public bool IsPolicyInEffect => _enforcedPolicyOptions.InEffect();
public int TypeSelectedIndex
public GeneratorType GeneratorTypeSelected
{
get => _typeSelectedIndex;
get => _generatorTypeSelected;
set
{
if (SetProperty(ref _typeSelectedIndex, value))
if (SetProperty(ref _generatorTypeSelected, value))
{
IsPassword = value == 0;
var task = SaveOptionsAsync();
IsUsername = value == GeneratorType.Username;
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
SaveOptionsAsync().FireAndForget();
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public int PasswordTypeSelectedIndex
{
get => _passwordTypeSelectedIndex;
set
{
if (SetProperty(ref _passwordTypeSelectedIndex, value))
{
IsPassword = value == 0;
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
SaveOptionsAsync().FireAndForget();
}
}
}
public UsernameType UsernameTypeSelected
{
get => _usernameOptions.Type;
set
{
if (_usernameOptions.Type != value)
{
_usernameOptions.Type = value;
Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(UsernameTypeSelected), new string[] { nameof(UsernameTypeDescriptionLabel) });
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string UsernameTypeDescriptionLabel => GetUsernameTypeLabelDescription(UsernameTypeSelected);
public ForwardedEmailServiceType ForwardedEmailServiceSelected
{
get => _usernameOptions.ServiceType;
set
{
if (_usernameOptions.ServiceType != value)
{
_usernameOptions.ServiceType = value;
Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string CatchAllEmailDomain
{
get => _usernameOptions.CatchAllEmailDomain;
set
{
if (_usernameOptions.CatchAllEmailDomain != value)
{
_usernameOptions.CatchAllEmailDomain = value;
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string AnonAddyApiAccessToken
{
get => _usernameOptions.AnonAddyApiAccessToken;
set
{
if (_usernameOptions.AnonAddyApiAccessToken != value)
{
_usernameOptions.AnonAddyApiAccessToken = value;
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowAnonAddyApiAccessToken
{
get
{
return _showAnonAddyApiAccessToken;
}
set => SetProperty(ref _showAnonAddyApiAccessToken, value,
additionalPropertyNames: new string[]
{
nameof(ShowAnonAddyHiddenValueIcon)
});
}
public string ShowAnonAddyHiddenValueIcon => _showAnonAddyApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string AnonAddyDomainName
{
get => _usernameOptions.AnonAddyDomainName;
set
{
if (_usernameOptions.AnonAddyDomainName != value)
{
_usernameOptions.AnonAddyDomainName = value;
TriggerPropertyChanged(nameof(AnonAddyDomainName));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string FirefoxRelayApiAccessToken
{
get => _usernameOptions.FirefoxRelayApiAccessToken;
set
{
if (_usernameOptions.FirefoxRelayApiAccessToken != value)
{
_usernameOptions.FirefoxRelayApiAccessToken = value;
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowFirefoxRelayApiAccessToken
{
get
{
return _showFirefoxRelayApiAccessToken;
}
set => SetProperty(ref _showFirefoxRelayApiAccessToken, value,
additionalPropertyNames: new string[]
{
nameof(ShowFirefoxRelayHiddenValueIcon)
});
}
public string ShowFirefoxRelayHiddenValueIcon => _showFirefoxRelayApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string SimpleLoginApiKey
{
get => _usernameOptions.SimpleLoginApiKey;
set
{
if (_usernameOptions.SimpleLoginApiKey != value)
{
_usernameOptions.SimpleLoginApiKey = value;
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowSimpleLoginApiKey
{
get
{
return _showSimpleLoginApiKey;
}
set => SetProperty(ref _showSimpleLoginApiKey, value,
additionalPropertyNames: new string[]
{
nameof(ShowSimpleLoginHiddenValueIcon)
});
}
public string ShowSimpleLoginHiddenValueIcon => _showSimpleLoginApiKey ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public bool CapitalizeRandomWordUsername
{
get => _usernameOptions.CapitalizeRandomWordUsername;
set
{
if (_usernameOptions.CapitalizeRandomWordUsername != value)
{
_usernameOptions.CapitalizeRandomWordUsername = value;
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public bool IncludeNumberRandomWordUsername
{
get => _usernameOptions.IncludeNumberRandomWordUsername;
set
{
if (_usernameOptions.IncludeNumberRandomWordUsername != value)
{
_usernameOptions.IncludeNumberRandomWordUsername = value;
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public UsernameEmailType PlusAddressedEmailTypeSelected
{
get => _plusAddressedEmailTypeSelected;
set
{
if (SetProperty(ref _plusAddressedEmailTypeSelected, value))
{
_usernameOptions.PlusAddressedEmailType = value;
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public UsernameEmailType CatchAllEmailTypeSelected
{
get => _catchAllEmailTypeSelected;
set
{
if (SetProperty(ref _catchAllEmailTypeSelected, value))
{
_usernameOptions.CatchAllEmailType = value;
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string EmailWebsite
{
get => _emailWebsite;
set => SetProperty(ref _emailWebsite, value, additionalPropertyNames: new string[]
{
nameof(ShowUsernameEmailType)
});
}
public async Task InitAsync()
{
(_options, EnforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync();
LoadFromOptions();
await RegenerateAsync();
_usernameOptions = await _usernameGenerationService.GetOptionsAsync();
if (!EditMode)
{
_usernameOptions.CatchAllEmailType = _usernameOptions.PlusAddressedEmailType = UsernameEmailType.Random;
}
TriggerUsernamePropertiesChanged();
Username = Constants.DefaultUsernameGenerated;
_doneIniting = true;
}
@@ -274,6 +621,11 @@ namespace Bit.App.Pages
await _passwordGenerationService.AddHistoryAsync(Password);
}
public async Task RegenerateUsernameAsync()
{
Username = await _usernameGenerationService.GenerateAsync(_usernameOptions);
}
public void RedrawPassword()
{
if (!string.IsNullOrEmpty(_password))
@@ -282,6 +634,14 @@ namespace Bit.App.Pages
}
}
public void RedrawUsername()
{
if (!string.IsNullOrEmpty(_username))
{
TriggerPropertyChanged(nameof(ColoredUsername));
}
}
public async Task SaveOptionsAsync(bool regenerate = true)
{
if (!_doneIniting)
@@ -291,6 +651,7 @@ namespace Bit.App.Pages
SetOptions();
_passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
await _passwordGenerationService.SaveOptionsAsync(_options);
LoadFromOptions();
if (regenerate)
{
@@ -298,6 +659,21 @@ namespace Bit.App.Pages
}
}
public async Task SaveUsernameOptionsAsync(bool regenerate = true)
{
if (!_doneIniting)
{
return;
}
await _usernameGenerationService.SaveOptionsAsync(_usernameOptions);
if (regenerate)
{
await RegenerateUsernameAsync();
}
}
public async Task SliderChangedAsync()
{
await SaveOptionsAsync(false);
@@ -317,15 +693,28 @@ namespace Bit.App.Pages
public async Task CopyAsync()
{
await _clipboardService.CopyTextAsync(Password);
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
await _clipboardService.CopyTextAsync(IsUsername ? Username : Password);
_platformUtilsService.ShowToastForCopiedValue(IsUsername ? AppResources.Username : AppResources.Password);
}
public void UsernameTypePromptHelp()
{
try
{
_platformUtilsService.LaunchUri("https://bitwarden.com/help/generator/#username-types");
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
}
private void LoadFromOptions()
{
AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault();
TypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
IsPassword = TypeSelectedIndex == 0;
PasswordTypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
IsPassword = PasswordTypeSelectedIndex == 0;
MinNumber = _options.MinNumber.GetValueOrDefault();
MinSpecial = _options.MinSpecial.GetValueOrDefault();
Special = _options.Special.GetValueOrDefault();
@@ -339,10 +728,30 @@ namespace Bit.App.Pages
IncludeNumber = _options.IncludeNumber.GetValueOrDefault();
}
private void TriggerUsernamePropertiesChanged()
{
TriggerPropertyChanged(nameof(CatchAllEmailTypeSelected));
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
TriggerPropertyChanged(nameof(AnonAddyDomainName));
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
TriggerPropertyChanged(nameof(UsernameTypeSelected));
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
TriggerPropertyChanged(nameof(PlusAddressedEmail));
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
TriggerPropertyChanged(nameof(UsernameTypeDescriptionLabel));
}
private void SetOptions()
{
_options.AllowAmbiguousChar = AllowAmbiguousChars;
_options.Type = TypeSelectedIndex == 1 ? "passphrase" : "password";
_options.Type = PasswordTypeSelectedIndex == 1 ? "passphrase" : "password";
_options.MinNumber = MinNumber;
_options.MinSpecial = MinSpecial;
_options.Special = Special;
@@ -355,5 +764,51 @@ namespace Bit.App.Pages
_options.Capitalize = Capitalize;
_options.IncludeNumber = IncludeNumber;
}
private async void OnSubmitException(Exception ex)
{
_logger.Value.Exception(ex);
if (IsUsername && UsernameTypeSelected == UsernameType.ForwardedEmailAlias)
{
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(
AppResources.AnErrorHasOccurred, string.Format(AppResources.UnknownXErrorMessage, ForwardedEmailServiceSelected), AppResources.Ok));
}
else
{
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok));
}
}
private string GetUsernameTypeLabelDescription(UsernameType value)
{
switch (value)
{
case UsernameType.PlusAddressedEmail:
return AppResources.PlusAddressedEmailDescription;
case UsernameType.CatchAllEmail:
return AppResources.CatchAllEmailDescription;
case UsernameType.ForwardedEmailAlias:
return AppResources.ForwardedEmailDescription;
default:
return string.Empty;
}
}
private async Task ToggleForwardedEmailHiddenValueAsync()
{
switch (ForwardedEmailServiceSelected)
{
case ForwardedEmailServiceType.AnonAddy:
ShowAnonAddyApiAccessToken = !ShowAnonAddyApiAccessToken;
break;
case ForwardedEmailServiceType.FirefoxRelay:
ShowFirefoxRelayApiAccessToken = !ShowFirefoxRelayApiAccessToken;
break;
case ForwardedEmailServiceType.SimpleLogin:
ShowSimpleLoginApiKey = !ShowSimpleLoginApiKey;
break;
}
}
}
}