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;