diff --git a/src/App/App.csproj b/src/App/App.csproj index c90ae7fee..e3c233973 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -168,6 +168,7 @@ + diff --git a/src/App/Models/Api/Request/CipherRequest.cs b/src/App/Models/Api/Request/CipherRequest.cs index 59ac8fce4..4e59a5b99 100644 --- a/src/App/Models/Api/Request/CipherRequest.cs +++ b/src/App/Models/Api/Request/CipherRequest.cs @@ -1,4 +1,6 @@ using Bit.App.Enums; +using System.Collections.Generic; +using System.Linq; namespace Bit.App.Models.Api { @@ -13,6 +15,16 @@ namespace Bit.App.Models.Api Notes = login.Notes?.EncryptedString; Favorite = login.Favorite; + if(login.Fields != null) + { + Fields = login.Fields.Select(f => new FieldDataModel + { + Name = f.Name?.EncryptedString, + Value = f.Value?.EncryptedString, + Type = f.Type + }); + } + switch(Type) { case CipherType.Login: @@ -29,6 +41,7 @@ namespace Bit.App.Models.Api public bool Favorite { get; set; } public string Name { get; set; } public string Notes { get; set; } + public IEnumerable Fields { get; set; } public LoginType Login { get; set; } public class LoginType diff --git a/src/App/Models/Field.cs b/src/App/Models/Field.cs index c0df3167e..9de157b72 100644 --- a/src/App/Models/Field.cs +++ b/src/App/Models/Field.cs @@ -5,6 +5,8 @@ namespace Bit.App.Models { public class Field { + public Field() { } + public Field(FieldDataModel model) { Type = model.Type; diff --git a/src/App/Pages/Vault/VaultCustomFieldsPage.cs b/src/App/Pages/Vault/VaultCustomFieldsPage.cs new file mode 100644 index 000000000..ab2cfb575 --- /dev/null +++ b/src/App/Pages/Vault/VaultCustomFieldsPage.cs @@ -0,0 +1,236 @@ +using System; +using System.Linq; +using Acr.UserDialogs; +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 ILoginService _loginService; + private readonly IUserDialogs _userDialogs; + private readonly IConnectivity _connectivity; + private readonly IGoogleAnalyticsService _googleAnalyticsService; + private readonly string _loginId; + private Login _login; + private DateTime? _lastAction; + + public VaultCustomFieldsPage(string loginId) + : base(true) + { + _loginId = loginId; + _loginService = Resolver.Resolve(); + _connectivity = Resolver.Resolve(); + _userDialogs = Resolver.Resolve(); + _googleAnalyticsService = Resolver.Resolve(); + + Init(); + } + + public ToolbarItem SaveToolbarItem { get; set; } + public Label NoDataLabel { get; set; } + public TableSection FieldsSection { get; set; } + public ExtendedTableView Table { get; set; } + + private void Init() + { + FieldsSection = new TableSection(" "); + + 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, null, async () => + { + if(_lastAction.LastActionWasRecent() || _login == 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(_login.OrganizationId), + Value = string.IsNullOrWhiteSpace(entryCell.Entry.Text) ? null : + entryCell.Entry.Text.Encrypt(_login.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(_login.OrganizationId), + Value = value.Encrypt(_login.OrganizationId), + Type = FieldType.Boolean + }); + } + } + _login.Fields = fields; + } + else + { + _login.Fields = null; + } + + _userDialogs.ShowLoading(AppResources.Saving, MaskType.Black); + var saveTask = await _loginService.SaveAsync(_login); + + _userDialogs.HideLoading(); + + if(saveTask.Succeeded) + { + _userDialogs.Toast(AppResources.CustomFieldsUpdated); + _googleAnalyticsService.TrackAppEvent("UpdatedCustomFields"); + await Navigation.PopForDeviceAsync(); + } + else if(saveTask.Errors.Count() > 0) + { + await _userDialogs.AlertAsync(saveTask.Errors.First().Message, AppResources.AnErrorHasOccurred); + } + else + { + await _userDialogs.AlertAsync(AppResources.AnErrorHasOccurred); + } + }, ToolbarItemOrder.Default, 0); + + Title = AppResources.CustomFields; + Content = Table; + + if(Device.RuntimePlatform == Device.iOS) + { + Table.RowHeight = -1; + Table.EstimatedRowHeight = 44; + } + } + + protected async override void OnAppearing() + { + base.OnAppearing(); + + _login = await _loginService.GetByIdAsync(_loginId); + if(_login == null) + { + await Navigation.PopForDeviceAsync(); + return; + } + + if(_login.Fields != null && _login.Fields.Any()) + { + Content = Table; + ToolbarItems.Add(SaveToolbarItem); + if(Device.RuntimePlatform == Device.iOS) + { + ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Cancel)); + } + + foreach(var field in _login.Fields) + { + var label = field.Name?.Decrypt(_login.OrganizationId) ?? string.Empty; + var value = field.Value?.Decrypt(_login.OrganizationId); + switch(field.Type) + { + case FieldType.Text: + case FieldType.Hidden: + var hidden = field.Type == FieldType.Hidden; + + var textFieldCell = new FormEntryCell(label, isPassword: 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", WinPhone: "Courier"); + } + + 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(Device.RuntimePlatform == Device.iOS) + { + ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close)); + } + } + } + + 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/VaultEditLoginPage.cs b/src/App/Pages/Vault/VaultEditLoginPage.cs index b086b6513..f059ca9e5 100644 --- a/src/App/Pages/Vault/VaultEditLoginPage.cs +++ b/src/App/Pages/Vault/VaultEditLoginPage.cs @@ -45,6 +45,7 @@ namespace Bit.App.Pages public FormPickerCell FolderCell { get; private set; } public ExtendedTextCell GenerateCell { get; private set; } public ExtendedTextCell AttachmentsCell { get; private set; } + public ExtendedTextCell CustomFieldsCell { get; private set; } public ExtendedTextCell DeleteCell { get; private set; } private void Init() @@ -125,6 +126,12 @@ namespace Bit.App.Pages ShowDisclousure = true }; + CustomFieldsCell = new ExtendedTextCell + { + Text = AppResources.CustomFields, + ShowDisclousure = true + }; + DeleteCell = new ExtendedTextCell { Text = AppResources.Delete, TextColor = Color.Red }; var table = new ExtendedTableView @@ -147,7 +154,8 @@ namespace Bit.App.Pages TotpCell, FolderCell, favoriteCell, - AttachmentsCell + AttachmentsCell, + CustomFieldsCell }, new TableSection(AppResources.Notes) { @@ -226,7 +234,7 @@ namespace Bit.App.Pages if(saveTask.Succeeded) { _userDialogs.Toast(AppResources.LoginUpdated); - _googleAnalyticsService.TrackAppEvent("EditeLogin"); + _googleAnalyticsService.TrackAppEvent("EditedLogin"); await Navigation.PopForDeviceAsync(); } else if(saveTask.Errors.Count() > 0) @@ -280,6 +288,10 @@ namespace Bit.App.Pages { AttachmentsCell.Tapped += AttachmentsCell_Tapped; } + if(CustomFieldsCell != null) + { + CustomFieldsCell.Tapped += CustomFieldsCell_Tapped; + } if(DeleteCell != null) { DeleteCell.Tapped += DeleteCell_Tapped; @@ -313,6 +325,10 @@ namespace Bit.App.Pages { AttachmentsCell.Tapped -= AttachmentsCell_Tapped; } + if(CustomFieldsCell != null) + { + CustomFieldsCell.Tapped -= CustomFieldsCell_Tapped; + } if(DeleteCell != null) { DeleteCell.Tapped -= DeleteCell_Tapped; @@ -369,6 +385,12 @@ namespace Bit.App.Pages await Navigation.PushModalAsync(page); } + private async void CustomFieldsCell_Tapped(object sender, EventArgs e) + { + var page = new ExtendedNavigationPage(new VaultCustomFieldsPage(_loginId)); + await Navigation.PushModalAsync(page); + } + private async void DeleteCell_Tapped(object sender, EventArgs e) { if(!_connectivity.IsConnected) diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 8bec4193f..09eb0f0b3 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -727,6 +727,15 @@ 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 Delete. /// @@ -1726,6 +1735,15 @@ 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.. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 2eb540bae..ba835f98c 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1033,4 +1033,10 @@ Custom Fields + + Custom fields updated. + + + No custom fields. You can fully manage custom fields from the web vault or browser extension. + \ No newline at end of file