[EC-528] Refactor Custom Fields into separate components (#1662)

* Refactored CustomFields to stop using RepeaterView and use BindableLayout and divided the different types on different files and added a factory to create them

* Fix formatting
This commit is contained in:
Federico Maccaroni 2022-09-09 15:58:11 -03:00 committed by GitHub
parent 2016eadb0d
commit b7048de2a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 913 additions and 450 deletions

View File

@ -48,6 +48,7 @@ namespace Bit.Droid
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
ServiceContainer.Init(deviceActionService.DeviceUserAgent, Constants.ClearCiphersCacheKey,
Constants.AndroidAllClearCipherCacheKeys);
InitializeAppSetup();
// TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged
var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner(
@ -193,5 +194,12 @@ namespace Bit.Droid
{
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
}
private void InitializeAppSetup()
{
var appSetup = new AppSetup();
appSetup.InitializeServicesLastChance();
ServiceContainer.Register<IAppSetup>("appSetup", appSetup);
}
}
}

View File

@ -21,6 +21,7 @@
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2478" />
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="1.3.0" />
</ItemGroup>
<ItemGroup>
@ -127,6 +128,12 @@
<ItemGroup>
<Folder Include="Resources\" />
<Folder Include="Behaviors\" />
<Folder Include="Lists\" />
<Folder Include="Lists\ItemLayouts\" />
<Folder Include="Lists\DataTemplateSelectors\" />
<Folder Include="Lists\ItemLayouts\CustomFields\" />
<Folder Include="Lists\ItemViewModels\" />
<Folder Include="Lists\ItemViewModels\CustomFields\" />
<Folder Include="Controls\AccountSwitchingOverlay\" />
<Folder Include="Utilities\AccountManagement\" />
<Folder Include="Controls\DateTime\" />
@ -412,6 +419,12 @@
<ItemGroup>
<None Remove="Behaviors\" />
<None Remove="Xamarin.CommunityToolkit" />
<None Remove="Lists\" />
<None Remove="Lists\DataTemplates\" />
<None Remove="Lists\DataTemplateSelectors\" />
<None Remove="Lists\DataTemplates\CustomFields\" />
<None Remove="Lists\ItemViewModels\" />
<None Remove="Lists\ItemViewModels\CustomFields\" />
<None Remove="Controls\AccountSwitchingOverlay\" />
<None Remove="Utilities\AccountManagement\" />
<None Remove="Controls\DateTime\" />

View File

@ -1,9 +1,11 @@
using System.Collections;
using System;
using System.Collections;
using System.Collections.Specialized;
using Xamarin.Forms;
namespace Bit.App.Controls
{
[Obsolete]
public class RepeaterView : StackLayout
{
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(

View File

@ -0,0 +1,28 @@
using Bit.App.Lists.ItemViewModels.CustomFields;
using Xamarin.Forms;
namespace Bit.App.Lists.DataTemplateSelectors
{
public class CustomFieldItemTemplateSelector : DataTemplateSelector
{
public DataTemplate TextTemplate { get; set; }
public DataTemplate BooleanTemplate { get; set; }
public DataTemplate LinkedTemplate { get; set; }
public DataTemplate HiddenTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
switch (item)
{
case BooleanCustomFieldItemViewModel _:
return BooleanTemplate;
case LinkedCustomFieldItemViewModel _:
return LinkedTemplate;
case HiddenCustomFieldItemViewModel _:
return HiddenTemplate;
default:
return TextTemplate;
}
}
}
}

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" ?>
<StackLayout
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.BooleanCustomFieldItemLayout"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="cfvm:BooleanCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
<Label
Text="{Binding Field.Name, Mode=OneWay}"
IsVisible="{Binding IsEditing}"
StyleClass="box-value"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2" />
<controls:IconLabel
Text="{Binding BooleanValue, Mode=OneWay, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Checkbox}}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
Margin="0, 5, 0, 0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
<Switch
IsToggled="{Binding BooleanValue}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" />
</StackLayout>

View File

@ -0,0 +1,12 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class BooleanCustomFieldItemLayout : StackLayout
{
public BooleanCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.HiddenCustomFieldItemLayout"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="cfvm:HiddenCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<StackLayout
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing, Converter={StaticResource inverseBool}}">
<controls:MonoLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue}" />
<controls:MonoLabel
Text="{Binding Field.MaskedValue, Mode=OneWay}"
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
</StackLayout>
<controls:MonoEntry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing}"
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding ShowViewHidden}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False">
<Entry.Keyboard>
<Keyboard x:FactoryMethod="Create">
<x:Arguments>
<KeyboardFlags>None</KeyboardFlags>
</x:Arguments>
</Keyboard>
</Entry.Keyboard>
</controls:MonoEntry>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowHiddenValue, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleHiddenValueCommand}"
IsVisible="{Binding ShowViewHidden}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyFieldCommand}"
IsVisible="{Binding ShowCopyButton}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Copy}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
</StackLayout>

