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