From 1f21a2ecc710dfe2a5022786c85d705a7ad27429 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 5 Mar 2018 15:15:20 -0500 Subject: [PATCH] add/edit/delete custom fields. remove field page. --- src/Android/Resources/values/styles.xml | 1 + src/Android/Services/DeviceActionService.cs | 40 +++ .../Services/IDeviceActionService.cs | 1 + .../Models/Page/VaultViewCipherPageModel.cs | 2 +- .../Pages/Tools/ToolsPasswordGeneratorPage.cs | 9 +- src/App/Pages/Vault/VaultAddCipherPage.cs | 42 ++- src/App/Pages/Vault/VaultCustomFieldsPage.cs | 251 ------------------ src/App/Pages/Vault/VaultEditCipherPage.cs | 85 ++++-- src/App/Resources/AppResources.Designer.cs | 81 ++++-- src/App/Resources/AppResources.resx | 27 +- src/App/Utilities/Colors.cs | 9 + src/App/Utilities/Helpers.cs | 178 +++++++++++++ src/UWP/Services/DeviceActionService.cs | 16 ++ src/iOS/Services/DeviceActionService.cs | 21 ++ 14 files changed, 459 insertions(+), 304 deletions(-) delete mode 100644 src/App/Pages/Vault/VaultCustomFieldsPage.cs create mode 100644 src/App/Utilities/Colors.cs diff --git a/src/Android/Resources/values/styles.xml b/src/Android/Resources/values/styles.xml index 70ddd5208..39fe3c659 100644 --- a/src/Android/Resources/values/styles.xml +++ b/src/Android/Resources/values/styles.xml @@ -13,5 +13,6 @@ @color/lightgray true @color/darkaccent + @color/darkaccent diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index 2897bb80d..85009c27f 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -24,6 +24,7 @@ using Bit.Android.Autofill; using System.Linq; using Plugin.Settings.Abstractions; using Android.Views.InputMethods; +using Android.Widget; namespace Bit.Android.Services { @@ -469,5 +470,44 @@ namespace Bit.Android.Services _progressDialog.Dispose(); _progressDialog = null; } + + public Task DisplayPromptAync(string title = null, string description = null, string text = null) + { + var activity = (MainActivity)CurrentContext; + var alertBuilder = new AlertDialog.Builder(activity); + alertBuilder.SetTitle(title); + alertBuilder.SetMessage(description); + + var input = new EditText(activity) + { + InputType = global::Android.Text.InputTypes.ClassText + }; + if(text != null) + { + input.Text = text; + input.SetSelection(text.Length); + } + else + { + input.FocusedByDefault = true; + } + alertBuilder.SetView(input); + + var result = new TaskCompletionSource(); + alertBuilder.SetPositiveButton(AppResources.Ok, (sender, args) => + { + result.TrySetResult(input.Text ?? string.Empty); + }); + + alertBuilder.SetNegativeButton(AppResources.Cancel, (sender, args) => + { + result.TrySetResult(null); + }); + + var alert = alertBuilder.Create(); + alert.Window.SetSoftInputMode(global::Android.Views.SoftInput.StateVisible); + alert.Show(); + return result.Task; + } } } diff --git a/src/App/Abstractions/Services/IDeviceActionService.cs b/src/App/Abstractions/Services/IDeviceActionService.cs index 65c1f7786..f7fc2006b 100644 --- a/src/App/Abstractions/Services/IDeviceActionService.cs +++ b/src/App/Abstractions/Services/IDeviceActionService.cs @@ -22,5 +22,6 @@ namespace Bit.App.Abstractions void OpenAccessibilitySettings(); void OpenAutofillSettings(); Task LaunchAppAsync(string appName, Page page); + Task DisplayPromptAync(string title = null, string description = null, string text = null); } } diff --git a/src/App/Models/Page/VaultViewCipherPageModel.cs b/src/App/Models/Page/VaultViewCipherPageModel.cs index f6f16d0cb..4b5c878a8 100644 --- a/src/App/Models/Page/VaultViewCipherPageModel.cs +++ b/src/App/Models/Page/VaultViewCipherPageModel.cs @@ -592,7 +592,7 @@ namespace Bit.App.Models.Page } else { - cipher.Fields = null; + Fields = null; } switch(cipher.Type) diff --git a/src/App/Pages/Tools/ToolsPasswordGeneratorPage.cs b/src/App/Pages/Tools/ToolsPasswordGeneratorPage.cs index e456f98a1..59fded1c9 100644 --- a/src/App/Pages/Tools/ToolsPasswordGeneratorPage.cs +++ b/src/App/Pages/Tools/ToolsPasswordGeneratorPage.cs @@ -71,9 +71,12 @@ namespace Bit.App.Pages SliderCell = new SliderViewCell(this, _passwordGenerationService, _settings); - var buttonColor = Color.FromHex("3c8dbc"); - RegenerateCell = new ExtendedTextCell { Text = AppResources.RegeneratePassword, TextColor = buttonColor }; - CopyCell = new ExtendedTextCell { Text = AppResources.CopyPassword, TextColor = buttonColor }; + RegenerateCell = new ExtendedTextCell + { + Text = AppResources.RegeneratePassword, + TextColor = Colors.Primary + }; + CopyCell = new ExtendedTextCell { Text = AppResources.CopyPassword, TextColor = Colors.Primary }; UppercaseCell = new ExtendedSwitchCell { diff --git a/src/App/Pages/Vault/VaultAddCipherPage.cs b/src/App/Pages/Vault/VaultAddCipherPage.cs index d0405a3f8..39e33af50 100644 --- a/src/App/Pages/Vault/VaultAddCipherPage.cs +++ b/src/App/Pages/Vault/VaultAddCipherPage.cs @@ -86,12 +86,14 @@ namespace Bit.App.Pages public TableRoot TableRoot { get; set; } public TableSection TopSection { get; set; } public TableSection MiddleSection { get; set; } + public TableSection FieldsSection { get; set; } public ExtendedTableView Table { get; set; } public FormEntryCell NameCell { get; private set; } public FormEditorCell NotesCell { get; private set; } public FormPickerCell FolderCell { get; private set; } public ExtendedSwitchCell FavoriteCell { get; set; } + public ExtendedTextCell AddFieldCell { get; private set; } // Login public FormEntryCell LoginPasswordCell { get; private set; } @@ -184,6 +186,11 @@ namespace Bit.App.Pages NotesCell.InitEvents(); FolderCell.InitEvents(); + if(AddFieldCell != null) + { + AddFieldCell.Tapped += AddFieldCell_Tapped; + } + switch(_type) { case CipherType.Login: @@ -256,6 +263,11 @@ namespace Bit.App.Pages NotesCell.Dispose(); FolderCell.Dispose(); + if(AddFieldCell != null) + { + AddFieldCell.Tapped -= AddFieldCell_Tapped; + } + switch(_type) { case CipherType.Login: @@ -301,6 +313,17 @@ namespace Bit.App.Pages default: break; } + + if(FieldsSection != null && FieldsSection.Count > 0) + { + foreach(var cell in FieldsSection) + { + if(cell is FormEntryCell entrycell) + { + entrycell.Dispose(); + } + } + } } protected override bool OnBackButtonPressed() @@ -540,6 +563,14 @@ namespace Bit.App.Pages NameCell.NextElement = NotesCell.Editor; } + FieldsSection = new TableSection(AppResources.CustomFields); + AddFieldCell = new ExtendedTextCell + { + Text = AppResources.NewCustomField, + TextColor = Colors.Primary + }; + FieldsSection.Add(AddFieldCell); + // Make table TableRoot = new TableRoot { @@ -548,7 +579,8 @@ namespace Bit.App.Pages new TableSection(AppResources.Notes) { NotesCell - } + }, + FieldsSection }; Table = new ExtendedTableView @@ -744,6 +776,8 @@ namespace Bit.App.Pages cipher.FolderId = Folders.ElementAt(FolderCell.Picker.SelectedIndex - 1).Id; } + Helpers.ProcessFieldsSectionForSave(FieldsSection, cipher); + _deviceActionService.ShowLoading(AppResources.Saving); var saveTask = await _cipherService.SaveAsync(cipher); _deviceActionService.HideLoading(); @@ -782,6 +816,10 @@ namespace Bit.App.Pages ToolbarItems.Add(saveToolBarItem); } + + private async void AddFieldCell_Tapped(object sender, EventArgs e) + { + await Helpers.AddField(this, FieldsSection); + } } } - diff --git a/src/App/Pages/Vault/VaultCustomFieldsPage.cs b/src/App/Pages/Vault/VaultCustomFieldsPage.cs deleted file mode 100644 index b0fe9cf47..000000000 --- a/src/App/Pages/Vault/VaultCustomFieldsPage.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System; -using System.Linq; -using Bit.App.Abstractions; -using Bit.App.Controls; -using Bit.App.Resources; -using Xamarin.Forms; -using XLabs.Ioc; -using Bit.App.Utilities; -using Plugin.Connectivity.Abstractions; -using Bit.App.Models; -using Bit.App.Enums; -using System.Collections.Generic; - -namespace Bit.App.Pages -{ - public class VaultCustomFieldsPage : ExtendedContentPage - { - private readonly ICipherService _cipherService; - private readonly IDeviceActionService _deviceActionService; - private readonly IConnectivity _connectivity; - private readonly IGoogleAnalyticsService _googleAnalyticsService; - private readonly string _cipherId; - private Cipher _cipher; - private DateTime? _lastAction; - - public VaultCustomFieldsPage(string cipherId) - : base(true) - { - _cipherId = cipherId; - _cipherService = Resolver.Resolve(); - _connectivity = Resolver.Resolve(); - _deviceActionService = Resolver.Resolve(); - _googleAnalyticsService = Resolver.Resolve(); - - Init(); - } - - public ToolbarItem SaveToolbarItem { get; set; } - public ToolbarItem CloseToolbarItem { get; set; } - public Label NoDataLabel { get; set; } - public TableSection FieldsSection { get; set; } - public ExtendedTableView Table { get; set; } - - private void Init() - { - FieldsSection = new TableSection(Helpers.GetEmptyTableSectionTitle()); - - Table = new ExtendedTableView - { - Intent = TableIntent.Settings, - EnableScrolling = true, - HasUnevenRows = true, - Root = new TableRoot - { - FieldsSection - } - }; - - NoDataLabel = new Label - { - Text = AppResources.NoCustomFields, - HorizontalTextAlignment = TextAlignment.Center, - FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)), - Margin = new Thickness(10, 40, 10, 0) - }; - - SaveToolbarItem = new ToolbarItem(AppResources.Save, Helpers.ToolbarImage("envelope.png"), async () => - { - if(_lastAction.LastActionWasRecent() || _cipher == null) - { - return; - } - _lastAction = DateTime.UtcNow; - - if(!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - - if(FieldsSection.Count > 0) - { - var fields = new List(); - foreach(var cell in FieldsSection) - { - if(cell is FormEntryCell entryCell) - { - fields.Add(new Field - { - Name = string.IsNullOrWhiteSpace(entryCell.Label.Text) ? null : - entryCell.Label.Text.Encrypt(_cipher.OrganizationId), - Value = string.IsNullOrWhiteSpace(entryCell.Entry.Text) ? null : - entryCell.Entry.Text.Encrypt(_cipher.OrganizationId), - Type = entryCell.Entry.IsPassword ? FieldType.Hidden : FieldType.Text - }); - } - else if(cell is ExtendedSwitchCell switchCell) - { - var value = switchCell.On ? "true" : "false"; - fields.Add(new Field - { - Name = string.IsNullOrWhiteSpace(switchCell.Text) ? null : - switchCell.Text.Encrypt(_cipher.OrganizationId), - Value = value.Encrypt(_cipher.OrganizationId), - Type = FieldType.Boolean - }); - } - } - _cipher.Fields = fields; - } - else - { - _cipher.Fields = null; - } - - _deviceActionService.ShowLoading(AppResources.Saving); - var saveTask = await _cipherService.SaveAsync(_cipher); - _deviceActionService.HideLoading(); - - if(saveTask.Succeeded) - { - _deviceActionService.Toast(AppResources.CustomFieldsUpdated); - _googleAnalyticsService.TrackAppEvent("UpdatedCustomFields"); - await Navigation.PopForDeviceAsync(); - } - else if(saveTask.Errors.Count() > 0) - { - await DisplayAlert(AppResources.AnErrorHasOccurred, saveTask.Errors.First().Message, AppResources.Ok); - } - else - { - await DisplayAlert(null, AppResources.AnErrorHasOccurred, AppResources.Ok); - } - }, ToolbarItemOrder.Default, 0); - ToolbarItems.Add(SaveToolbarItem); - - Title = AppResources.CustomFields; - Content = Table; - - if(Device.RuntimePlatform == Device.iOS) - { - CloseToolbarItem = new DismissModalToolBarItem(this, AppResources.Close); - ToolbarItems.Add(CloseToolbarItem); - - Table.RowHeight = -1; - Table.EstimatedRowHeight = 44; - } - else if(Device.RuntimePlatform == Device.Android) - { - Table.BottomPadding = 50; - } - } - - protected async override void OnAppearing() - { - base.OnAppearing(); - - _cipher = await _cipherService.GetByIdAsync(_cipherId); - if(_cipher == null) - { - await Navigation.PopForDeviceAsync(); - return; - } - - var hasFields = _cipher.Fields?.Any() ?? false; - - if(hasFields) - { - Content = Table; - if(CloseToolbarItem != null) - { - CloseToolbarItem.Text = AppResources.Cancel; - } - - foreach(var field in _cipher.Fields) - { - var label = field.Name?.Decrypt(_cipher.OrganizationId) ?? string.Empty; - var value = field.Value?.Decrypt(_cipher.OrganizationId); - switch(field.Type) - { - case FieldType.Text: - case FieldType.Hidden: - var hidden = field.Type == FieldType.Hidden; - - var textFieldCell = new FormEntryCell(label, isPassword: hidden, useButton: hidden); - textFieldCell.Entry.Text = value; - textFieldCell.Entry.DisableAutocapitalize = true; - textFieldCell.Entry.Autocorrect = false; - - if(hidden) - { - textFieldCell.Entry.FontFamily = Helpers.OnPlatform( - iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); - textFieldCell.Button.Image = "eye.png"; - textFieldCell.Button.Command = new Command(() => - { - textFieldCell.Entry.InvokeToggleIsPassword(); - textFieldCell.Button.Image = - "eye" + (!textFieldCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty) + ".png"; - }); - } - - textFieldCell.InitEvents(); - FieldsSection.Add(textFieldCell); - break; - case FieldType.Boolean: - var switchFieldCell = new ExtendedSwitchCell - { - Text = label, - On = value == "true" - }; - FieldsSection.Add(switchFieldCell); - break; - default: - continue; - } - } - } - else - { - Content = NoDataLabel; - if(ToolbarItems.Count > 0) - { - ToolbarItems.RemoveAt(0); - } - } - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - - if(FieldsSection != null && FieldsSection.Count > 0) - { - foreach(var cell in FieldsSection) - { - if(cell is FormEntryCell entrycell) - { - entrycell.Dispose(); - } - } - } - } - - private void AlertNoConnection() - { - DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, - AppResources.Ok); - } - } -} diff --git a/src/App/Pages/Vault/VaultEditCipherPage.cs b/src/App/Pages/Vault/VaultEditCipherPage.cs index 40dd242de..1504bc0d2 100644 --- a/src/App/Pages/Vault/VaultEditCipherPage.cs +++ b/src/App/Pages/Vault/VaultEditCipherPage.cs @@ -42,6 +42,7 @@ namespace Bit.App.Pages public TableRoot TableRoot { get; set; } public TableSection TopSection { get; set; } public TableSection MiddleSection { get; set; } + public TableSection FieldsSection { get; set; } public ExtendedTableView Table { get; set; } public FormEntryCell NameCell { get; private set; } @@ -49,8 +50,8 @@ namespace Bit.App.Pages public FormPickerCell FolderCell { get; private set; } public ExtendedSwitchCell FavoriteCell { get; set; } public ExtendedTextCell AttachmentsCell { get; private set; } - public ExtendedTextCell CustomFieldsCell { get; private set; } public ExtendedTextCell DeleteCell { get; private set; } + public ExtendedTextCell AddFieldCell { get; private set; } // Login public FormEntryCell LoginPasswordCell { get; private set; } @@ -152,12 +153,6 @@ namespace Bit.App.Pages ShowDisclousure = true }; - CustomFieldsCell = new ExtendedTextCell - { - Text = AppResources.CustomFields, - ShowDisclousure = true - }; - // Sections TopSection = new TableSection(AppResources.ItemInformation) { @@ -168,8 +163,7 @@ namespace Bit.App.Pages { FolderCell, FavoriteCell, - AttachmentsCell, - CustomFieldsCell + AttachmentsCell }; // Types @@ -405,6 +399,27 @@ namespace Bit.App.Pages NameCell.NextElement = NotesCell.Editor; } + FieldsSection = new TableSection(AppResources.CustomFields); + if(Cipher.Fields != null) + { + foreach(var field in Cipher.Fields) + { + var label = field.Name?.Decrypt(Cipher.OrganizationId) ?? string.Empty; + var value = field.Value?.Decrypt(Cipher.OrganizationId); + var cell = Helpers.MakeFieldCell(field.Type, label, value, FieldsSection); + if(cell != null) + { + FieldsSection.Add(cell); + } + } + } + AddFieldCell = new ExtendedTextCell + { + Text = AppResources.NewCustomField, + TextColor = Colors.Primary + }; + FieldsSection.Add(AddFieldCell); + // Make table TableRoot = new TableRoot { @@ -414,6 +429,7 @@ namespace Bit.App.Pages { NotesCell }, + FieldsSection, new TableSection(Helpers.GetEmptyTableSectionTitle()) { DeleteCell @@ -614,6 +630,8 @@ namespace Bit.App.Pages Cipher.FolderId = null; } + Helpers.ProcessFieldsSectionForSave(FieldsSection, Cipher); + _deviceActionService.ShowLoading(AppResources.Saving); var saveTask = await _cipherService.SaveAsync(Cipher); _deviceActionService.HideLoading(); @@ -653,14 +671,14 @@ namespace Bit.App.Pages { AttachmentsCell.Tapped += AttachmentsCell_Tapped; } - if(CustomFieldsCell != null) - { - CustomFieldsCell.Tapped += CustomFieldsCell_Tapped; - } if(DeleteCell != null) { DeleteCell.Tapped += DeleteCell_Tapped; } + if(AddFieldCell != null) + { + AddFieldCell.Tapped += AddFieldCell_Tapped; + } switch(Cipher.Type) { @@ -713,6 +731,17 @@ namespace Bit.App.Pages default: break; } + + if(FieldsSection != null && FieldsSection.Count > 0) + { + foreach(var cell in FieldsSection) + { + if(cell is FormEntryCell entrycell) + { + entrycell.InitEvents(); + } + } + } } protected override void OnDisappearing() @@ -727,14 +756,14 @@ namespace Bit.App.Pages { AttachmentsCell.Tapped -= AttachmentsCell_Tapped; } - if(CustomFieldsCell != null) - { - CustomFieldsCell.Tapped -= CustomFieldsCell_Tapped; - } if(DeleteCell != null) { DeleteCell.Tapped -= DeleteCell_Tapped; } + if(AddFieldCell != null) + { + AddFieldCell.Tapped -= AddFieldCell_Tapped; + } switch(Cipher.Type) { @@ -787,6 +816,17 @@ namespace Bit.App.Pages default: break; } + + if(FieldsSection != null && FieldsSection.Count > 0) + { + foreach(var cell in FieldsSection) + { + if(cell is FormEntryCell entrycell) + { + entrycell.Dispose(); + } + } + } } private void PasswordButton_Clicked(object sender, EventArgs e) @@ -840,12 +880,6 @@ namespace Bit.App.Pages await Navigation.PushModalAsync(page); } - private async void CustomFieldsCell_Tapped(object sender, EventArgs e) - { - var page = new ExtendedNavigationPage(new VaultCustomFieldsPage(_cipherId)); - await Navigation.PushModalAsync(page); - } - private async void DeleteCell_Tapped(object sender, EventArgs e) { if(!_connectivity.IsConnected) @@ -881,6 +915,11 @@ namespace Bit.App.Pages } } + private async void AddFieldCell_Tapped(object sender, EventArgs e) + { + await Helpers.AddField(this, FieldsSection); + } + private void AlertNoConnection() { DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 6cb2eb525..46fc30bf2 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -897,6 +897,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Custom Field Name. + /// + public static string CustomFieldName { + get { + return ResourceManager.GetString("CustomFieldName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Custom Fields. /// @@ -906,15 +915,6 @@ namespace Bit.App.Resources { } } - /// - /// Looks up a localized string similar to Custom fields updated.. - /// - public static string CustomFieldsUpdated { - get { - return ResourceManager.GetString("CustomFieldsUpdated", resourceCulture); - } - } - /// /// Looks up a localized string similar to December. /// @@ -1347,6 +1347,33 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Boolean. + /// + public static string FieldTypeBoolean { + get { + return ResourceManager.GetString("FieldTypeBoolean", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hidden. + /// + public static string FieldTypeHidden { + get { + return ResourceManager.GetString("FieldTypeHidden", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Text. + /// + public static string FieldTypeText { + get { + return ResourceManager.GetString("FieldTypeText", resourceCulture); + } + } + /// /// Looks up a localized string similar to File. /// @@ -2085,6 +2112,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to New Custom Field. + /// + public static string NewCustomField { + get { + return ResourceManager.GetString("NewCustomField", resourceCulture); + } + } + /// /// Looks up a localized string similar to New item created.. /// @@ -2112,15 +2148,6 @@ namespace Bit.App.Resources { } } - /// - /// Looks up a localized string similar to No custom fields. You can fully manage custom fields from the web vault or browser extension.. - /// - public static string NoCustomFields { - get { - return ResourceManager.GetString("NoCustomFields", resourceCulture); - } - } - /// /// Looks up a localized string similar to There are no favorites in your vault.. /// @@ -2445,6 +2472,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Remove. + /// + public static string Remove { + get { + return ResourceManager.GetString("Remove", resourceCulture); + } + } + /// /// Looks up a localized string similar to Re-type Master Password. /// @@ -2544,6 +2580,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to What type of custom field do you want to add?. + /// + public static string SelectTypeField { + get { + return ResourceManager.GetString("SelectTypeField", resourceCulture); + } + } + /// /// Looks up a localized string similar to Self-hosted Environment. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 953253a55..369c7f829 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1023,12 +1023,6 @@ Custom Fields - - Custom fields updated. - - - No custom fields. You can fully manage custom fields from the web vault or browser extension. - Copy Number @@ -1234,4 +1228,25 @@ We were unable to automatically open the Android autofill settings menu for you. You can navigate to the autofill settings menu manually from Android Settings > System > Languages and input > Advanced > Autofill service. + + Custom Field Name + + + Boolean + + + Hidden + + + Text + + + New Custom Field + + + What type of custom field do you want to add? + + + Remove + \ No newline at end of file diff --git a/src/App/Utilities/Colors.cs b/src/App/Utilities/Colors.cs new file mode 100644 index 000000000..fcca4b00b --- /dev/null +++ b/src/App/Utilities/Colors.cs @@ -0,0 +1,9 @@ +using Xamarin.Forms; + +namespace Bit.App.Utilities +{ + public static class Colors + { + public static Color Primary = Color.FromHex("3c8dbc"); + } +} diff --git a/src/App/Utilities/Helpers.cs b/src/App/Utilities/Helpers.cs index c6555522e..e37335fe4 100644 --- a/src/App/Utilities/Helpers.cs +++ b/src/App/Utilities/Helpers.cs @@ -1,11 +1,14 @@ using Bit.App.Abstractions; +using Bit.App.Controls; using Bit.App.Enums; +using Bit.App.Models; using Bit.App.Models.Page; using Bit.App.Pages; using Bit.App.Resources; using Plugin.Settings.Abstractions; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Xamarin.Forms; using XLabs.Ioc; @@ -195,5 +198,180 @@ namespace Bit.App.Utilities var addPage = new VaultAddCipherPage(selectedType, defaultFolderId: folderId); await page.Navigation.PushForDeviceAsync(addPage); } + + public static async Task AddField(Page page, TableSection fieldsSection) + { + var type = await page.DisplayActionSheet(AppResources.SelectTypeField, AppResources.Cancel, null, + AppResources.FieldTypeText, AppResources.FieldTypeHidden, AppResources.FieldTypeBoolean); + + FieldType fieldType; + if(type == AppResources.FieldTypeText) + { + fieldType = FieldType.Text; + } + else if(type == AppResources.FieldTypeHidden) + { + fieldType = FieldType.Hidden; + } + else if(type == AppResources.FieldTypeBoolean) + { + fieldType = FieldType.Boolean; + } + else + { + return; + } + + var daService = Resolver.Resolve(); + var label = await daService.DisplayPromptAync(AppResources.CustomFieldName); + if(label == null) + { + return; + } + + var cell = MakeFieldCell(fieldType, label, string.Empty, fieldsSection); + if(cell != null) + { + fieldsSection.Insert(fieldsSection.Count - 1, cell); + if(cell is FormEntryCell feCell) + { + feCell.InitEvents(); + } + } + } + + public static Cell MakeFieldCell(FieldType type, string label, string value, TableSection fieldsSection) + { + Cell cell; + switch(type) + { + case FieldType.Text: + case FieldType.Hidden: + var hidden = type == FieldType.Hidden; + var textFieldCell = new FormEntryCell(label, isPassword: hidden, useButton: hidden); + textFieldCell.Entry.Text = value; + textFieldCell.Entry.DisableAutocapitalize = true; + textFieldCell.Entry.Autocorrect = false; + + if(hidden) + { + textFieldCell.Entry.FontFamily = Helpers.OnPlatform( + iOS: "Menlo-Regular", Android: "monospace", Windows: "Courier"); + textFieldCell.Button.Image = "eye.png"; + textFieldCell.Button.Command = new Command(() => + { + textFieldCell.Entry.InvokeToggleIsPassword(); + textFieldCell.Button.Image = + "eye" + (!textFieldCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty) + ".png"; + }); + } + cell = textFieldCell; + break; + case FieldType.Boolean: + var switchFieldCell = new ExtendedSwitchCell + { + Text = label, + On = value == "true" + }; + cell = switchFieldCell; + break; + default: + cell = null; + break; + } + + if(cell != null) + { + var deleteAction = new MenuItem { Text = AppResources.Remove, IsDestructive = true }; + deleteAction.Clicked += (sender, e) => + { + if(fieldsSection.Contains(cell)) + { + fieldsSection.Remove(cell); + } + + if(cell is FormEntryCell feCell) + { + feCell.Dispose(); + } + cell = null; + }; + + var editNameAction = new MenuItem { Text = AppResources.Edit }; + editNameAction.Clicked += async (sender, e) => + { + string existingLabel = null; + var feCell = cell as FormEntryCell; + var esCell = cell as ExtendedSwitchCell; + if(feCell != null) + { + existingLabel = feCell.Label.Text; + } + else if(esCell != null) + { + existingLabel = esCell.Text; + } + + var daService = Resolver.Resolve(); + var editLabel = await daService.DisplayPromptAync(AppResources.CustomFieldName, + null, existingLabel); + if(editLabel != null) + { + if(feCell != null) + { + feCell.Label.Text = editLabel; + } + else if(esCell != null) + { + esCell.Text = editLabel; + } + } + }; + + cell.ContextActions.Add(editNameAction); + cell.ContextActions.Add(deleteAction); + } + + return cell; + } + + public static void ProcessFieldsSectionForSave(TableSection fieldsSection, Cipher cipher) + { + if(fieldsSection != null && fieldsSection.Count > 0) + { + var fields = new List(); + foreach(var cell in fieldsSection) + { + if(cell is FormEntryCell entryCell) + { + fields.Add(new Field + { + Name = string.IsNullOrWhiteSpace(entryCell.Label.Text) ? null : + entryCell.Label.Text.Encrypt(cipher.OrganizationId), + Value = string.IsNullOrWhiteSpace(entryCell.Entry.Text) ? null : + entryCell.Entry.Text.Encrypt(cipher.OrganizationId), + Type = entryCell.Entry.IsPassword ? FieldType.Hidden : FieldType.Text + }); + } + else if(cell is ExtendedSwitchCell switchCell) + { + var value = switchCell.On ? "true" : "false"; + fields.Add(new Field + { + Name = string.IsNullOrWhiteSpace(switchCell.Text) ? null : + switchCell.Text.Encrypt(cipher.OrganizationId), + Value = value.Encrypt(cipher.OrganizationId), + Type = FieldType.Boolean + }); + } + } + cipher.Fields = fields; + } + + if(!cipher.Fields?.Any() ?? true) + { + cipher.Fields = null; + } + } } } diff --git a/src/UWP/Services/DeviceActionService.cs b/src/UWP/Services/DeviceActionService.cs index 1f8712261..d81bdfe43 100644 --- a/src/UWP/Services/DeviceActionService.cs +++ b/src/UWP/Services/DeviceActionService.cs @@ -1,6 +1,7 @@ using Acr.UserDialogs; using Bit.App.Abstractions; using Bit.App.Models.Page; +using Bit.App.Resources; using Coding4Fun.Toolkit.Controls; using System; using System.Linq; @@ -164,5 +165,20 @@ namespace Bit.UWP.Services { throw new NotImplementedException(); } + + public async Task DisplayPromptAync(string title = null, string description = null, string text = null) + { + var result = await _userDialogs.PromptAsync(new PromptConfig + { + Title = title, + InputType = InputType.Default, + OkText = AppResources.Ok, + CancelText = AppResources.Cancel, + Message = description, + Text = text + }); + + return result.Ok ? result.Value ?? string.Empty : null; + } } } diff --git a/src/iOS/Services/DeviceActionService.cs b/src/iOS/Services/DeviceActionService.cs index 808cbda71..6e506a58d 100644 --- a/src/iOS/Services/DeviceActionService.cs +++ b/src/iOS/Services/DeviceActionService.cs @@ -323,6 +323,27 @@ namespace Bit.iOS.Services throw new NotImplementedException(); } + public Task DisplayPromptAync(string title = null, string description = null, string text = null) + { + var result = new TaskCompletionSource(); + var alert = UIAlertController.Create(title ?? string.Empty, description, UIAlertControllerStyle.Alert); + UITextField input = null; + alert.AddAction(UIAlertAction.Create(AppResources.Cancel, UIAlertActionStyle.Cancel, x => + { + result.TrySetResult(null); + })); + alert.AddAction(UIAlertAction.Create(AppResources.Ok, UIAlertActionStyle.Default, x => + { + result.TrySetResult(input.Text ?? string.Empty); + })); + alert.AddTextField(x => + { + input = x; + input.Text = text ?? string.Empty; + }); + return result.Task; + } + private UIViewController GetPresentedViewController() { var window = UIApplication.SharedApplication.KeyWindow;