View File

@ -0,0 +1,12 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class HiddenCustomFieldItemLayout : StackLayout
{
public HiddenCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.LinkedCustomFieldItemLayout"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="cfvm:LinkedCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<controls:IconLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
<StackLayout
StyleClass="box-row, box-row-input"
IsVisible="{Binding IsEditing}">
<Picker
x:Name="_linkedFieldOptionPicker"
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
ItemDisplayBinding="{Binding Key}"
StyleClass="box-value" />
</StackLayout>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
</StackLayout>

View File

@ -0,0 +1,12 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class LinkedCustomFieldItemLayout : StackLayout
{
public LinkedCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.TextCustomFieldItemLayout"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="cfvm:TextCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
<Entry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyFieldCommand}"
IsVisible="{Binding ShowCopyButton}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Copy}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
</StackLayout>

View File

@ -0,0 +1,12 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class TextCustomFieldItemLayout : StackLayout
{
public TextCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,49 @@
using System.Windows.Input;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public abstract class BaseCustomFieldItemViewModel : ExtendedViewModel, ICustomFieldItemViewModel
{
protected FieldView _field;
protected bool _isEditing;
private string[] _additionalFieldProperties = new string[]
{
nameof(ValueText),
nameof(ShowCopyButton)
};
public BaseCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand)
{
_field = field;
_isEditing = isEditing;
FieldOptionsCommand = new Command(() => fieldOptionsCommand?.Execute(this));
}
public FieldView Field
{
get => _field;
set => SetProperty(ref _field, value,
additionalPropertyNames: new string[]
{
nameof(ValueText),
nameof(ShowCopyButton),
});
}
public bool IsEditing => _isEditing;
public virtual bool ShowCopyButton => false;
public virtual string ValueText => _field.Value;
public ICommand FieldOptionsCommand { get; }
public void TriggerFieldChanged()
{
TriggerPropertyChanged(nameof(Field), _additionalFieldProperties);
}
}
}

View File

@ -0,0 +1,23 @@
using System.Windows.Input;
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class BooleanCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
public BooleanCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand)
: base(field, isEditing, fieldOptionsCommand)
{
}
public bool BooleanValue
{
get => bool.TryParse(Field.Value, out var boolVal) && boolVal;
set
{
Field.Value = value.ToString().ToLower();
TriggerPropertyChanged(nameof(BooleanValue));
}
}
}
}

View File

@ -0,0 +1,53 @@
using System;
using System.Windows.Input;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public interface ICustomFieldItemFactory
{
ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field,
bool isEditing,
CipherView cipher,
IPasswordPromptable passwordPromptable,
ICommand copyFieldCommand,
ICommand fieldOptionsCommand);
}
public class CustomFieldItemFactory : ICustomFieldItemFactory
{
readonly II18nService _i18nService;
readonly IEventService _eventService;
public CustomFieldItemFactory(II18nService i18nService, IEventService eventService)
{
_i18nService = i18nService;
_eventService = eventService;
}
public ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field,
bool isEditing,
CipherView cipher,
IPasswordPromptable passwordPromptable,
ICommand copyFieldCommand,
ICommand fieldOptionsCommand)
{
switch (field.Type)
{
case FieldType.Text:
return new TextCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, copyFieldCommand);
case FieldType.Boolean:
return new BooleanCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand);
case FieldType.Hidden:
return new HiddenCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, passwordPromptable, _eventService, copyFieldCommand);
case FieldType.Linked:
return new LinkedCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, _i18nService);
default:
throw new NotImplementedException("There is no custom field item for field type " + field.Type);
}
}
}
}

