From 505426cd6af6a7584b8c25788f543e18a1c438e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gon=C3=A7alves?= Date: Tue, 25 Oct 2022 21:05:15 +0100 Subject: [PATCH] [SG 547] Mobile username generator iOS.Extension UI changes (#2140) * [SG-547] - Added button to generate username when using iOS extension * [SG-547] - Missing changes from last commit * SG-547 - Added missing interface method * SG-547 - Added token renovation for iOS.Extension flow * SG-547 Replaced generate buttons for icons * SG-547 Removed unnecessary validation * SG-547 - Fixed PR comments * SG 547 - Missing file from last commit * SG-547 - Fixed PR comments * SG-547 - Renamed method --- src/Android/Services/DeviceActionService.cs | 6 ++ src/App/Abstractions/IDeviceActionService.cs | 1 + src/App/Pages/Generator/GeneratorPage.xaml | 2 +- src/App/Pages/Generator/GeneratorPage.xaml.cs | 14 +-- .../Pages/Generator/GeneratorPageViewModel.cs | 26 ++++- src/Core/Abstractions/ITokenService.cs | 1 + src/Core/Services/TokenService.cs | 5 + .../BaseLockPasswordViewController.cs | 11 +-- .../Controllers/LockPasswordViewController.cs | 10 +- .../Controllers/LoginAddViewController.cs | 77 +++++++++------ src/iOS.Core/Services/DeviceActionService.cs | 5 + src/iOS.Core/Views/FormEntryTableViewCell.cs | 96 +++++++++++++++---- 12 files changed, 179 insertions(+), 75 deletions(-) diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index 2ddee1245..c50558280 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -520,5 +520,11 @@ namespace Bit.Droid.Services intent.SetData(uri); Application.Context.StartActivity(intent); } + + public void CloseExtensionPopUp() + { + // only used by iOS + throw new NotImplementedException(); + } } } diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs index 8f4a19a34..70384100e 100644 --- a/src/App/Abstractions/IDeviceActionService.cs +++ b/src/App/Abstractions/IDeviceActionService.cs @@ -37,5 +37,6 @@ namespace Bit.App.Abstractions Task OnAccountSwitchCompleteAsync(); Task SetScreenCaptureAllowedAsync(); void OpenAppSettings(); + void CloseExtensionPopUp(); } } diff --git a/src/App/Pages/Generator/GeneratorPage.xaml b/src/App/Pages/Generator/GeneratorPage.xaml index 00cd35381..06f436759 100644 --- a/src/App/Pages/Generator/GeneratorPage.xaml +++ b/src/App/Pages/Generator/GeneratorPage.xaml @@ -21,7 +21,7 @@ - _selectAction; private readonly TabsPage _tabsPage; - public GeneratorPage(bool fromTabPage, Action selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false) + public GeneratorPage(bool fromTabPage, Action selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false, AppOptions appOptions = null) { _tabsPage = tabsPage; InitializeComponent(); - _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + _broadcasterService = ServiceContainer.Resolve(); _vm = BindingContext as GeneratorPageViewModel; _vm.Page = this; _fromTabPage = fromTabPage; @@ -31,6 +32,7 @@ namespace Bit.App.Pages _vm.IsUsername = isUsernameGenerator; _vm.EmailWebsite = emailWebsite; _vm.EditMode = editMode; + _vm.IosExtension = appOptions?.IosExtension ?? false; var isIos = Device.RuntimePlatform == Device.iOS; if (selectAction != null) { @@ -134,14 +136,6 @@ namespace Bit.App.Pages await _vm.SliderChangedAsync(); } - private async void Close_Clicked(object sender, EventArgs e) - { - if (DoOnce()) - { - await Navigation.PopModalAsync(); - } - } - public override async Task UpdateOnThemeChanged() { await base.UpdateOnThemeChanged(); diff --git a/src/App/Pages/Generator/GeneratorPageViewModel.cs b/src/App/Pages/Generator/GeneratorPageViewModel.cs index 6e51ef0d4..b5f103034 100644 --- a/src/App/Pages/Generator/GeneratorPageViewModel.cs +++ b/src/App/Pages/Generator/GeneratorPageViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using System.Windows.Input; +using Bit.App.Abstractions; using Bit.App.Resources; using Bit.App.Utilities; using Bit.Core; @@ -21,6 +22,7 @@ namespace Bit.App.Pages private readonly IClipboardService _clipboardService; private readonly IUsernameGenerationService _usernameGenerationService; private readonly ITokenService _tokenService; + private readonly IDeviceActionService _deviceActionService; readonly LazyResolve _logger = new LazyResolve("logger"); private PasswordGenerationOptions _options; @@ -59,6 +61,7 @@ namespace Bit.App.Pages _clipboardService = ServiceContainer.Resolve(); _usernameGenerationService = ServiceContainer.Resolve(); _tokenService = ServiceContainer.Resolve(); + _deviceActionService = ServiceContainer.Resolve(); PageTitle = AppResources.Generator; GeneratorTypeOptions = new List { @@ -89,8 +92,9 @@ namespace Bit.App.Pages 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); + ToggleForwardedEmailHiddenValueCommand = new AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false); + CopyCommand = new AsyncCommand(CopyAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false); + CloseCommand = new AsyncCommand(CloseAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false); } public List GeneratorTypeOptions { get; set; } @@ -104,6 +108,7 @@ namespace Bit.App.Pages public ICommand RegenerateUsernameCommand { get; set; } public ICommand ToggleForwardedEmailHiddenValueCommand { get; set; } public ICommand CopyCommand { get; set; } + public ICommand CloseCommand { get; set; } public string Password { @@ -140,6 +145,8 @@ namespace Bit.App.Pages set => SetProperty(ref _isUsername, value); } + public bool IosExtension { get; set; } + public bool ShowTypePicker { get => _showTypePicker; @@ -606,6 +613,7 @@ namespace Bit.App.Pages LoadFromOptions(); _usernameOptions = await _usernameGenerationService.GetOptionsAsync(); + await _tokenService.PrepareTokenForDecodingAsync(); _usernameOptions.PlusAddressedEmail = _tokenService.GetEmail(); _usernameOptions.EmailWebsite = EmailWebsite; _usernameOptions.CatchAllEmailType = _usernameOptions.PlusAddressedEmailType = string.IsNullOrWhiteSpace(EmailWebsite) || !EditMode ? UsernameEmailType.Random : UsernameEmailType.Website; @@ -681,6 +689,7 @@ namespace Bit.App.Pages return; } + _usernameOptions.EmailWebsite = EmailWebsite; await _usernameGenerationService.SaveOptionsAsync(_usernameOptions); if (regenerate && UsernameTypeSelected != UsernameType.ForwardedEmailAlias) @@ -729,6 +738,18 @@ namespace Bit.App.Pages } } + public async Task CloseAsync() + { + if (IosExtension) + { + _deviceActionService.CloseExtensionPopUp(); + } + else + { + await Page.Navigation.PopModalAsync(); + } + } + private void LoadFromOptions() { AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault(); @@ -765,6 +786,7 @@ namespace Bit.App.Pages TriggerPropertyChanged(nameof(PlusAddressedEmail)); TriggerPropertyChanged(nameof(GeneratorTypeSelected)); TriggerPropertyChanged(nameof(UsernameTypeDescriptionLabel)); + TriggerPropertyChanged(nameof(EmailWebsite)); } private void SetOptions() diff --git a/src/Core/Abstractions/ITokenService.cs b/src/Core/Abstractions/ITokenService.cs index 578c97d8f..123c60e8a 100644 --- a/src/Core/Abstractions/ITokenService.cs +++ b/src/Core/Abstractions/ITokenService.cs @@ -28,5 +28,6 @@ namespace Bit.Core.Abstractions Task SetTwoFactorTokenAsync(string token, string email); bool TokenNeedsRefresh(int minutes = 5); int TokenSecondsRemaining(); + Task PrepareTokenForDecodingAsync(); } } diff --git a/src/Core/Services/TokenService.cs b/src/Core/Services/TokenService.cs index 9edd32b22..30bbbe565 100644 --- a/src/Core/Services/TokenService.cs +++ b/src/Core/Services/TokenService.cs @@ -44,6 +44,11 @@ namespace Bit.Core.Services return _accessTokenForDecoding; } + public async Task PrepareTokenForDecodingAsync() + { + _accessTokenForDecoding = await _stateService.GetAccessTokenAsync(); + } + public async Task SetRefreshTokenAsync(string refreshToken) { await _stateService.SetRefreshTokenAsync(refreshToken, await SkipTokenStorage()); diff --git a/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs index 3a9f24ce4..98db64ad6 100644 --- a/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs @@ -5,7 +5,6 @@ using Bit.App.Models; using Bit.App.Pages; using Bit.App.Resources; using Bit.App.Utilities; -using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Domain; @@ -55,7 +54,7 @@ namespace Bit.iOS.Core.Controllers public abstract Action Cancel { get; } public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell( - AppResources.MasterPassword, useButton: true); + AppResources.MasterPassword, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.One); public string BiometricIntegrityKey { get; set; } @@ -161,13 +160,7 @@ namespace Bit.iOS.Core.Controllers { MasterPasswordCell.TextField.KeyboardType = UIKeyboardType.NumberPad; } - MasterPasswordCell.Button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f); - MasterPasswordCell.Button.SetTitle(BitwardenIcons.Eye, UIControlState.Normal); - MasterPasswordCell.Button.TouchUpInside += (sender, e) => - { - MasterPasswordCell.TextField.SecureTextEntry = !MasterPasswordCell.TextField.SecureTextEntry; - MasterPasswordCell.Button.SetTitle(MasterPasswordCell.TextField.SecureTextEntry ? BitwardenIcons.Eye : BitwardenIcons.EyeSlash, UIControlState.Normal); - }; + MasterPasswordCell.ConfigureToggleSecureTextCell(); } if (TableView != null) diff --git a/src/iOS.Core/Controllers/LockPasswordViewController.cs b/src/iOS.Core/Controllers/LockPasswordViewController.cs index 7f879d3bd..7a04759cf 100644 --- a/src/iOS.Core/Controllers/LockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/LockPasswordViewController.cs @@ -14,7 +14,6 @@ using Bit.Core.Enums; using Bit.App.Pages; using Bit.App.Models; using Xamarin.Forms; -using Bit.Core; namespace Bit.iOS.Core.Controllers { @@ -52,7 +51,7 @@ namespace Bit.iOS.Core.Controllers public abstract Action Cancel { get; } public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell( - AppResources.MasterPassword, useButton: true); + AppResources.MasterPassword, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.One); public string BiometricIntegrityKey { get; set; } @@ -155,12 +154,7 @@ namespace Bit.iOS.Core.Controllers { MasterPasswordCell.TextField.KeyboardType = UIKeyboardType.NumberPad; } - MasterPasswordCell.Button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f); - MasterPasswordCell.Button.SetTitle(BitwardenIcons.Eye, UIControlState.Normal); - MasterPasswordCell.Button.TouchUpInside += (sender, e) => { - MasterPasswordCell.TextField.SecureTextEntry = !MasterPasswordCell.TextField.SecureTextEntry; - MasterPasswordCell.Button.SetTitle(MasterPasswordCell.TextField.SecureTextEntry ? BitwardenIcons.Eye : BitwardenIcons.EyeSlash, UIControlState.Normal); - }; + MasterPasswordCell.ConfigureToggleSecureTextCell(); } TableView.RowHeight = UITableView.AutomaticDimension; diff --git a/src/iOS.Core/Controllers/LoginAddViewController.cs b/src/iOS.Core/Controllers/LoginAddViewController.cs index 2274aa37d..935f70b56 100644 --- a/src/iOS.Core/Controllers/LoginAddViewController.cs +++ b/src/iOS.Core/Controllers/LoginAddViewController.cs @@ -1,18 +1,23 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using AuthenticationServices; +using Bit.App.Models; +using Bit.App.Pages; using Bit.App.Resources; +using Bit.App.Utilities; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Bit.iOS.Core.Models; +using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Views; using Foundation; using UIKit; -using Bit.iOS.Core.Utilities; -using Bit.iOS.Core.Models; -using System.Threading.Tasks; -using AuthenticationServices; -using Bit.Core.Abstractions; -using Bit.Core.Models.View; -using Bit.Core.Utilities; -using Bit.Core.Exceptions; +using Xamarin.Forms; namespace Bit.iOS.Core.Controllers { @@ -29,10 +34,8 @@ namespace Bit.iOS.Core.Controllers public AppExtensionContext Context { get; set; } public FormEntryTableViewCell NameCell { get; set; } = new FormEntryTableViewCell(AppResources.Name); - public FormEntryTableViewCell UsernameCell { get; set; } = new FormEntryTableViewCell(AppResources.Username); - public FormEntryTableViewCell PasswordCell { get; set; } = new FormEntryTableViewCell(AppResources.Password); - public UITableViewCell GeneratePasswordCell { get; set; } = new ExtendedUITableViewCell( - UITableViewCellStyle.Subtitle, "GeneratePasswordCell"); + public FormEntryTableViewCell UsernameCell { get; set; } = new FormEntryTableViewCell(AppResources.Username, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.One); + public FormEntryTableViewCell PasswordCell { get; set; } = new FormEntryTableViewCell(AppResources.Password, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.Two); public FormEntryTableViewCell UriCell { get; set; } = new FormEntryTableViewCell(AppResources.URI); public SwitchTableViewCell FavoriteCell { get; set; } = new SwitchTableViewCell(AppResources.Favorite); public FormEntryTableViewCell NotesCell { get; set; } = new FormEntryTableViewCell( @@ -67,6 +70,12 @@ namespace Bit.iOS.Core.Controllers UsernameCell.TextField.AutocorrectionType = UITextAutocorrectionType.No; UsernameCell.TextField.SpellCheckingType = UITextSpellCheckingType.No; UsernameCell.TextField.ReturnKeyType = UIReturnKeyType.Next; + UsernameCell.Button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f); + UsernameCell.Button.SetTitle(BitwardenIcons.Generate, UIControlState.Normal); + UsernameCell.Button.TouchUpInside += (sender, e) => + { + LaunchUsernameGeneratorFlow(); + }; UsernameCell.TextField.ShouldReturn += (UITextField tf) => { PasswordCell.TextField.BecomeFirstResponder(); @@ -75,17 +84,20 @@ namespace Bit.iOS.Core.Controllers PasswordCell.TextField.SecureTextEntry = true; PasswordCell.TextField.ReturnKeyType = UIReturnKeyType.Next; + PasswordCell.Button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f); + PasswordCell.Button.SetTitle(BitwardenIcons.Generate, UIControlState.Normal); + PasswordCell.Button.TouchUpInside += (sender, e) => + { + PerformSegue("passwordGeneratorSegue", this); + }; + + PasswordCell.ConfigureToggleSecureTextCell(true); PasswordCell.TextField.ShouldReturn += (UITextField tf) => { UriCell.TextField.BecomeFirstResponder(); return true; }; - GeneratePasswordCell.TextLabel.Text = AppResources.GeneratePassword; - GeneratePasswordCell.TextLabel.TextColor = GeneratePasswordCell.TextLabel.TintColor = - ThemeHelpers.TextColor; - GeneratePasswordCell.Accessory = UITableViewCellAccessory.DisclosureIndicator; - UriCell.TextField.Text = Context?.UrlString ?? string.Empty; UriCell.TextField.KeyboardType = UIKeyboardType.Url; UriCell.TextField.ReturnKeyType = UIReturnKeyType.Next; @@ -210,6 +222,26 @@ namespace Bit.iOS.Core.Controllers AppResources.InternetConnectionRequiredMessage, AppResources.Ok); } + private void LaunchUsernameGeneratorFlow() + { + var appOptions = new AppOptions { IosExtension = true }; + var app = new App.App(appOptions); + + var generatorPage = new GeneratorPage(false, selectAction: async (username) => + { + UsernameCell.TextField.Text = username; + DismissViewController(false, null); + }, isUsernameGenerator: true, emailWebsite: NameCell.TextField.Text, appOptions: appOptions); + + ThemeManager.SetTheme(app.Resources); + ThemeManager.ApplyResourcesTo(generatorPage); + + var navigationPage = new NavigationPage(generatorPage); + var generatorController = navigationPage.CreateViewController(); + generatorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; + PresentViewController(generatorController, true, null); + } + public class TableSource : ExtendedUITableViewSource { private LoginAddViewController _controller; @@ -235,10 +267,6 @@ namespace Bit.iOS.Core.Controllers { return _controller.PasswordCell; } - else if (indexPath.Row == 3) - { - return _controller.GeneratePasswordCell; - } } else if (indexPath.Section == 1) { @@ -277,7 +305,7 @@ namespace Bit.iOS.Core.Controllers { if (section == 0) { - return 4; + return 3; } else if (section == 1) { @@ -317,11 +345,6 @@ namespace Bit.iOS.Core.Controllers tableView.DeselectRow(indexPath, true); tableView.EndEditing(true); - if (indexPath.Section == 0 && indexPath.Row == 3) - { - _controller.PerformSegue("passwordGeneratorSegue", this); - } - var cell = tableView.CellAt(indexPath); if (cell == null) { diff --git a/src/iOS.Core/Services/DeviceActionService.cs b/src/iOS.Core/Services/DeviceActionService.cs index c2bec2753..21de54abe 100644 --- a/src/iOS.Core/Services/DeviceActionService.cs +++ b/src/iOS.Core/Services/DeviceActionService.cs @@ -379,5 +379,10 @@ namespace Bit.iOS.Core.Services var url = new NSUrl(UIApplication.OpenSettingsUrlString); UIApplication.SharedApplication.OpenUrl(url); } + + public void CloseExtensionPopUp() + { + GetPresentedViewController().DismissViewController(true, null); + } } } diff --git a/src/iOS.Core/Views/FormEntryTableViewCell.cs b/src/iOS.Core/Views/FormEntryTableViewCell.cs index 9ebc25a6d..3de96806a 100644 --- a/src/iOS.Core/Views/FormEntryTableViewCell.cs +++ b/src/iOS.Core/Views/FormEntryTableViewCell.cs @@ -1,7 +1,7 @@ -using Bit.iOS.Core.Controllers; +using System; +using Bit.Core; +using Bit.iOS.Core.Controllers; using Bit.iOS.Core.Utilities; -using System; -using System.Drawing; using UIKit; namespace Bit.iOS.Core.Views @@ -12,14 +12,14 @@ namespace Bit.iOS.Core.Views public UITextField TextField { get; set; } public UITextView TextView { get; set; } public UIButton Button { get; set; } + public UIButton SecondButton { get; set; } public event EventHandler ValueChanged; - public FormEntryTableViewCell( string labelName = null, bool useTextView = false, nfloat? height = null, - bool useButton = false, + ButtonsConfig buttonsConfig = ButtonsConfig.None, bool useLabelAsPlaceholder = false, float leadingConstant = 15f) : base(UITableViewCellStyle.Default, nameof(FormEntryTableViewCell)) @@ -85,7 +85,6 @@ namespace Bit.iOS.Core.Views ValueChanged?.Invoke(sender, e); }; } - else { TextField = new UITextField @@ -110,9 +109,10 @@ namespace Bit.iOS.Core.Views } ContentView.Add(TextField); + ContentView.AddConstraints(new NSLayoutConstraint[] { NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, leadingConstant), - NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, useButton ? 55f : 15f), + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, GetTextFieldToContainerTrailingConstant(buttonsConfig)), NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Bottom, 1f, 10f) }); @@ -148,18 +148,9 @@ namespace Bit.iOS.Core.Views }); } - if (useButton) + if(buttonsConfig != ButtonsConfig.None) { - Button = new UIButton(UIButtonType.System); - Button.Frame = ContentView.Bounds; - Button.TranslatesAutoresizingMaskIntoConstraints = false; - Button.SetTitleColor(ThemeHelpers.PrimaryColor, UIControlState.Normal); - - ContentView.Add(Button); - - ContentView.BottomAnchor.ConstraintEqualTo(Button.BottomAnchor, 10f).Active = true; - ContentView.TrailingAnchor.ConstraintEqualTo(Button.TrailingAnchor, 10f).Active = true; - Button.LeadingAnchor.ConstraintEqualTo(TextField.TrailingAnchor, 10f).Active = true; + AddButtons(buttonsConfig); } } @@ -174,5 +165,74 @@ namespace Bit.iOS.Core.Views TextField.BecomeFirstResponder(); } } + + public void ConfigureToggleSecureTextCell(bool useSecondaryButton = false) + { + var button = useSecondaryButton ? SecondButton : Button; + button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f); + button.SetTitle(BitwardenIcons.Eye, UIControlState.Normal); + button.TouchUpInside += (sender, e) => + { + TextField.SecureTextEntry = !TextField.SecureTextEntry; + button.SetTitle(TextField.SecureTextEntry ? BitwardenIcons.Eye : BitwardenIcons.EyeSlash, UIControlState.Normal); + }; + } + + private void AddButtons(ButtonsConfig buttonsConfig) + { + Button = new UIButton(UIButtonType.System); + Button.TranslatesAutoresizingMaskIntoConstraints = false; + Button.SetTitleColor(ThemeHelpers.PrimaryColor, UIControlState.Normal); + + ContentView.Add(Button); + + ContentView.BottomAnchor.ConstraintEqualTo(Button.BottomAnchor, 10f).Active = true; + + switch (buttonsConfig) + { + case ButtonsConfig.One: + ContentView.AddConstraints(new NSLayoutConstraint[] { + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, Button, NSLayoutAttribute.Trailing, 1f, 10f), + NSLayoutConstraint.Create(Button, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, 10f) + }); + break; + case ButtonsConfig.Two: + SecondButton = new UIButton(UIButtonType.System); + SecondButton.TranslatesAutoresizingMaskIntoConstraints = false; + SecondButton.SetTitleColor(ThemeHelpers.PrimaryColor, UIControlState.Normal); + + ContentView.Add(SecondButton); + + ContentView.AddConstraints(new NSLayoutConstraint[] { + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, SecondButton, NSLayoutAttribute.Bottom, 1f, 10f), + NSLayoutConstraint.Create(SecondButton, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, 9f), + NSLayoutConstraint.Create(Button, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, SecondButton, NSLayoutAttribute.Trailing, 1f, 10f), + NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, Button, NSLayoutAttribute.Trailing, 1f, 10f) + }); + break; + } + } + + private float GetTextFieldToContainerTrailingConstant(ButtonsConfig buttonsConfig) + { + switch (buttonsConfig) + { + case ButtonsConfig.None: + return 15f; + case ButtonsConfig.One: + return 55f; + case ButtonsConfig.Two: + return 95f; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public enum ButtonsConfig : byte + { + None = 0, + One = 1, + Two = 2 + } } }