mirror of
https://github.com/bitwarden/mobile
synced 2025-01-28 01:09:43 +01: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:
parent
673ba9f3cc
commit
b1fb867b6e
@ -1,12 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
|
||||
x:Class="Bit.App.Pages.GeneratorPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||
xmlns:enums="clr-namespace:Bit.Core.Enums;assembly=BitwardenCore"
|
||||
x:DataType="pages:GeneratorPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
@ -16,6 +19,8 @@
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:LocalizableEnumConverter x:Key="localizableEnum" />
|
||||
<xct:EnumToBoolConverter x:Key="enumToBool"/>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Select}"
|
||||
@ -42,60 +47,302 @@
|
||||
in ContentView.-->
|
||||
<ContentView>
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||
Margin="0, 12, 0, 0"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="Accent">
|
||||
<Label
|
||||
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<StackLayout Spacing="0"
|
||||
Padding="10,0">
|
||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||
Margin="0, 12, 0, 0"
|
||||
Padding="10,0"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="Accent">
|
||||
<Label
|
||||
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-row"
|
||||
RowDefinitions="Auto"
|
||||
ColumnDefinitions="*,Auto,Auto">
|
||||
<controls:MonoLabel
|
||||
x:Name="lblPassword"
|
||||
StyleClass="text-lg, text-html"
|
||||
Text="{Binding ColoredPassword, Mode=OneWay}"
|
||||
Margin="0, 20" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
||||
Command="{Binding RegenerateCommand}"
|
||||
Grid.Column="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n GeneratePassword}" />
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding IsUsername}"
|
||||
StyleClass="box-row"
|
||||
RowDefinitions="Auto"
|
||||
ColumnDefinitions="*,Auto,Auto">
|
||||
<controls:MonoLabel
|
||||
x:Name="lblUsername"
|
||||
StyleClass="text-lg, text-html"
|
||||
Text="{Binding ColoredUsername, Mode=OneWay}"
|
||||
Margin="0, 20"
|
||||
HorizontalTextAlignment="Center"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
LineBreakMode="CharacterWrap" />
|
||||
<Button Text="{u:I18n RegeneratePassword}"
|
||||
StyleClass="btn-primary"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
Clicked="Regenerate_Clicked"></Button>
|
||||
<Button Text="{u:I18n CopyPassword}"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
Clicked="Copy_Clicked"></Button>
|
||||
HorizontalOptions="Start" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
Grid.Column="1"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyUsername}" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
||||
Command="{Binding RegenerateUsernameCommand}"
|
||||
Grid.Column="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n GenerateUsername}" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"/>
|
||||
<StackLayout StyleClass="box"
|
||||
IsVisible="{Binding ShowTypePicker}"
|
||||
Padding="0,10">
|
||||
<Label
|
||||
Text="{u:I18n WhatWouldYouLikeToGenerate}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_typePicker"
|
||||
ItemsSource="{Binding GeneratorTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding GeneratorTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n Options, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label Text="{u:I18n Options, Header=True}"
|
||||
StyleClass="box-header, box-header-platform"
|
||||
Margin="0,10,0,0"/>
|
||||
<!--USERNAME OPTIONS-->
|
||||
<StackLayout IsVisible="{Binding IsUsername}">
|
||||
<StackLayout Orientation="Horizontal">
|
||||
<Label
|
||||
Text="{u:I18n Type}"
|
||||
Text="{u:I18n UsernameType}"
|
||||
StyleClass="box-label"
|
||||
VerticalOptions="Center"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.QuestionCircle}}"
|
||||
Command="{Binding UsernameTypePromptHelpCommand}"
|
||||
TextColor="{DynamicResource HyperlinkColor}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n UsernamePromptHelpLink}"
|
||||
VerticalOptions="Center"/>
|
||||
</StackLayout>
|
||||
<Picker
|
||||
x:Name="_usernameTypePicker"
|
||||
ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding UsernameTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
<Label
|
||||
StyleClass="box-footer-label"
|
||||
Text="{Binding UsernameTypeDescriptionLabel}" />
|
||||
<!--PLUS ADDRESSED EMAIL OPTIONS-->
|
||||
<StackLayout StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.PlusAddressedEmail}}">
|
||||
<Label Text="{u:I18n EmailRequiredParenthesis}"
|
||||
StyleClass="box-label" />
|
||||
<Entry x:Name="_plusAddressedEmailEntry"
|
||||
Text="{Binding PlusAddressedEmail}"
|
||||
StyleClass="box-value" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n EmailType}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"/>
|
||||
<Picker IsVisible="{Binding ShowUsernameEmailType}"
|
||||
x:Name="_plusAddressedEmailTypePicker"
|
||||
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding PlusAddressedEmailTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n Website}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{Binding EmailWebsite}"
|
||||
StyleClass="box-value" />
|
||||
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
|
||||
StyleClass="box-row-separator"
|
||||
Margin="0,10,0,0" />
|
||||
</StackLayout>
|
||||
<!--CATCH-ALL EMAIL OPTIONS-->
|
||||
<StackLayout StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.CatchAllEmail}}">
|
||||
<Label
|
||||
Text="{u:I18n DomainNameRequiredParenthesis}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_catchAllEmailDomainNameEntry"
|
||||
Text="{Binding CatchAllEmailDomain}"
|
||||
StyleClass="box-value" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n EmailType}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"/>
|
||||
<Picker IsVisible="{Binding ShowUsernameEmailType}"
|
||||
x:Name="_catchallEmailTypePicker"
|
||||
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding CatchAllEmailTypeSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{u:I18n Website}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0" />
|
||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
||||
Text="{Binding EmailWebsite}"
|
||||
StyleClass="box-value"/>
|
||||
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
|
||||
StyleClass="box-row-separator"
|
||||
Margin="0,10,0,0"/>
|
||||
</StackLayout>
|
||||
<!--FORWARDED EMAIL OPTIONS-->
|
||||
<StackLayout StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.ForwardedEmailAlias}}">
|
||||
<Label
|
||||
Text="{u:I18n Service}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_typePicker"
|
||||
ItemsSource="{Binding TypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding TypeSelectedIndex}"
|
||||
x:Name="_serviceTypePicker"
|
||||
ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}"
|
||||
SelectedItem="{Binding ForwardedEmailServiceSelected}"
|
||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
||||
StyleClass="box-value" />
|
||||
<!--ANONADDY OPTIONS-->
|
||||
<Grid IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Margin="0,10,0,0"
|
||||
Text="{u:I18n APIAccessToken}"
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_anonAddyApiAccessTokenEntry"
|
||||
Text="{Binding AnonAddyApiAccessToken}"
|
||||
IsPassword="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowAnonAddyHiddenValueIcon}"
|
||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"/>
|
||||
</Grid>
|
||||
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||
Text="{u:I18n DomainNameRequiredParenthesis}"
|
||||
StyleClass="box-label"
|
||||
Margin="0,10,0,0"/>
|
||||
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
||||
x:Name="_anonAddyDomainNameEntry"
|
||||
Text="{Binding AnonAddyDomainName}"
|
||||
StyleClass="box-value"/>
|
||||
<!--FIREFOX RELAY OPTIONS-->
|
||||
<Grid StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.FirefoxRelay}}"
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Text="{u:I18n APIAccessToken}"
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_firefoxRelayApiAccessTokenEntry"
|
||||
Text="{Binding FirefoxRelayApiAccessToken}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
IsPassword="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool}}"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowFirefoxRelayHiddenValueIcon}"
|
||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"/>
|
||||
</Grid>
|
||||
<!--SIMPLELOGIN OPTIONS-->
|
||||
<Grid StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.SimpleLogin}}"
|
||||
Grid.RowDefinitions="Auto,*"
|
||||
Grid.ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Text="{u:I18n APIKeyRequiredParenthesis}"
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_simpleLoginApiKeyEntry"
|
||||
Text="{Binding SimpleLoginApiKey}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
IsPassword="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool}}"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowSimpleLoginHiddenValueIcon}"
|
||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"/>
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<!--RANDOM WORD OPTIONS-->
|
||||
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
|
||||
<Label
|
||||
Text="{u:I18n Capitalize}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding CapitalizeRandomWordUsername}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</Grid>
|
||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
||||
StyleClass="box-row-separator" />
|
||||
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
|
||||
<Label
|
||||
Text="{u:I18n IncludeNumber}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding IncludeNumberRandomWordUsername}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</Grid>
|
||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
||||
StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
<!--PASSWORD OPTIONS-->
|
||||
<StackLayout IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
Text="{u:I18n PasswordType}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_passwordTypePicker"
|
||||
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding PasswordTypeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0"
|
||||
|
@ -18,7 +18,7 @@ namespace Bit.App.Pages
|
||||
private readonly Action<string> _selectAction;
|
||||
private readonly TabsPage _tabsPage;
|
||||
|
||||
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null)
|
||||
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false)
|
||||
{
|
||||
_tabsPage = tabsPage;
|
||||
InitializeComponent();
|
||||
@ -27,6 +27,10 @@ namespace Bit.App.Pages
|
||||
_vm.Page = this;
|
||||
_fromTabPage = fromTabPage;
|
||||
_selectAction = selectAction;
|
||||
_vm.ShowTypePicker = fromTabPage;
|
||||
_vm.IsUsername = isUsernameGenerator;
|
||||
_vm.EmailWebsite = emailWebsite;
|
||||
_vm.EditMode = editMode;
|
||||
var isIos = Device.RuntimePlatform == Device.iOS;
|
||||
if (selectAction != null)
|
||||
{
|
||||
@ -47,10 +51,12 @@ namespace Bit.App.Pages
|
||||
ToolbarItems.Add(_historyItem);
|
||||
}
|
||||
}
|
||||
if (isIos)
|
||||
{
|
||||
_typePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
_typePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_passwordTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_usernameTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_serviceTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_plusAddressedEmailTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_catchallEmailTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
@ -97,16 +103,6 @@ namespace Bit.App.Pages
|
||||
return base.OnBackButtonPressed();
|
||||
}
|
||||
|
||||
private async void Regenerate_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
await _vm.RegenerateAsync();
|
||||
}
|
||||
|
||||
private async void Copy_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
await _vm.CopyAsync();
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (!DoOnce())
|
||||
@ -124,7 +120,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private void Select_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
_selectAction?.Invoke(_vm.Password);
|
||||
_selectAction?.Invoke(_vm.IsUsername ? _vm.Username : _vm.Password);
|
||||
}
|
||||
|
||||
private async void History_Clicked(object sender, EventArgs e)
|
||||
@ -150,7 +146,20 @@ namespace Bit.App.Pages
|
||||
{
|
||||
await base.UpdateOnThemeChanged();
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(() => _vm?.RedrawPassword());
|
||||
await Device.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
if (_vm != null)
|
||||
{
|
||||
if (_vm.IsUsername)
|
||||
{
|
||||
_vm.RedrawUsername();
|
||||
}
|
||||
else
|
||||
{
|
||||
_vm.RedrawPassword();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,15 +107,26 @@
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
<Grid StyleClass="box-row, box-row-input"
|
||||
RowDefinitions="Auto,*"
|
||||
ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Text="{u:I18n Username}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_loginUsernameEntry"
|
||||
Text="{Binding Cipher.Login.Username}"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"/>
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
|
||||
Command="{Binding GenerateUsernameCommand}"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n GenerateUsername}" />
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
|
@ -84,6 +84,7 @@ namespace Bit.App.Pages
|
||||
FieldOptionsCommand = new Command<CipherAddEditPageFieldViewModel>(FieldOptions);
|
||||
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
|
||||
CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
GenerateUsernameCommand = new AsyncCommand(GenerateUsernameAsync, onException: ex => OnGenerateUsernameException(ex), allowsMultipleExecutions: false);
|
||||
Uris = new ExtendedObservableCollection<LoginUriView>();
|
||||
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
|
||||
Collections = new ExtendedObservableCollection<CollectionViewModel>();
|
||||
@ -145,6 +146,7 @@ namespace Bit.App.Pages
|
||||
public Command FieldOptionsCommand { get; set; }
|
||||
public Command PasswordPromptHelpCommand { get; set; }
|
||||
public AsyncCommand CopyCommand { get; set; }
|
||||
public AsyncCommand GenerateUsernameCommand { get; set; }
|
||||
public string CipherId { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
@ -592,6 +594,30 @@ namespace Bit.App.Pages
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
public async Task GenerateUsernameAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Cipher?.Login?.Username)
|
||||
&& !await _platformUtilsService.ShowDialogAsync(AppResources.AreYouSureYouWantToOverwriteTheCurrentUsername, null, AppResources.Yes, AppResources.No))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var page = new GeneratorPage(false, async (username) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Cipher.Login.Username = username;
|
||||
TriggerCipherChanged();
|
||||
await Page.Navigation.PopModalAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnGenerateUsernameException(ex);
|
||||
}
|
||||
}, isUsernameGenerator: true, emailWebsite: Cipher?.Name, editMode: true);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
public async void UriOptions(LoginUriView uri)
|
||||
{
|
||||
if (!(Page as CipherAddEditPage).DoOnce())
|
||||
@ -838,6 +864,12 @@ namespace Bit.App.Pages
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnGenerateUsernameException(Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
|
||||
public class CipherAddEditPageFieldViewModel : ExtendedViewModel
|
||||
|
@ -141,7 +141,7 @@ namespace Bit.App.Pages
|
||||
public bool IsIdentity => Cipher?.Type == Core.Enums.CipherType.Identity;
|
||||
public bool IsCard => Cipher?.Type == Core.Enums.CipherType.Card;
|
||||
public bool IsSecureNote => Cipher?.Type == Core.Enums.CipherType.SecureNote;
|
||||
public FormattedString ColoredPassword => PasswordFormatter.FormatPassword(Cipher.Login.Password);
|
||||
public FormattedString ColoredPassword => GeneratedValueFormatter.Format(Cipher.Login.Password);
|
||||
public FormattedString UpdatedText
|
||||
{
|
||||
get
|
||||
@ -751,7 +751,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public FormattedString ColoredHiddenValue => PasswordFormatter.FormatPassword(_field.Value);
|
||||
public FormattedString ColoredHiddenValue => GeneratedValueFormatter.Format(_field.Value);
|
||||
|
||||
public Command ToggleHiddenValueCommand { get; set; }
|
||||
|
||||
|
146
src/App/Resources/AppResources.Designer.cs
generated
146
src/App/Resources/AppResources.Designer.cs
generated
@ -1,4 +1,4 @@
|
||||
//------------------------------------------------------------------------------
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
@ -4150,5 +4150,149 @@ namespace Bit.App.Resources {
|
||||
return ResourceManager.GetString("AreYouSureYouWantToEnableScreenCapture", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PasswordType {
|
||||
get {
|
||||
return ResourceManager.GetString("PasswordType", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string WhatWouldYouLikeToGenerate {
|
||||
get {
|
||||
return ResourceManager.GetString("WhatWouldYouLikeToGenerate", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string UsernameType {
|
||||
get {
|
||||
return ResourceManager.GetString("UsernameType", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PlusAddressedEmail {
|
||||
get {
|
||||
return ResourceManager.GetString("PlusAddressedEmail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string CatchAllEmail {
|
||||
get {
|
||||
return ResourceManager.GetString("CatchAllEmail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ForwardedEmailAlias {
|
||||
get {
|
||||
return ResourceManager.GetString("ForwardedEmailAlias", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string RandomWord {
|
||||
get {
|
||||
return ResourceManager.GetString("RandomWord", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string EmailRequiredParenthesis {
|
||||
get {
|
||||
return ResourceManager.GetString("EmailRequiredParenthesis", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string DomainNameRequiredParenthesis {
|
||||
get {
|
||||
return ResourceManager.GetString("DomainNameRequiredParenthesis", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string APIKeyRequiredParenthesis {
|
||||
get {
|
||||
return ResourceManager.GetString("APIKeyRequiredParenthesis", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Service {
|
||||
get {
|
||||
return ResourceManager.GetString("Service", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string AnonAddy {
|
||||
get {
|
||||
return ResourceManager.GetString("AnonAddy", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string FirefoxRelay {
|
||||
get {
|
||||
return ResourceManager.GetString("FirefoxRelay", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SimpleLogin {
|
||||
get {
|
||||
return ResourceManager.GetString("SimpleLogin", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string APIAccessToken {
|
||||
get {
|
||||
return ResourceManager.GetString("APIAccessToken", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string AreYouSureYouWantToOverwriteTheCurrentUsername {
|
||||
get {
|
||||
return ResourceManager.GetString("AreYouSureYouWantToOverwriteTheCurrentUsername", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GenerateUsername {
|
||||
get {
|
||||
return ResourceManager.GetString("GenerateUsername", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string EmailType {
|
||||
get {
|
||||
return ResourceManager.GetString("EmailType", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string WebsiteRequired {
|
||||
get {
|
||||
return ResourceManager.GetString("WebsiteRequired", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string UnknownXErrorMessage {
|
||||
get {
|
||||
return ResourceManager.GetString("UnknownXErrorMessage", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PlusAddressedEmailDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("PlusAddressedEmailDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string CatchAllEmailDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("CatchAllEmailDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ForwardedEmailDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("ForwardedEmailDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Random {
|
||||
get {
|
||||
return ResourceManager.GetString("Random", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
@ -2314,4 +2314,79 @@ select Add TOTP to store the key safely</value>
|
||||
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||
<value>Are you sure you want to enable Screen Capture?</value>
|
||||
</data>
|
||||
<data name="PasswordType" xml:space="preserve">
|
||||
<value>Password Type</value>
|
||||
</data>
|
||||
<data name="WhatWouldYouLikeToGenerate" xml:space="preserve">
|
||||
<value>What would you like to generate?</value>
|
||||
</data>
|
||||
<data name="UsernameType" xml:space="preserve">
|
||||
<value>Username Type</value>
|
||||
</data>
|
||||
<data name="PlusAddressedEmail" xml:space="preserve">
|
||||
<value>Plus Addressed Email</value>
|
||||
</data>
|
||||
<data name="CatchAllEmail" xml:space="preserve">
|
||||
<value>Catch-all Email</value>
|
||||
</data>
|
||||
<data name="ForwardedEmailAlias" xml:space="preserve">
|
||||
<value>Forwarded Email Alias</value>
|
||||
</data>
|
||||
<data name="RandomWord" xml:space="preserve">
|
||||
<value>Random Word</value>
|
||||
</data>
|
||||
<data name="EmailRequiredParenthesis" xml:space="preserve">
|
||||
<value>Email (required)</value>
|
||||
</data>
|
||||
<data name="DomainNameRequiredParenthesis" xml:space="preserve">
|
||||
<value>Domain Name (required)</value>
|
||||
</data>
|
||||
<data name="APIKeyRequiredParenthesis" xml:space="preserve">
|
||||
<value>API Key (required)</value>
|
||||
</data>
|
||||
<data name="Service" xml:space="preserve">
|
||||
<value>Service</value>
|
||||
</data>
|
||||
<data name="AnonAddy" xml:space="preserve">
|
||||
<value>AnonAddy</value>
|
||||
<comment>"AnonAddy" is the product name and should not be translated.</comment>
|
||||
</data>
|
||||
<data name="FirefoxRelay" xml:space="preserve">
|
||||
<value>Firefox Relay</value>
|
||||
<comment>"Firefox Relay" is the product name and should not be translated.</comment>
|
||||
</data>
|
||||
<data name="SimpleLogin" xml:space="preserve">
|
||||
<value>SimpleLogin</value>
|
||||
<comment>"SimpleLogin" is the product name and should not be translated.</comment>
|
||||
</data>
|
||||
<data name="APIAccessToken" xml:space="preserve">
|
||||
<value>API Access Token</value>
|
||||
</data>
|
||||
<data name="AreYouSureYouWantToOverwriteTheCurrentUsername" xml:space="preserve">
|
||||
<value>Are you sure you want to overwrite the current username?</value>
|
||||
</data>
|
||||
<data name="GenerateUsername" xml:space="preserve">
|
||||
<value>Generate Username</value>
|
||||
</data>
|
||||
<data name="EmailType" xml:space="preserve">
|
||||
<value>Email Type</value>
|
||||
</data>
|
||||
<data name="WebsiteRequired" xml:space="preserve">
|
||||
<value>Website (required)</value>
|
||||
</data>
|
||||
<data name="UnknownXErrorMessage" xml:space="preserve">
|
||||
<value>Unknown {0} error occurred.</value>
|
||||
</data>
|
||||
<data name="PlusAddressedEmailDescription" xml:space="preserve">
|
||||
<value>Use your email provider's subaddress capabilities</value>
|
||||
</data>
|
||||
<data name="CatchAllEmailDescription" xml:space="preserve">
|
||||
<value>Use your domain's configured catch-all inbox.</value>
|
||||
</data>
|
||||
<data name="ForwardedEmailDescription" xml:space="preserve">
|
||||
<value>Generate an email alias with an external forwarding service.</value>
|
||||
</data>
|
||||
<data name="Random" xml:space="preserve">
|
||||
<value>Random</value>
|
||||
</data>
|
||||
</root>
|
||||
|
@ -567,6 +567,8 @@ namespace Bit.App.Utilities
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
var searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||
var usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>(
|
||||
"usernameGenerationService");
|
||||
|
||||
await Task.WhenAll(
|
||||
cipherService.ClearCacheAsync(),
|
||||
@ -580,6 +582,7 @@ namespace Bit.App.Utilities
|
||||
passwordGenerationService.ClearCache();
|
||||
policyService.ClearCache();
|
||||
searchService.ClearIndex();
|
||||
usernameGenerationService.ClearCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ namespace Bit.App.Utilities
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return PasswordFormatter.FormatPassword((string)value);
|
||||
return GeneratedValueFormatter.Format((string)value);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter,
|
||||
|
@ -5,14 +5,14 @@ using Xamarin.Forms;
|
||||
namespace Bit.App.Utilities
|
||||
{
|
||||
/**
|
||||
* Helper class to format a password with numeric encoding to separate
|
||||
* Helper class to format a password/username with numeric encoding to separate
|
||||
* normal text from numbers and special characters.
|
||||
*/
|
||||
class PasswordFormatter
|
||||
class GeneratedValueFormatter
|
||||
{
|
||||
/**
|
||||
* This enum is used for the state machine when building the colorized
|
||||
* password string.
|
||||
* password/username string.
|
||||
*/
|
||||
private enum CharType
|
||||
{
|
||||
@ -22,9 +22,9 @@ namespace Bit.App.Utilities
|
||||
Special
|
||||
}
|
||||
|
||||
public static string FormatPassword(string password)
|
||||
public static string Format(string generatedValue)
|
||||
{
|
||||
if (password == null)
|
||||
if (generatedValue == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
@ -37,7 +37,7 @@ namespace Bit.App.Utilities
|
||||
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. Hence, only iOS gets the div.
|
||||
// display:inline-block and adds a newline after the password/username. Hence, only iOS gets the div.
|
||||
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\">";
|
||||
@ -47,7 +47,7 @@ namespace Bit.App.Utilities
|
||||
// state.
|
||||
var currentType = CharType.None;
|
||||
|
||||
foreach (var c in password)
|
||||
foreach (var c in generatedValue)
|
||||
{
|
||||
// First, identify what the current char is.
|
||||
CharType charType;
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Models.Response;
|
||||
@ -82,5 +83,6 @@ namespace Bit.Core.Abstractions
|
||||
Task<SendResponse> PutSendAsync(string id, SendRequest request);
|
||||
Task<SendResponse> PutSendRemovePasswordAsync(string id);
|
||||
Task DeleteSendAsync(string id);
|
||||
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
|
||||
}
|
||||
}
|
||||
|
@ -151,5 +151,7 @@ namespace Bit.Core.Abstractions
|
||||
Task<bool> GetScreenCaptureAllowedAsync(string userId = null);
|
||||
Task SetScreenCaptureAllowedAsync(bool value, string userId = null);
|
||||
Task SaveExtensionActiveUserIdToStorageAsync(string userId);
|
||||
Task<UsernameGenerationOptions> GetUsernameGenerationOptionsAsync(string userId = null);
|
||||
Task SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null);
|
||||
}
|
||||
}
|
||||
|
13
src/Core/Abstractions/IUsernameGenerationService.cs
Normal file
13
src/Core/Abstractions/IUsernameGenerationService.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IUsernameGenerationService
|
||||
{
|
||||
Task<string> GenerateAsync(UsernameGenerationOptions options);
|
||||
void ClearCache();
|
||||
Task<UsernameGenerationOptions> GetOptionsAsync();
|
||||
Task SaveOptionsAsync(UsernameGenerationOptions options);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
public const int MaxAccounts = 5;
|
||||
public const string AndroidAppProtocol = "androidapp://";
|
||||
public const string iOSAppProtocol = "iosapp://";
|
||||
public const string DefaultUsernameGenerated = "-";
|
||||
public static string StateVersionKey = "stateVersion";
|
||||
public static string StateKey = "state";
|
||||
public static string PreAuthEnvironmentUrlsKey = "preAuthEnvironmentUrls";
|
||||
@ -83,5 +84,6 @@
|
||||
public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}";
|
||||
public static string LastSyncKey(string userId) => $"lastSync_{userId}";
|
||||
public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}";
|
||||
public static string UsernameGenOptionsKey(string userId) => $"usernameGenerationOptions_{userId}";
|
||||
}
|
||||
}
|
||||
|
14
src/Core/Enums/ForwardedEmailServiceType.cs
Normal file
14
src/Core/Enums/ForwardedEmailServiceType.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Bit.Core.Attributes;
|
||||
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum ForwardedEmailServiceType
|
||||
{
|
||||
[LocalizableEnum("AnonAddy")]
|
||||
AnonAddy = 0,
|
||||
[LocalizableEnum("FirefoxRelay")]
|
||||
FirefoxRelay = 1,
|
||||
[LocalizableEnum("SimpleLogin")]
|
||||
SimpleLogin = 2,
|
||||
}
|
||||
}
|
12
src/Core/Enums/GeneratorType.cs
Normal file
12
src/Core/Enums/GeneratorType.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Bit.Core.Attributes;
|
||||
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum GeneratorType
|
||||
{
|
||||
[LocalizableEnum("Password")]
|
||||
Password = 0,
|
||||
[LocalizableEnum("Username")]
|
||||
Username = 1
|
||||
}
|
||||
}
|
12
src/Core/Enums/UsernameEmailType.cs
Normal file
12
src/Core/Enums/UsernameEmailType.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Bit.Core.Attributes;
|
||||
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum UsernameEmailType
|
||||
{
|
||||
[LocalizableEnum("Random")]
|
||||
Random = 0,
|
||||
[LocalizableEnum("Website")]
|
||||
Website = 1,
|
||||
}
|
||||
}
|
16
src/Core/Enums/UsernameType.cs
Normal file
16
src/Core/Enums/UsernameType.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Bit.Core.Attributes;
|
||||
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum UsernameType
|
||||
{
|
||||
[LocalizableEnum("PlusAddressedEmail")]
|
||||
PlusAddressedEmail = 0,
|
||||
[LocalizableEnum("CatchAllEmail")]
|
||||
CatchAllEmail = 1,
|
||||
[LocalizableEnum("ForwardedEmailAlias")]
|
||||
ForwardedEmailAlias = 2,
|
||||
[LocalizableEnum("RandomWord")]
|
||||
RandomWord = 3,
|
||||
}
|
||||
}
|
23
src/Core/Models/Domain/UsernameGenerationOptions.cs
Normal file
23
src/Core/Models/Domain/UsernameGenerationOptions.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Domain
|
||||
{
|
||||
public class UsernameGenerationOptions
|
||||
{
|
||||
public UsernameGenerationOptions() { }
|
||||
|
||||
public UsernameType Type { get; set; }
|
||||
public ForwardedEmailServiceType ServiceType { get; set; }
|
||||
public UsernameEmailType PlusAddressedEmailType { get; set; }
|
||||
public UsernameEmailType CatchAllEmailType { get; set; }
|
||||
public bool CapitalizeRandomWordUsername { get; set; }
|
||||
public bool IncludeNumberRandomWordUsername { get; set; }
|
||||
public string PlusAddressedEmail { get; set; }
|
||||
public string CatchAllEmailDomain { get; set; }
|
||||
public string FirefoxRelayApiAccessToken { get; set; }
|
||||
public string SimpleLoginApiKey { get; set; }
|
||||
public string AnonAddyApiAccessToken { get; set; }
|
||||
public string AnonAddyDomainName { get; set; }
|
||||
public string EmailWebsite { get; set; }
|
||||
}
|
||||
}
|
9
src/Core/Models/Domain/UsernameGeneratorConfig.cs
Normal file
9
src/Core/Models/Domain/UsernameGeneratorConfig.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Models.Domain
|
||||
{
|
||||
public class UsernameGeneratorConfig
|
||||
{
|
||||
public string ApiToken { get; set; }
|
||||
public string Domain { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
@ -700,6 +700,66 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config)
|
||||
{
|
||||
using (var requestMessage = new HttpRequestMessage())
|
||||
{
|
||||
requestMessage.Version = new Version(1, 0);
|
||||
requestMessage.Method = HttpMethod.Post;
|
||||
requestMessage.RequestUri = new Uri(config.Url);
|
||||
requestMessage.Headers.Add("Accept", "application/json");
|
||||
|
||||
switch (service)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
requestMessage.Headers.Add("Authorization", $"Bearer {config.ApiToken}");
|
||||
requestMessage.Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["domain"] = config.Domain
|
||||
});
|
||||
break;
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
requestMessage.Headers.Add("Authorization", $"Token {config.ApiToken}");
|
||||
break;
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
requestMessage.Headers.Add("Authentication", config.ApiToken);
|
||||
break;
|
||||
}
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await _httpClient.SendAsync(requestMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ApiException(HandleWebError(e));
|
||||
}
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new ApiException(new ErrorResponse
|
||||
{
|
||||
StatusCode = response.StatusCode,
|
||||
Message = $"{service} error: {(int)response.StatusCode} {response.ReasonPhrase}."
|
||||
});
|
||||
}
|
||||
var responseJsonString = await response.Content.ReadAsStringAsync();
|
||||
var result = JObject.Parse(responseJsonString);
|
||||
|
||||
switch (service)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
return result["data"]?["email"]?.ToString();
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
return result["full_address"]?.ToString();
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
return result["alias"]?.ToString();
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ErrorResponse HandleWebError(Exception e)
|
||||
{
|
||||
return new ErrorResponse
|
||||
|
@ -1145,6 +1145,22 @@ namespace Bit.Core.Services
|
||||
await SetValueAsync(key, value, reconciledOptions);
|
||||
}
|
||||
|
||||
public async Task<UsernameGenerationOptions> GetUsernameGenerationOptionsAsync(string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
await GetDefaultStorageOptionsAsync());
|
||||
var key = Constants.UsernameGenOptionsKey(reconciledOptions.UserId);
|
||||
return await GetValueAsync<UsernameGenerationOptions>(key, reconciledOptions);
|
||||
}
|
||||
|
||||
public async Task SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
await GetDefaultStorageOptionsAsync());
|
||||
var key = Constants.UsernameGenOptionsKey(reconciledOptions.UserId);
|
||||
await SetValueAsync(key, value, reconciledOptions);
|
||||
}
|
||||
|
||||
public async Task<List<GeneratedPasswordHistory>> GetEncryptedPasswordGenerationHistory(string userId = null)
|
||||
{
|
||||
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||
@ -1458,6 +1474,7 @@ namespace Bit.Core.Services
|
||||
await SetAutoDarkThemeAsync(null, userId);
|
||||
await SetAddSitePromptShownAsync(null, userId);
|
||||
await SetPasswordGenerationOptionsAsync(null, userId);
|
||||
await SetUsernameGenerationOptionsAsync(null, userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
211
src/Core/Services/UsernameGenerationService.cs
Normal file
211
src/Core/Services/UsernameGenerationService.cs
Normal file
@ -0,0 +1,211 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class UsernameGenerationService : IUsernameGenerationService
|
||||
{
|
||||
private const string CATCH_ALL_EMAIL_DOMAIN_FORMAT = "{0}@{1}";
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IStateService _stateService;
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
private UsernameGenerationOptions _optionsCache;
|
||||
|
||||
public UsernameGenerationService(
|
||||
ICryptoService cryptoService,
|
||||
IApiService apiService,
|
||||
IStateService stateService)
|
||||
{
|
||||
_cryptoService = cryptoService;
|
||||
_apiService = apiService;
|
||||
_stateService = stateService;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAsync(UsernameGenerationOptions options)
|
||||
{
|
||||
switch (options.Type)
|
||||
{
|
||||
case UsernameType.PlusAddressedEmail:
|
||||
return await GeneratePlusAddressedEmailAsync(options);
|
||||
case UsernameType.CatchAllEmail:
|
||||
return await GenerateCatchAllAsync(options);
|
||||
case UsernameType.ForwardedEmailAlias:
|
||||
return await GenerateForwardedEmailAliasAsync(options);
|
||||
case UsernameType.RandomWord:
|
||||
return await GenerateRandomWordAsync(options);
|
||||
default:
|
||||
_logger.Value.Error($"Error UsernameGenerationService: UsernameType {options.Type} not implemented.");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UsernameGenerationOptions> GetOptionsAsync()
|
||||
{
|
||||
if (_optionsCache == null)
|
||||
{
|
||||
var options = await _stateService.GetUsernameGenerationOptionsAsync();
|
||||
_optionsCache = options ?? new UsernameGenerationOptions();
|
||||
}
|
||||
|
||||
return _optionsCache;
|
||||
}
|
||||
public async Task SaveOptionsAsync(UsernameGenerationOptions options)
|
||||
{
|
||||
await _stateService.SetUsernameGenerationOptionsAsync(options);
|
||||
_optionsCache = options;
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
_optionsCache = null;
|
||||
}
|
||||
|
||||
private async Task<string> GenerateRandomWordAsync(UsernameGenerationOptions options)
|
||||
{
|
||||
var listLength = EEFLongWordList.Instance.List.Count - 1;
|
||||
var wordIndex = await _cryptoService.RandomNumberAsync(0, listLength);
|
||||
var randomWord = EEFLongWordList.Instance.List[wordIndex];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(randomWord))
|
||||
{
|
||||
_logger.Value.Error($"Error UsernameGenerationService: EEFLongWordList has NullOrWhiteSpace value at {wordIndex} index.");
|
||||
return Constants.DefaultUsernameGenerated;
|
||||
}
|
||||
|
||||
if (options.CapitalizeRandomWordUsername)
|
||||
{
|
||||
randomWord = Capitalize(randomWord);
|
||||
}
|
||||
|
||||
if (options.IncludeNumberRandomWordUsername)
|
||||
{
|
||||
randomWord = await AppendRandomNumberToRandomWordAsync(randomWord);
|
||||
}
|
||||
|
||||
return randomWord;
|
||||
}
|
||||
|
||||
private async Task<string> GeneratePlusAddressedEmailAsync(UsernameGenerationOptions options)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.PlusAddressedEmail) || options.PlusAddressedEmail.Length < 3)
|
||||
{
|
||||
return Constants.DefaultUsernameGenerated;
|
||||
}
|
||||
|
||||
var atIndex = options.PlusAddressedEmail.IndexOf("@");
|
||||
if (atIndex < 1 || atIndex >= options.PlusAddressedEmail.Length - 1)
|
||||
{
|
||||
return options.PlusAddressedEmail;
|
||||
}
|
||||
|
||||
if (options.PlusAddressedEmailType == UsernameEmailType.Random)
|
||||
{
|
||||
var randomString = await RandomStringAsync(8);
|
||||
return options.PlusAddressedEmail.Insert(atIndex, $"+{randomString}");
|
||||
}
|
||||
else
|
||||
{
|
||||
return options.PlusAddressedEmail.Insert(atIndex, $"+{options.EmailWebsite}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GenerateCatchAllAsync(UsernameGenerationOptions options)
|
||||
{
|
||||
var catchAllEmailDomain = options.CatchAllEmailDomain;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(catchAllEmailDomain))
|
||||
{
|
||||
return Constants.DefaultUsernameGenerated;
|
||||
}
|
||||
|
||||
if (options.CatchAllEmailType == UsernameEmailType.Random)
|
||||
{
|
||||
var randomString = await RandomStringAsync(8);
|
||||
return string.Format(CATCH_ALL_EMAIL_DOMAIN_FORMAT, randomString, catchAllEmailDomain);
|
||||
}
|
||||
|
||||
return string.Format(CATCH_ALL_EMAIL_DOMAIN_FORMAT, options.EmailWebsite, catchAllEmailDomain);
|
||||
}
|
||||
|
||||
private async Task<string> GenerateForwardedEmailAliasAsync(UsernameGenerationOptions options)
|
||||
{
|
||||
switch (options.ServiceType)
|
||||
{
|
||||
case ForwardedEmailServiceType.AnonAddy:
|
||||
if (string.IsNullOrWhiteSpace(options.AnonAddyApiAccessToken) || string.IsNullOrWhiteSpace(options.AnonAddyDomainName))
|
||||
{
|
||||
return Constants.DefaultUsernameGenerated;
|
||||
}
|
||||
return await _apiService.GetUsernameFromAsync(ForwardedEmailServiceType.AnonAddy,
|
||||
new UsernameGeneratorConfig()
|
||||
{
|
||||
ApiToken = options.AnonAddyApiAccessToken,
|
||||
Domain = options.AnonAddyDomainName,
|
||||
Url = "https://app.anonaddy.com/api/v1/aliases"
|
||||
});
|
||||
|
||||
case ForwardedEmailServiceType.FirefoxRelay:
|
||||
if (string.IsNullOrWhiteSpace(options.FirefoxRelayApiAccessToken))
|
||||
{
|
||||
return Constants.DefaultUsernameGenerated;
|
||||
}
|
||||
return await _apiService.GetUsernameFromAsync(ForwardedEmailServiceType.FirefoxRelay,
|
||||
new UsernameGeneratorConfig()
|
||||
{
|
||||
ApiToken = options.FirefoxRelayApiAccessToken,
|
||||
Url = "https://relay.firefox.com/api/v1/relayaddresses/"
|
||||
});
|
||||
|
||||
case ForwardedEmailServiceType.SimpleLogin:
|
||||
if (string.IsNullOrWhiteSpace(options.SimpleLoginApiKey))
|
||||
{
|
||||
return Constants.DefaultUsernameGenerated;
|
||||
}
|
||||
return await _apiService.GetUsernameFromAsync(ForwardedEmailServiceType.SimpleLogin,
|
||||
new UsernameGeneratorConfig()
|
||||
{
|
||||
ApiToken = options.SimpleLoginApiKey,
|
||||
Url = "https://app.simplelogin.io/api/alias/random/new"
|
||||
});
|
||||
default:
|
||||
_logger.Value.Error($"Error UsernameGenerationService: ForwardedEmailServiceType {options.ServiceType} not implemented.");
|
||||
return Constants.DefaultUsernameGenerated;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> RandomStringAsync(int length)
|
||||
{
|
||||
var str = "";
|
||||
var charSet = "abcdefghijklmnopqrstuvwxyz1234567890";
|
||||
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
var randomCharIndex = await _cryptoService.RandomNumberAsync(0, charSet.Length - 1);
|
||||
str += charSet[randomCharIndex];
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
private string Capitalize(string str)
|
||||
{
|
||||
return char.ToUpper(str[0]) + str.Substring(1);
|
||||
}
|
||||
|
||||
private async Task<string> AppendRandomNumberToRandomWordAsync(string word)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(word))
|
||||
{
|
||||
return word;
|
||||
}
|
||||
|
||||
var randomNumber = await _cryptoService.RandomNumberAsync(1, 9999);
|
||||
|
||||
return word + randomNumber.ToString("0000");
|
||||
}
|
||||
}
|
||||
}
|
@ -84,6 +84,7 @@ namespace Bit.Core.Utilities
|
||||
var eventService = new EventService(apiService, stateService, organizationService, cipherService);
|
||||
var userVerificationService = new UserVerificationService(apiService, platformUtilsService, i18nService,
|
||||
cryptoService);
|
||||
var usernameGenerationService = new UsernameGenerationService(cryptoService, apiService, stateService);
|
||||
|
||||
Register<ITokenService>("tokenService", tokenService);
|
||||
Register<IApiService>("apiService", apiService);
|
||||
@ -107,6 +108,7 @@ namespace Bit.Core.Utilities
|
||||
Register<IEventService>("eventService", eventService);
|
||||
Register<IKeyConnectorService>("keyConnectorService", keyConnectorService);
|
||||
Register<IUserVerificationService>("userVerificationService", userVerificationService);
|
||||
Register<IUsernameGenerationService>(usernameGenerationService);
|
||||
}
|
||||
|
||||
public static void Register<T>(string serviceName, T obj)
|
||||
|
Loading…
x
Reference in New Issue
Block a user