View File

@ -0,0 +1,70 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class HiddenCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
private readonly CipherView _cipher;
private readonly IPasswordPromptable _passwordPromptable;
private readonly IEventService _eventService;
private bool _showHiddenValue;
public HiddenCustomFieldItemViewModel(FieldView field,
bool isEditing,
ICommand fieldOptionsCommand,
CipherView cipher,
IPasswordPromptable passwordPromptable,
IEventService eventService,
ICommand copyFieldCommand)
: base(field, isEditing, fieldOptionsCommand)
{
_cipher = cipher;
_passwordPromptable = passwordPromptable;
_eventService = eventService;
CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field));
ToggleHiddenValueCommand = new AsyncCommand(ToggleHiddenValueAsync, (Func<bool>)null, ex =>
{
#if !FDROID
Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
#endif
});
}
public ICommand CopyFieldCommand { get; }
public ICommand ToggleHiddenValueCommand { get; set; }
public bool ShowHiddenValue
{
get => _showHiddenValue;
set => SetProperty(ref _showHiddenValue, value);
}
public bool ShowViewHidden => _cipher.ViewPassword || (_isEditing && _field.NewField);
public override bool ShowCopyButton => !_isEditing && _cipher.ViewPassword && !string.IsNullOrWhiteSpace(Field.Value);
public async Task ToggleHiddenValueAsync()
{
if (!_isEditing && !await _passwordPromptable.PromptPasswordAsync())
{
return;
}
ShowHiddenValue = !ShowHiddenValue;
if (ShowHiddenValue && (!_isEditing || _cipher?.Id != null))
{
await _eventService.CollectAsync(
Core.Enums.EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
}
}
}
}

View File

@ -0,0 +1,13 @@
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public interface ICustomFieldItemViewModel
{
FieldView Field { get; set; }
bool ShowCopyButton { get; }
void TriggerFieldChanged();
}
}

View File

@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class LinkedCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
private readonly CipherView _cipher;
private readonly II18nService _i18nService;
private int _linkedFieldOptionSelectedIndex;
public LinkedCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, CipherView cipher, II18nService i18nService)
: base(field, isEditing, fieldOptionsCommand)
{
_cipher = cipher;
_i18nService = i18nService;
LinkedFieldOptionSelectedIndex = Field.LinkedId.HasValue
? LinkedFieldOptions.FindIndex(lfo => lfo.Value == Field.LinkedId.Value)
: 0;
if (isEditing && Field.LinkedId is null)
{
field.LinkedId = LinkedFieldOptions[0].Value;
}
}
public override string ValueText
{
get
{
var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault());
return $"{BitwardenIcons.Link} {_i18nService.T(i18nKey)}";
}
}
public int LinkedFieldOptionSelectedIndex
{
get => _linkedFieldOptionSelectedIndex;
set
{
if (SetProperty(ref _linkedFieldOptionSelectedIndex, value))
{
LinkedFieldValueChanged();
}
}
}
public List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions
{
get => _cipher.LinkedFieldOptions
.Select(kvp => new KeyValuePair<string, LinkedIdType>(_i18nService.T(kvp.Key), kvp.Value))
.ToList();
}
private void LinkedFieldValueChanged()
{
if (Field != null && LinkedFieldOptionSelectedIndex > -1)
{
Field.LinkedId = LinkedFieldOptions.Find(lfo =>
lfo.Value == LinkedFieldOptions[LinkedFieldOptionSelectedIndex].Value).Value;
}
}
}
}

