1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-26 12:16:07 +01:00

add/edit/delete custom fields. remove field page.

This commit is contained in:
Kyle Spearrin 2018-03-05 15:15:20 -05:00
parent c3f4d56d1e
commit 1f21a2ecc7
14 changed files with 459 additions and 304 deletions

View File

@ -13,5 +13,6 @@
<item name="android:windowBackground">@color/lightgray</item> <item name="android:windowBackground">@color/lightgray</item>
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
<item name="android:navigationBarColor">@color/darkaccent</item> <item name="android:navigationBarColor">@color/darkaccent</item>
<item name="android:actionModeBackground">@color/darkaccent</item>
</style> </style>
</resources> </resources>

View File

@ -24,6 +24,7 @@ using Bit.Android.Autofill;
using System.Linq; using System.Linq;
using Plugin.Settings.Abstractions; using Plugin.Settings.Abstractions;
using Android.Views.InputMethods; using Android.Views.InputMethods;
using Android.Widget;
namespace Bit.Android.Services namespace Bit.Android.Services
{ {
@ -469,5 +470,44 @@ namespace Bit.Android.Services
_progressDialog.Dispose(); _progressDialog.Dispose();
_progressDialog = null; _progressDialog = null;
} }
public Task<string> 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<string>();
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;
}
} }
} }

View File

@ -22,5 +22,6 @@ namespace Bit.App.Abstractions
void OpenAccessibilitySettings(); void OpenAccessibilitySettings();
void OpenAutofillSettings(); void OpenAutofillSettings();
Task LaunchAppAsync(string appName, Page page); Task LaunchAppAsync(string appName, Page page);
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null);
} }
} }

View File

@ -592,7 +592,7 @@ namespace Bit.App.Models.Page
} }
else else
{ {
cipher.Fields = null; Fields = null;
} }
switch(cipher.Type) switch(cipher.Type)

View File

@ -71,9 +71,12 @@ namespace Bit.App.Pages
SliderCell = new SliderViewCell(this, _passwordGenerationService, _settings); SliderCell = new SliderViewCell(this, _passwordGenerationService, _settings);
var buttonColor = Color.FromHex("3c8dbc"); RegenerateCell = new ExtendedTextCell
RegenerateCell = new ExtendedTextCell { Text = AppResources.RegeneratePassword, TextColor = buttonColor }; {
CopyCell = new ExtendedTextCell { Text = AppResources.CopyPassword, TextColor = buttonColor }; Text = AppResources.RegeneratePassword,
TextColor = Colors.Primary
};
CopyCell = new ExtendedTextCell { Text = AppResources.CopyPassword, TextColor = Colors.Primary };
UppercaseCell = new ExtendedSwitchCell UppercaseCell = new ExtendedSwitchCell
{ {

View File

@ -86,12 +86,14 @@ namespace Bit.App.Pages
public TableRoot TableRoot { get; set; } public TableRoot TableRoot { get; set; }
public TableSection TopSection { get; set; } public TableSection TopSection { get; set; }
public TableSection MiddleSection { get; set; } public TableSection MiddleSection { get; set; }
public TableSection FieldsSection { get; set; }
public ExtendedTableView Table { get; set; } public ExtendedTableView Table { get; set; }
public FormEntryCell NameCell { get; private set; } public FormEntryCell NameCell { get; private set; }
public FormEditorCell NotesCell { get; private set; } public FormEditorCell NotesCell { get; private set; }
public FormPickerCell FolderCell { get; private set; } public FormPickerCell FolderCell { get; private set; }
public ExtendedSwitchCell FavoriteCell { get; set; } public ExtendedSwitchCell FavoriteCell { get; set; }
public ExtendedTextCell AddFieldCell { get; private set; }
// Login // Login
public FormEntryCell LoginPasswordCell { get; private set; } public FormEntryCell LoginPasswordCell { get; private set; }
@ -184,6 +186,11 @@ namespace Bit.App.Pages
NotesCell.InitEvents(); NotesCell.InitEvents();
FolderCell.InitEvents(); FolderCell.InitEvents();
if(AddFieldCell != null)
{
AddFieldCell.Tapped += AddFieldCell_Tapped;
}
switch(_type) switch(_type)
{ {
case CipherType.Login: case CipherType.Login:
@ -256,6 +263,11 @@ namespace Bit.App.Pages
NotesCell.Dispose(); NotesCell.Dispose();
FolderCell.Dispose(); FolderCell.Dispose();
if(AddFieldCell != null)
{
AddFieldCell.Tapped -= AddFieldCell_Tapped;
}
switch(_type) switch(_type)
{ {
case CipherType.Login: case CipherType.Login:
@ -301,6 +313,17 @@ namespace Bit.App.Pages
default: default:
break; break;
} }
if(FieldsSection != null && FieldsSection.Count > 0)
{
foreach(var cell in FieldsSection)
{
if(cell is FormEntryCell entrycell)
{
entrycell.Dispose();
}
}
}
} }
protected override bool OnBackButtonPressed() protected override bool OnBackButtonPressed()
@ -540,6 +563,14 @@ namespace Bit.App.Pages
NameCell.NextElement = NotesCell.Editor; NameCell.NextElement = NotesCell.Editor;
} }
FieldsSection = new TableSection(AppResources.CustomFields);
AddFieldCell = new ExtendedTextCell
{
Text = AppResources.NewCustomField,
TextColor = Colors.Primary
};
FieldsSection.Add(AddFieldCell);
// Make table // Make table
TableRoot = new TableRoot TableRoot = new TableRoot
{ {
@ -548,7 +579,8 @@ namespace Bit.App.Pages
new TableSection(AppResources.Notes) new TableSection(AppResources.Notes)
{ {
NotesCell NotesCell
} },
FieldsSection
}; };
Table = new ExtendedTableView Table = new ExtendedTableView
@ -744,6 +776,8 @@ namespace Bit.App.Pages
cipher.FolderId = Folders.ElementAt(FolderCell.Picker.SelectedIndex - 1).Id; cipher.FolderId = Folders.ElementAt(FolderCell.Picker.SelectedIndex - 1).Id;
} }
Helpers.ProcessFieldsSectionForSave(FieldsSection, cipher);
_deviceActionService.ShowLoading(AppResources.Saving); _deviceActionService.ShowLoading(AppResources.Saving);
var saveTask = await _cipherService.SaveAsync(cipher); var saveTask = await _cipherService.SaveAsync(cipher);
_deviceActionService.HideLoading(); _deviceActionService.HideLoading();
@ -782,6 +816,10 @@ namespace Bit.App.Pages
ToolbarItems.Add(saveToolBarItem); ToolbarItems.Add(saveToolBarItem);
} }
private async void AddFieldCell_Tapped(object sender, EventArgs e)
{
await Helpers.AddField(this, FieldsSection);
}
} }
} }

