diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 05e765da8..fe15ebb59 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -49,6 +49,7 @@ namespace Bit.Droid var deviceActionService = ServiceContainer.Resolve("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( @@ -203,5 +204,12 @@ namespace Bit.Droid { await ServiceContainer.Resolve("environmentService").SetUrlsFromStorageAsync(); } + + private void InitializeAppSetup() + { + var appSetup = new AppSetup(); + appSetup.InitializeServicesLastChance(); + ServiceContainer.Register("appSetup", appSetup); + } } } diff --git a/src/App/App.csproj b/src/App/App.csproj index 9e00bde3f..fa68d8c97 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -21,6 +21,7 @@ + @@ -127,6 +128,12 @@ + + + + + + @@ -413,6 +420,12 @@ + + + + + + diff --git a/src/App/Controls/RepeaterView.cs b/src/App/Controls/RepeaterView.cs index e5928e3e6..e463abd6f 100644 --- a/src/App/Controls/RepeaterView.cs +++ b/src/App/Controls/RepeaterView.cs @@ -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( diff --git a/src/App/Lists/DataTemplateSelectors/CustomFieldItemTemplateSelector.cs b/src/App/Lists/DataTemplateSelectors/CustomFieldItemTemplateSelector.cs new file mode 100644 index 000000000..a69363fe8 --- /dev/null +++ b/src/App/Lists/DataTemplateSelectors/CustomFieldItemTemplateSelector.cs @@ -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; + } + } + } +} diff --git a/src/App/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml b/src/App/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml new file mode 100644 index 000000000..c50cf53eb --- /dev/null +++ b/src/App/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml.cs b/src/App/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml.cs new file mode 100644 index 000000000..91df61b79 --- /dev/null +++ b/src/App/Lists/ItemLayouts/CustomFields/BooleanCustomFieldItemLayout.xaml.cs @@ -0,0 +1,12 @@ +using Xamarin.Forms; + +namespace Bit.App.Lists.ItemLayouts.CustomFields +{ + public partial class BooleanCustomFieldItemLayout : StackLayout + { + public BooleanCustomFieldItemLayout() + { + InitializeComponent(); + } + } +} diff --git a/src/App/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml b/src/App/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml new file mode 100644 index 000000000..59d05d14c --- /dev/null +++ b/src/App/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml.cs b/src/App/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml.cs new file mode 100644 index 000000000..e4222aa9f --- /dev/null +++ b/src/App/Lists/ItemLayouts/CustomFields/HiddenCustomFieldItemLayout.xaml.cs @@ -0,0 +1,12 @@ +using Xamarin.Forms; + +namespace Bit.App.Lists.ItemLayouts.CustomFields +{ + public partial class HiddenCustomFieldItemLayout : StackLayout + { + public HiddenCustomFieldItemLayout() + { + InitializeComponent(); + } + } +} diff --git a/src/App/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml b/src/App/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml new file mode 100644 index 000000000..f2cc6a91c --- /dev/null +++ b/src/App/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml.cs b/src/App/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml.cs new file mode 100644 index 000000000..2dfe4d880 --- /dev/null +++ b/src/App/Lists/ItemLayouts/CustomFields/LinkedCustomFieldItemLayout.xaml.cs @@ -0,0 +1,12 @@ +using Xamarin.Forms; + +namespace Bit.App.Lists.ItemLayouts.CustomFields +{ + public partial class LinkedCustomFieldItemLayout : StackLayout + { + public LinkedCustomFieldItemLayout() + { + InitializeComponent(); + } + } +} diff --git a/src/App/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml b/src/App/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml new file mode 100644 index 000000000..a5d3239b0 --- /dev/null +++ b/src/App/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml.cs b/src/App/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml.cs new file mode 100644 index 000000000..a31cd57c5 --- /dev/null +++ b/src/App/Lists/ItemLayouts/CustomFields/TextCustomFieldItemLayout.xaml.cs @@ -0,0 +1,12 @@ +using Xamarin.Forms; + +namespace Bit.App.Lists.ItemLayouts.CustomFields +{ + public partial class TextCustomFieldItemLayout : StackLayout + { + public TextCustomFieldItemLayout() + { + InitializeComponent(); + } + } +} diff --git a/src/App/Lists/ItemViewModels/CustomFields/BaseCustomFieldItemViewModel.cs b/src/App/Lists/ItemViewModels/CustomFields/BaseCustomFieldItemViewModel.cs new file mode 100644 index 000000000..138884eeb --- /dev/null +++ b/src/App/Lists/ItemViewModels/CustomFields/BaseCustomFieldItemViewModel.cs @@ -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); + } + } +} diff --git a/src/App/Lists/ItemViewModels/CustomFields/BooleanCustomFieldItemViewModel.cs b/src/App/Lists/ItemViewModels/CustomFields/BooleanCustomFieldItemViewModel.cs new file mode 100644 index 000000000..81b72f68c --- /dev/null +++ b/src/App/Lists/ItemViewModels/CustomFields/BooleanCustomFieldItemViewModel.cs @@ -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)); + } + } + } +} diff --git a/src/App/Lists/ItemViewModels/CustomFields/CustomFieldItemFactory.cs b/src/App/Lists/ItemViewModels/CustomFields/CustomFieldItemFactory.cs new file mode 100644 index 000000000..9987700e4 --- /dev/null +++ b/src/App/Lists/ItemViewModels/CustomFields/CustomFieldItemFactory.cs @@ -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); + } + } + } +} diff --git a/src/App/Lists/ItemViewModels/CustomFields/HiddenCustomFieldItemViewModel.cs b/src/App/Lists/ItemViewModels/CustomFields/HiddenCustomFieldItemViewModel.cs new file mode 100644 index 000000000..4f5e9f5f6 --- /dev/null +++ b/src/App/Lists/ItemViewModels/CustomFields/HiddenCustomFieldItemViewModel.cs @@ -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)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); + } + } + } +} diff --git a/src/App/Lists/ItemViewModels/CustomFields/ICustomFieldItemViewModel.cs b/src/App/Lists/ItemViewModels/CustomFields/ICustomFieldItemViewModel.cs new file mode 100644 index 000000000..c671fa2b8 --- /dev/null +++ b/src/App/Lists/ItemViewModels/CustomFields/ICustomFieldItemViewModel.cs @@ -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(); + } +} diff --git a/src/App/Lists/ItemViewModels/CustomFields/LinkedCustomFieldItemViewModel.cs b/src/App/Lists/ItemViewModels/CustomFields/LinkedCustomFieldItemViewModel.cs new file mode 100644 index 000000000..a07d1ca57 --- /dev/null +++ b/src/App/Lists/ItemViewModels/CustomFields/LinkedCustomFieldItemViewModel.cs @@ -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> LinkedFieldOptions + { + get => _cipher.LinkedFieldOptions + .Select(kvp => new KeyValuePair(_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; + } + } + } +} diff --git a/src/App/Lists/ItemViewModels/CustomFields/TextCustomFieldItemViewModel.cs b/src/App/Lists/ItemViewModels/CustomFields/TextCustomFieldItemViewModel.cs new file mode 100644 index 000000000..01ebe5440 --- /dev/null +++ b/src/App/Lists/ItemViewModels/CustomFields/TextCustomFieldItemViewModel.cs @@ -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; } + } +} diff --git a/src/App/Pages/Vault/BaseCipherViewModel.cs b/src/App/Pages/Vault/BaseCipherViewModel.cs index 070c52402..bd74befa5 100644 --- a/src/App/Pages/Vault/BaseCipherViewModel.cs +++ b/src/App/Pages/Vault/BaseCipherViewModel.cs @@ -73,4 +73,3 @@ namespace Bit.App.Pages } } } - diff --git a/src/App/Pages/Vault/CipherAddEditPage.xaml b/src/App/Pages/Vault/CipherAddEditPage.xaml index 4dc250856..bf32355cd 100644 --- a/src/App/Pages/Vault/CipherAddEditPage.xaml +++ b/src/App/Pages/Vault/CipherAddEditPage.xaml @@ -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" /> + + + + + + + + + + + + + + + @@ -647,101 +668,10 @@