View File

@ -0,0 +1,19 @@
using System.Windows.Input;
using Bit.Core.Models.View;
using Xamarin.Forms;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class TextCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
public TextCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, ICommand copyFieldCommand)
: base(field, isEditing, fieldOptionsCommand)
{
CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field));
}
public override bool ShowCopyButton => !_isEditing && !string.IsNullOrWhiteSpace(Field.Value);
public ICommand CopyFieldCommand { get; }
}
}

View File

@ -73,4 +73,3 @@ namespace Bit.App.Pages
}
}
}

View File

@ -9,6 +9,8 @@
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:dts="clr-namespace:Bit.App.Lists.DataTemplateSelectors"
xmlns:il="clr-namespace:Bit.App.Lists.ItemLayouts.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:CipherAddEditPageViewModel"
x:Name="_page"
@ -53,6 +55,25 @@
IsDestructive="True"
x:Name="_deleteItem"
x:Key="deleteItem" />
<DataTemplate x:Key="TextCustomFieldDataTemplate">
<il:TextCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="BooleanCustomFieldDataTemplate">
<il:BooleanCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="HiddenCustomFieldDataTemplate">
<il:HiddenCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="LinkedCustomFieldDataTemplate">
<il:LinkedCustomFieldItemLayout />
</DataTemplate>
<dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector"
TextTemplate="{StaticResource TextCustomFieldDataTemplate}"
BooleanTemplate="{StaticResource BooleanCustomFieldDataTemplate}"
HiddenTemplate="{StaticResource HiddenCustomFieldDataTemplate}"
LinkedTemplate="{StaticResource LinkedCustomFieldDataTemplate}"/>
</ResourceDictionary>
</ContentPage.Resources>
@ -647,101 +668,10 @@
<Label Text="{u:I18n CustomFields, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Fields}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:CipherAddEditPageFieldViewModel">
<StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row, box-row-input">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
IsVisible="{Binding IsBooleanType, Mode=OneWay, Converter={StaticResource inverseBool}}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding Field.Name, Mode=OneWay}"
IsVisible="{Binding IsBooleanType, Mode=OneWay}"
StyleClass="box-value"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2" />
<Entry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsTextType}" />
<controls:MonoEntry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsHiddenType}"
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding ShowViewHidden}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False">
<Entry.Keyboard>
<Keyboard x:FactoryMethod="Create">
<x:Arguments>
<KeyboardFlags>None</KeyboardFlags>
</x:Arguments>
</Keyboard>
</Entry.Keyboard>
</controls:MonoEntry>
<StackLayout
StyleClass="box-row, box-row-input"
IsVisible="{Binding IsLinkedType}">
<Picker
x:Name="_linkedFieldOptionPicker"
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
ItemDisplayBinding="{Binding Key}"
StyleClass="box-value" />
</StackLayout>
<Switch
IsToggled="{Binding BooleanValue}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
IsVisible="{Binding IsBooleanType}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowHiddenValueIcon}"
Command="{Binding ToggleHiddenValueCommand}"
IsVisible="{Binding ShowViewHidden}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding BindingContext.FieldOptionsCommand, Source={x:Reference _page}}"
CommandParameter="{Binding .}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsBooleanType}" />
</StackLayout>
</DataTemplate>
</controls:RepeaterView.ItemTemplate>
</controls:RepeaterView>
<StackLayout
Spacing="0"
BindableLayout.ItemsSource="{Binding Fields}"
BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}" />
<Button Text="{u:I18n NewCustomField}" StyleClass="box-button-row"
Clicked="NewField_Clicked"></Button>
</StackLayout>

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Lists.ItemViewModels.CustomFields;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.Core;
@ -25,6 +26,7 @@ namespace Bit.App.Pages
private readonly IMessagingService _messagingService;
private readonly IEventService _eventService;
private readonly IPolicyService _policyService;
private readonly ICustomFieldItemFactory _customFieldItemFactory;
private readonly IClipboardService _clipboardService;
private bool _showNotesSeparator;
@ -74,6 +76,7 @@ namespace Bit.App.Pages
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
GeneratePasswordCommand = new Command(GeneratePassword);
@ -81,12 +84,12 @@ namespace Bit.App.Pages
ToggleCardNumberCommand = new Command(ToggleCardNumber);
ToggleCardCodeCommand = new Command(ToggleCardCode);
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
FieldOptionsCommand = new Command<CipherAddEditPageFieldViewModel>(FieldOptions);
FieldOptionsCommand = new Command<ICustomFieldItemViewModel>(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>();
Fields = new ExtendedObservableCollection<ICustomFieldItemViewModel>();
Collections = new ExtendedObservableCollection<CollectionViewModel>();
AllowPersonal = true;
@ -161,7 +164,7 @@ namespace Bit.App.Pages
public List<KeyValuePair<string, string>> FolderOptions { get; set; }
public List<KeyValuePair<string, string>> OwnershipOptions { get; set; }
public ExtendedObservableCollection<LoginUriView> Uris { get; set; }
public ExtendedObservableCollection<CipherAddEditPageFieldViewModel> Fields { get; set; }
public ExtendedObservableCollection<ICustomFieldItemViewModel> Fields { get; set; }
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
public int TypeSelectedIndex
@ -414,7 +417,7 @@ namespace Bit.App.Pages
}
if (Cipher.Fields != null)
{
Fields.ResetWithRange(Cipher.Fields?.Select(f => new CipherAddEditPageFieldViewModel(Cipher, f)));
Fields.ResetWithRange(Cipher.Fields?.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, true, Cipher, null, null, FieldOptionsCommand)));
}
}
@ -656,7 +659,7 @@ namespace Bit.App.Pages
Uris.Add(new LoginUriView());
}
public async void FieldOptions(CipherAddEditPageFieldViewModel field)
public async void FieldOptions(ICustomFieldItemViewModel field)
{
if (!(Page as CipherAddEditPage).DoOnce())
{
@ -718,15 +721,15 @@ namespace Bit.App.Pages
}
if (Fields == null)
{
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
Fields = new ExtendedObservableCollection<ICustomFieldItemViewModel>();
}
var type = fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key;
Fields.Add(new CipherAddEditPageFieldViewModel(Cipher, new FieldView
Fields.Add(_customFieldItemFactory.CreateCustomFieldItem(new FieldView
{
Type = type,
Name = string.IsNullOrWhiteSpace(name) ? null : name,
NewField = true,
}));
}, true, Cipher, null, null, FieldOptionsCommand));
}
}
@ -788,7 +791,7 @@ namespace Bit.App.Pages
TriggerCipherChanged();
// Linked Custom Fields only apply to a specific item type
foreach (var field in Fields.Where(f => f.IsLinkedType).ToList())
foreach (var field in Fields.OfType<LinkedCustomFieldItemViewModel>().ToList())
{
Fields.Remove(field);
}
@ -871,113 +874,4 @@ namespace Bit.App.Pages
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
}
public class CipherAddEditPageFieldViewModel : ExtendedViewModel
{
private II18nService _i18nService;
private FieldView _field;
private CipherView _cipher;
private bool _showHiddenValue;
private bool _booleanValue;
private int _linkedFieldOptionSelectedIndex;
private string[] _additionalFieldProperties = new string[]
{
nameof(IsBooleanType),
nameof(IsHiddenType),
nameof(IsTextType),
nameof(IsLinkedType),
};
public CipherAddEditPageFieldViewModel(CipherView cipher, FieldView field)
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_cipher = cipher;
Field = field;
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
BooleanValue = IsBooleanType && field.Value == "true";
LinkedFieldOptionSelectedIndex = !Field.LinkedId.HasValue ? 0 :
LinkedFieldOptions.FindIndex(lfo => lfo.Value == Field.LinkedId.Value);
}
public FieldView Field
{
get => _field;
set => SetProperty(ref _field, value, additionalPropertyNames: _additionalFieldProperties);
}
public bool ShowHiddenValue
{
get => _showHiddenValue;
set => SetProperty(ref _showHiddenValue, value,
additionalPropertyNames: new string[]
{
nameof(ShowHiddenValueIcon)
});
}
public bool BooleanValue
{
get => _booleanValue;
set
{
SetProperty(ref _booleanValue, value);
if (IsBooleanType)
{
Field.Value = value ? "true" : "false";
}
}
}
public int LinkedFieldOptionSelectedIndex
{
get => _linkedFieldOptionSelectedIndex;
set
{
if (SetProperty(ref _linkedFieldOptionSelectedIndex, value))
{
LinkedFieldValueChanged();
}
}
}
public List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions
{
get => _cipher.LinkedFieldOptions?
.Select(kvp => new KeyValuePair<string, LinkedIdType>(_i18nService.T(kvp.Key), kvp.Value))
.ToList();
}
public Command ToggleHiddenValueCommand { get; set; }
public string ShowHiddenValueIcon => _showHiddenValue ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public bool IsTextType => _field.Type == FieldType.Text;
public bool IsBooleanType => _field.Type == FieldType.Boolean;
public bool IsHiddenType => _field.Type == FieldType.Hidden;
public bool IsLinkedType => _field.Type == FieldType.Linked;
public bool ShowViewHidden => IsHiddenType && (_cipher.ViewPassword || _field.NewField);
public void ToggleHiddenValue()
{
ShowHiddenValue = !ShowHiddenValue;
if (ShowHiddenValue && _cipher?.Id != null)
{
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
var task = eventService.CollectAsync(EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
}
}
public void TriggerFieldChanged()
{
TriggerPropertyChanged(nameof(Field), _additionalFieldProperties);
}
private void LinkedFieldValueChanged()
{
if (Field != null && LinkedFieldOptionSelectedIndex > -1)
{
Field.LinkedId = LinkedFieldOptions.Find(lfo =>
lfo.Value == LinkedFieldOptions[LinkedFieldOptionSelectedIndex].Value).Value;
}
}
}
}