View File

@ -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<ICipherService>();
_connectivity = Resolver.Resolve<IConnectivity>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
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<Field>();
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);
}
}
}

View File

@ -42,6 +42,7 @@ namespace Bit.App.Pages
public TableRoot TableRoot { get; set; } public TableRoot TableRoot { get; set; }
public TableSection TopSection { get; set; } public TableSection TopSection { get; set; }
public TableSection MiddleSection { get; set; } public TableSection MiddleSection { get; set; }
public TableSection FieldsSection { get; set; }
public ExtendedTableView Table { get; set; } public ExtendedTableView Table { get; set; }
public FormEntryCell NameCell { get; private set; } public FormEntryCell NameCell { get; private set; }
@ -49,8 +50,8 @@ namespace Bit.App.Pages
public FormPickerCell FolderCell { get; private set; } public FormPickerCell FolderCell { get; private set; }
public ExtendedSwitchCell FavoriteCell { get; set; } public ExtendedSwitchCell FavoriteCell { get; set; }
public ExtendedTextCell AttachmentsCell { get; private set; } public ExtendedTextCell AttachmentsCell { get; private set; }
public ExtendedTextCell CustomFieldsCell { get; private set; }
public ExtendedTextCell DeleteCell { get; private set; } public ExtendedTextCell DeleteCell { get; private set; }
public ExtendedTextCell AddFieldCell { get; private set; }
// Login // Login
public FormEntryCell LoginPasswordCell { get; private set; } public FormEntryCell LoginPasswordCell { get; private set; }
@ -152,12 +153,6 @@ namespace Bit.App.Pages
ShowDisclousure = true ShowDisclousure = true
}; };
CustomFieldsCell = new ExtendedTextCell
{
Text = AppResources.CustomFields,
ShowDisclousure = true
};
// Sections // Sections
TopSection = new TableSection(AppResources.ItemInformation) TopSection = new TableSection(AppResources.ItemInformation)
{ {
@ -168,8 +163,7 @@ namespace Bit.App.Pages
{ {
FolderCell, FolderCell,
FavoriteCell, FavoriteCell,
AttachmentsCell, AttachmentsCell
CustomFieldsCell
}; };
// Types // Types
@ -405,6 +399,27 @@ namespace Bit.App.Pages
NameCell.NextElement = NotesCell.Editor; 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 // Make table
TableRoot = new TableRoot TableRoot = new TableRoot
{ {
@ -414,6 +429,7 @@ namespace Bit.App.Pages
{ {
NotesCell NotesCell
}, },
FieldsSection,
new TableSection(Helpers.GetEmptyTableSectionTitle()) new TableSection(Helpers.GetEmptyTableSectionTitle())
{ {
DeleteCell DeleteCell
@ -614,6 +630,8 @@ namespace Bit.App.Pages
Cipher.FolderId = null; Cipher.FolderId = null;
} }
Helpers.ProcessFieldsSectionForSave(FieldsSection, Cipher);
_deviceActionService.ShowLoading(AppResources.Saving); _deviceActionService.ShowLoading(AppResources.Saving);
var saveTask = await _cipherService.SaveAsync(Cipher); var saveTask = await _cipherService.SaveAsync(Cipher);
_deviceActionService.HideLoading(); _deviceActionService.HideLoading();
@ -653,14 +671,14 @@ namespace Bit.App.Pages
{ {
AttachmentsCell.Tapped += AttachmentsCell_Tapped; AttachmentsCell.Tapped += AttachmentsCell_Tapped;
} }
if(CustomFieldsCell != null)
{
CustomFieldsCell.Tapped += CustomFieldsCell_Tapped;
}
if(DeleteCell != null) if(DeleteCell != null)
{ {
DeleteCell.Tapped += DeleteCell_Tapped; DeleteCell.Tapped += DeleteCell_Tapped;
} }
if(AddFieldCell != null)
{
AddFieldCell.Tapped += AddFieldCell_Tapped;
}
switch(Cipher.Type) switch(Cipher.Type)
{ {
@ -713,6 +731,17 @@ namespace Bit.App.Pages
default: default:
break; break;
} }
if(FieldsSection != null && FieldsSection.Count > 0)
{
foreach(var cell in FieldsSection)
{
if(cell is FormEntryCell entrycell)
{
entrycell.InitEvents();
}
}
}
} }
protected override void OnDisappearing() protected override void OnDisappearing()
@ -727,14 +756,14 @@ namespace Bit.App.Pages
{ {
AttachmentsCell.Tapped -= AttachmentsCell_Tapped; AttachmentsCell.Tapped -= AttachmentsCell_Tapped;
} }
if(CustomFieldsCell != null)
{
CustomFieldsCell.Tapped -= CustomFieldsCell_Tapped;
}
if(DeleteCell != null) if(DeleteCell != null)
{ {
DeleteCell.Tapped -= DeleteCell_Tapped; DeleteCell.Tapped -= DeleteCell_Tapped;
} }
if(AddFieldCell != null)
{
AddFieldCell.Tapped -= AddFieldCell_Tapped;
}
switch(Cipher.Type) switch(Cipher.Type)
{ {
@ -787,6 +816,17 @@ namespace Bit.App.Pages
default: default:
break; 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) private void PasswordButton_Clicked(object sender, EventArgs e)
@ -840,12 +880,6 @@ namespace Bit.App.Pages
await Navigation.PushModalAsync(page); 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) private async void DeleteCell_Tapped(object sender, EventArgs e)
{ {
if(!_connectivity.IsConnected) 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() private void AlertNoConnection()
{ {
DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage,

View File

@ -897,6 +897,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Custom Field Name.
/// </summary>
public static string CustomFieldName {
get {
return ResourceManager.GetString("CustomFieldName", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Custom Fields. /// Looks up a localized string similar to Custom Fields.
/// </summary> /// </summary>
@ -906,15 +915,6 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Custom fields updated..
/// </summary>
public static string CustomFieldsUpdated {
get {
return ResourceManager.GetString("CustomFieldsUpdated", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to December. /// Looks up a localized string similar to December.
/// </summary> /// </summary>
@ -1347,6 +1347,33 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Boolean.
/// </summary>
public static string FieldTypeBoolean {
get {
return ResourceManager.GetString("FieldTypeBoolean", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Hidden.
/// </summary>
public static string FieldTypeHidden {
get {
return ResourceManager.GetString("FieldTypeHidden", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Text.
/// </summary>
public static string FieldTypeText {
get {
return ResourceManager.GetString("FieldTypeText", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to File. /// Looks up a localized string similar to File.
/// </summary> /// </summary>
@ -2085,6 +2112,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to New Custom Field.
/// </summary>
public static string NewCustomField {
get {
return ResourceManager.GetString("NewCustomField", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to New item created.. /// Looks up a localized string similar to New item created..
/// </summary> /// </summary>
@ -2112,15 +2148,6 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to No custom fields. You can fully manage custom fields from the web vault or browser extension..
/// </summary>
public static string NoCustomFields {
get {
return ResourceManager.GetString("NoCustomFields", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to There are no favorites in your vault.. /// Looks up a localized string similar to There are no favorites in your vault..
/// </summary> /// </summary>
@ -2445,6 +2472,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Remove.
/// </summary>
public static string Remove {
get {
return ResourceManager.GetString("Remove", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Re-type Master Password. /// Looks up a localized string similar to Re-type Master Password.
/// </summary> /// </summary>
@ -2544,6 +2580,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to What type of custom field do you want to add?.
/// </summary>
public static string SelectTypeField {
get {
return ResourceManager.GetString("SelectTypeField", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Self-hosted Environment. /// Looks up a localized string similar to Self-hosted Environment.
/// </summary> /// </summary>

View File

@ -1023,12 +1023,6 @@
<data name="CustomFields" xml:space="preserve"> <data name="CustomFields" xml:space="preserve">
<value>Custom Fields</value> <value>Custom Fields</value>
</data> </data>
<data name="CustomFieldsUpdated" xml:space="preserve">
<value>Custom fields updated.</value>
</data>
<data name="NoCustomFields" xml:space="preserve">
<value>No custom fields. You can fully manage custom fields from the web vault or browser extension.</value>
</data>
<data name="CopyNumber" xml:space="preserve"> <data name="CopyNumber" xml:space="preserve">
<value>Copy Number</value> <value>Copy Number</value>
</data> </data>
@ -1234,4 +1228,25 @@
<data name="BitwardenAutofillGoToSettings" xml:space="preserve"> <data name="BitwardenAutofillGoToSettings" xml:space="preserve">
<value>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 &gt; System &gt; Languages and input &gt; Advanced &gt; Autofill service.</value> <value>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 &gt; System &gt; Languages and input &gt; Advanced &gt; Autofill service.</value>
</data> </data>
<data name="CustomFieldName" xml:space="preserve">
<value>Custom Field Name</value>
</data>
<data name="FieldTypeBoolean" xml:space="preserve">
<value>Boolean</value>
</data>
<data name="FieldTypeHidden" xml:space="preserve">
<value>Hidden</value>
</data>
<data name="FieldTypeText" xml:space="preserve">
<value>Text</value>
</data>
<data name="NewCustomField" xml:space="preserve">
<value>New Custom Field</value>
</data>
<data name="SelectTypeField" xml:space="preserve">
<value>What type of custom field do you want to add?</value>
</data>
<data name="Remove" xml:space="preserve">
<value>Remove</value>
</data>
</root> </root>

View File

@ -0,0 +1,9 @@
using Xamarin.Forms;
namespace Bit.App.Utilities
{
public static class Colors
{
public static Color Primary = Color.FromHex("3c8dbc");
}
}

View File

@ -1,11 +1,14 @@
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Enums; using Bit.App.Enums;
using Bit.App.Models;
using Bit.App.Models.Page; using Bit.App.Models.Page;
using Bit.App.Pages; using Bit.App.Pages;
using Bit.App.Resources; using Bit.App.Resources;
using Plugin.Settings.Abstractions; using Plugin.Settings.Abstractions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xamarin.Forms; using Xamarin.Forms;
using XLabs.Ioc; using XLabs.Ioc;
@ -195,5 +198,180 @@ namespace Bit.App.Utilities
var addPage = new VaultAddCipherPage(selectedType, defaultFolderId: folderId); var addPage = new VaultAddCipherPage(selectedType, defaultFolderId: folderId);
await page.Navigation.PushForDeviceAsync(addPage); 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<IDeviceActionService>();
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<IDeviceActionService>();
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<Field>();
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;
}
}
} }
} }

View File

@ -1,6 +1,7 @@
using Acr.UserDialogs; using Acr.UserDialogs;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Models.Page; using Bit.App.Models.Page;
using Bit.App.Resources;
using Coding4Fun.Toolkit.Controls; using Coding4Fun.Toolkit.Controls;
using System; using System;
using System.Linq; using System.Linq;
@ -164,5 +165,20 @@ namespace Bit.UWP.Services
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public async Task<string> 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;
}
} }
} }

View File

@ -323,6 +323,27 @@ namespace Bit.iOS.Services
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<string> DisplayPromptAync(string title = null, string description = null, string text = null)
{
var result = new TaskCompletionSource<string>();
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() private UIViewController GetPresentedViewController()
{ {
var window = UIApplication.SharedApplication.KeyWindow; var window = UIApplication.SharedApplication.KeyWindow;