View File

@ -8,6 +8,8 @@
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
xmlns:dts="clr-namespace:Bit.App.Lists.DataTemplateSelectors"
xmlns:il="clr-namespace:Bit.App.Lists.ItemLayouts.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:CipherDetailsPageViewModel"
x:Name="_page"
@ -46,6 +48,25 @@
<ToolbarItem Text="{u:I18n Clone}" Clicked="Clone_Clicked" Order="Secondary"
x:Name="_cloneItem" x:Key="cloneItem" />
<DataTemplate x:Key="TextCustomFieldDataTemplate">
<il:TextCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="BooleanCustomFieldDataTemplate">
<il:BooleanCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="HiddenCustomFieldDataTemplate">
<il:HiddenCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="LinkedCustomFieldDataTemplate">
<il:LinkedCustomFieldItemLayout />
</DataTemplate>
<dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector"
TextTemplate="{StaticResource TextCustomFieldDataTemplate}"
BooleanTemplate="{StaticResource BooleanCustomFieldDataTemplate}"
HiddenTemplate="{StaticResource HiddenCustomFieldDataTemplate}"
LinkedTemplate="{StaticResource LinkedCustomFieldDataTemplate}"/>
<ScrollView x:Key="scrollView" x:Name="_scrollView">
<StackLayout Spacing="20" x:Name="_mainLayout">
<StackLayout StyleClass="box">
@ -559,85 +580,10 @@
<Label Text="{u:I18n CustomFields, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Fields}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:CipherDetailsPageFieldViewModel">
<StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsTextType}" />
<controls:IconLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsLinkedType}" />
<controls:IconLabel
Text="{Binding ValueText, Mode=OneWay}"
AutomationProperties.IsInAccessibleTree="true"
AutomationProperties.Name="{Binding ValueAccessibilityText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsBooleanType}"
Margin="0, 5, 0, 0" />
<StackLayout IsVisible="{Binding IsHiddenType}"
Grid.Row="1"
Grid.Column="0">
<controls:MonoLabel
Text="{Binding ColoredHiddenValue, Mode=OneWay}"
StyleClass="box-value, text-html"
IsVisible="{Binding ShowHiddenValue}" />
<controls:MonoLabel
Text="{Binding Field.MaskedValue, Mode=OneWay}"
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
</StackLayout>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowHiddenValueIcon}"
Command="{Binding ToggleHiddenValueCommand}"
IsVisible="{Binding ShowViewHidden}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding BindingContext.CopyFieldCommand, Source={x:Reference _page}}"
CommandParameter="{Binding Field}"
IsVisible="{Binding ShowCopyButton}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Copy}" />
</Grid>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
</DataTemplate>
</controls:RepeaterView.ItemTemplate>
</controls:RepeaterView>
<StackLayout
Spacing="0"
BindableLayout.ItemsSource="{Binding Fields}"
BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}" />
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAttachments}">
<StackLayout StyleClass="box-row-header">

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Lists.ItemViewModels.CustomFields;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
@ -18,7 +19,7 @@ using Xamarin.Forms;
namespace Bit.App.Pages
{
public class CipherDetailsPageViewModel : BaseCipherViewModel
public class CipherDetailsPageViewModel : BaseCipherViewModel, IPasswordPromptable
{
private readonly ICipherService _cipherService;
private readonly IStateService _stateService;
@ -28,9 +29,10 @@ namespace Bit.App.Pages
private readonly IEventService _eventService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly ILocalizeService _localizeService;
private readonly ICustomFieldItemFactory _customFieldItemFactory;
private readonly IClipboardService _clipboardService;
private List<CipherDetailsPageFieldViewModel> _fields;
private List<ICustomFieldItemViewModel> _fields;
private bool _canAccessPremium;
private bool _showPassword;
private bool _showCardNumber;
@ -58,6 +60,7 @@ namespace Bit.App.Pages
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
CopyCommand = new AsyncCommand<string>((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
@ -99,7 +102,7 @@ namespace Bit.App.Pages
nameof(CanEdit),
nameof(ShowUpgradePremiumTotpText)
};
public List<CipherDetailsPageFieldViewModel> Fields
public List<ICustomFieldItemViewModel> Fields
{
get => _fields;
set => SetProperty(ref _fields, value);
@ -252,7 +255,10 @@ namespace Bit.App.Pages
}
Cipher = await cipher.DecryptAsync();
CanAccessPremium = await _stateService.CanAccessPremiumAsync();
Fields = Cipher.Fields?.Select(f => new CipherDetailsPageFieldViewModel(this, Cipher, f)).ToList();
Fields = Cipher.Fields?
.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, false, Cipher, this, CopyFieldCommand, null))
.ToList();
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
(Cipher.OrganizationUseTotp || CanAccessPremium))
@ -665,7 +671,7 @@ namespace Bit.App.Pages
}
}
internal async Task<bool> PromptPasswordAsync()
public async Task<bool> PromptPasswordAsync()
{
if (Cipher.Reprompt == CipherRepromptType.None || _passwordReprompted)
{
@ -675,110 +681,4 @@ namespace Bit.App.Pages
return _passwordReprompted = await _passwordRepromptService.ShowPasswordPromptAsync();
}
}
public class CipherDetailsPageFieldViewModel : ExtendedViewModel
{
private II18nService _i18nService;
private CipherDetailsPageViewModel _vm;
private FieldView _field;
private CipherView _cipher;
private bool _showHiddenValue;
public CipherDetailsPageFieldViewModel(CipherDetailsPageViewModel vm, CipherView cipher, FieldView field)
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_vm = vm;
_cipher = cipher;
Field = field;
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
}
public FieldView Field
{
get => _field;
set => SetProperty(ref _field, value,
additionalPropertyNames: new string[]
{
nameof(ValueText),
nameof(ValueAccessibilityText),
nameof(IsBooleanType),
nameof(IsHiddenType),
nameof(IsTextType),
nameof(ShowCopyButton),
});
}
public bool ShowHiddenValue
{
get => _showHiddenValue;
set => SetProperty(ref _showHiddenValue, value,
additionalPropertyNames: new string[]
{
nameof(ShowHiddenValueIcon)
});
}
public string ValueText
{
get
{
if (IsBooleanType)
{
return _field.BoolValue ? BitwardenIcons.CheckSquare : BitwardenIcons.Square;
}
else if (IsLinkedType)
{
var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault());
return BitwardenIcons.Link + _i18nService.T(i18nKey);
}
else
{
return _field.Value;
}
}
}
public string ValueAccessibilityText
{
get
{
if (IsBooleanType)
{
return _field.BoolValue ? AppResources.Enabled : AppResources.Disabled;
}
return ValueText;
}
}
public FormattedString ColoredHiddenValue => GeneratedValueFormatter.Format(_field.Value);
public Command ToggleHiddenValueCommand { get; set; }
public string ShowHiddenValueIcon => _showHiddenValue ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public bool IsTextType => _field.Type == Core.Enums.FieldType.Text;
public bool IsBooleanType => _field.Type == Core.Enums.FieldType.Boolean;
public bool IsHiddenType => _field.Type == Core.Enums.FieldType.Hidden;
public bool IsLinkedType => _field.Type == Core.Enums.FieldType.Linked;
public bool ShowViewHidden => IsHiddenType && _cipher.ViewPassword;
public bool ShowCopyButton => _field.Type != Core.Enums.FieldType.Boolean &&
!string.IsNullOrWhiteSpace(_field.Value) &&
!(IsHiddenType && !_cipher.ViewPassword) &&
_field.Type != FieldType.Linked;
public async void ToggleHiddenValue()
{
if (!await _vm.PromptPasswordAsync())
{
return;
}
ShowHiddenValue = !ShowHiddenValue;
if (ShowHiddenValue)
{
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
var task = eventService.CollectAsync(
Core.Enums.EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
using System;
using System.Globalization;
using Bit.Core;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Xamarin.Forms;
@ -11,54 +9,24 @@ namespace Bit.App.Utilities
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var cipher = value as CipherView;
return GetIcon(cipher);
if (value is CipherView cipher)
{
return cipher.GetIcon();
}
if (value is bool boolVal
&&
parameter is BooleanGlyphType boolGlyphType)
{
return IconGlyphExtensions.GetBooleanIconGlyph(boolVal, boolGlyphType);
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private string GetIcon(CipherView cipher)
{
string icon = null;
switch (cipher.Type)
{
case CipherType.Login:
icon = GetLoginIconGlyph(cipher);
break;
case CipherType.SecureNote:
icon = BitwardenIcons.StickyNote;
break;
case CipherType.Card:
icon = BitwardenIcons.CreditCard;
break;
case CipherType.Identity:
icon = BitwardenIcons.IdCard;
break;
default:
break;
}
return icon;
}
string GetLoginIconGlyph(CipherView cipher)
{
var icon = BitwardenIcons.Globe;
if (cipher.Login.Uri != null)
{
var hostnameUri = cipher.Login.Uri;
if (hostnameUri.StartsWith(Constants.AndroidAppProtocol))
{
icon = BitwardenIcons.Android;
}
else if (hostnameUri.StartsWith(Constants.iOSAppProtocol))
{
icon = BitwardenIcons.Apple;
}
}
return icon;
}
}
}

View File

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

View File

@ -199,6 +199,7 @@ namespace Bit.iOS.Core.Utilities
{
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
InitializeAppSetup();
// TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged
var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner(
ServiceContainer.Resolve<IApiService>("apiService"),
@ -229,5 +230,12 @@ namespace Bit.iOS.Core.Utilities
await postBootstrapFunc.Invoke();
}
}
private static void InitializeAppSetup()
{
var appSetup = new AppSetup();
appSetup.InitializeServicesLastChance();
ServiceContainer.Register<IAppSetup>("appSetup", appSetup);
}
}
}