using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Bit.App.Abstractions; using Bit.App.Lists.ItemViewModels.CustomFields; using Bit.App.Models; using Bit.App.Resources; using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.View; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Forms; namespace Bit.App.Pages { public class CipherAddEditPageViewModel : BaseCipherViewModel { private readonly ICipherService _cipherService; private readonly IFolderService _folderService; private readonly ICollectionService _collectionService; private readonly IStateService _stateService; private readonly IOrganizationService _organizationService; private readonly IMessagingService _messagingService; private readonly IEventService _eventService; private readonly IPolicyService _policyService; private readonly ICustomFieldItemFactory _customFieldItemFactory; private readonly IClipboardService _clipboardService; private readonly IAutofillHandler _autofillHandler; private readonly IWatchDeviceService _watchDeviceService; private readonly IAccountsManager _accountsManager; private bool _showNotesSeparator; private bool _showPassword; private bool _showCardNumber; private bool _showCardCode; private int _typeSelectedIndex; private int _cardBrandSelectedIndex; private int _cardExpMonthSelectedIndex; private int _identityTitleSelectedIndex; private int _folderSelectedIndex; private int _ownershipSelectedIndex; private bool _hasCollections; private string _previousCipherId; private List _writeableCollections; private bool _fromOtp; protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[] { nameof(IsLogin), nameof(IsIdentity), nameof(IsCard), nameof(IsSecureNote), nameof(ShowUris), nameof(ShowAttachments), nameof(ShowCollections), nameof(HasTotpValue) }; private List> _matchDetectionOptions = new List> { new KeyValuePair(null, AppResources.Default), new KeyValuePair(UriMatchType.Domain, AppResources.BaseDomain), new KeyValuePair(UriMatchType.Host, AppResources.Host), new KeyValuePair(UriMatchType.StartsWith, AppResources.StartsWith), new KeyValuePair(UriMatchType.RegularExpression, AppResources.RegEx), new KeyValuePair(UriMatchType.Exact, AppResources.Exact), new KeyValuePair(UriMatchType.Never, AppResources.Never) }; public CipherAddEditPageViewModel() { _cipherService = ServiceContainer.Resolve("cipherService"); _folderService = ServiceContainer.Resolve("folderService"); _stateService = ServiceContainer.Resolve("stateService"); _organizationService = ServiceContainer.Resolve("organizationService"); _messagingService = ServiceContainer.Resolve("messagingService"); _collectionService = ServiceContainer.Resolve("collectionService"); _eventService = ServiceContainer.Resolve("eventService"); _policyService = ServiceContainer.Resolve("policyService"); _customFieldItemFactory = ServiceContainer.Resolve("customFieldItemFactory"); _clipboardService = ServiceContainer.Resolve("clipboardService"); _autofillHandler = ServiceContainer.Resolve(); _watchDeviceService = ServiceContainer.Resolve(); _accountsManager = ServiceContainer.Resolve(); GeneratePasswordCommand = new Command(GeneratePassword); TogglePasswordCommand = new Command(TogglePassword); ToggleCardNumberCommand = new Command(ToggleCardNumber); ToggleCardCodeCommand = new Command(ToggleCardCode); UriOptionsCommand = new Command(UriOptions); FieldOptionsCommand = new Command(FieldOptions); PasswordPromptHelpCommand = new Command(PasswordPromptHelp); CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); GenerateUsernameCommand = new AsyncCommand(GenerateUsernameAsync, onException: ex => OnGenerateUsernameException(ex), allowsMultipleExecutions: false); Uris = new ExtendedObservableCollection(); Fields = new ExtendedObservableCollection(); Collections = new ExtendedObservableCollection(); AllowPersonal = true; TypeOptions = new List> { new KeyValuePair(AppResources.TypeLogin, CipherType.Login), new KeyValuePair(AppResources.TypeCard, CipherType.Card), new KeyValuePair(AppResources.TypeIdentity, CipherType.Identity), new KeyValuePair(AppResources.TypeSecureNote, CipherType.SecureNote), }; CardBrandOptions = new List> { new KeyValuePair($"-- {AppResources.Select} --", null), new KeyValuePair("Visa", "Visa"), new KeyValuePair("Mastercard", "Mastercard"), new KeyValuePair("American Express", "Amex"), new KeyValuePair("Discover", "Discover"), new KeyValuePair("Diners Club", "Diners Club"), new KeyValuePair("JCB", "JCB"), new KeyValuePair("Maestro", "Maestro"), new KeyValuePair("UnionPay", "UnionPay"), new KeyValuePair(AppResources.Other, "Other") }; CardExpMonthOptions = new List> { new KeyValuePair($"-- {AppResources.Select} --", null), new KeyValuePair($"01 - {AppResources.January}", "1"), new KeyValuePair($"02 - {AppResources.February}", "2"), new KeyValuePair($"03 - {AppResources.March}", "3"), new KeyValuePair($"04 - {AppResources.April}", "4"), new KeyValuePair($"05 - {AppResources.May}", "5"), new KeyValuePair($"06 - {AppResources.June}", "6"), new KeyValuePair($"07 - {AppResources.July}", "7"), new KeyValuePair($"08 - {AppResources.August}", "8"), new KeyValuePair($"09 - {AppResources.September}", "9"), new KeyValuePair($"10 - {AppResources.October}", "10"), new KeyValuePair($"11 - {AppResources.November}", "11"), new KeyValuePair($"12 - {AppResources.December}", "12") }; IdentityTitleOptions = new List> { new KeyValuePair($"-- {AppResources.Select} --", null), new KeyValuePair(AppResources.Mr, AppResources.Mr), new KeyValuePair(AppResources.Mrs, AppResources.Mrs), new KeyValuePair(AppResources.Ms, AppResources.Ms), new KeyValuePair(AppResources.Mx, AppResources.Mx), new KeyValuePair(AppResources.Dr, AppResources.Dr), }; FolderOptions = new List>(); OwnershipOptions = new List>(); } public Command GeneratePasswordCommand { get; set; } public Command TogglePasswordCommand { get; set; } public Command ToggleCardNumberCommand { get; set; } public Command ToggleCardCodeCommand { get; set; } public Command UriOptionsCommand { get; set; } public Command FieldOptionsCommand { get; set; } public Command PasswordPromptHelpCommand { get; set; } public AsyncCommand CopyCommand { get; set; } public AsyncCommand GenerateUsernameCommand { get; set; } public string CipherId { get; set; } public string OrganizationId { get; set; } public string FolderId { get; set; } public CipherType? Type { get; set; } public HashSet CollectionIds { get; set; } public string DefaultName { get; set; } public string DefaultUri { get; set; } public List> TypeOptions { get; set; } public List> CardBrandOptions { get; set; } public List> CardExpMonthOptions { get; set; } public List> IdentityTitleOptions { get; set; } public List> FolderOptions { get; set; } public List> OwnershipOptions { get; set; } public ExtendedObservableCollection Uris { get; set; } public ExtendedObservableCollection Fields { get; set; } public ExtendedObservableCollection Collections { get; set; } public int TypeSelectedIndex { get => _typeSelectedIndex; set { if (SetProperty(ref _typeSelectedIndex, value)) { TypeChanged(); } } } public int CardBrandSelectedIndex { get => _cardBrandSelectedIndex; set { if (SetProperty(ref _cardBrandSelectedIndex, value)) { CardBrandChanged(); } } } public int CardExpMonthSelectedIndex { get => _cardExpMonthSelectedIndex; set { if (SetProperty(ref _cardExpMonthSelectedIndex, value)) { CardExpMonthChanged(); } } } public int IdentityTitleSelectedIndex { get => _identityTitleSelectedIndex; set { if (SetProperty(ref _identityTitleSelectedIndex, value)) { IdentityTitleChanged(); } } } public int FolderSelectedIndex { get => _folderSelectedIndex; set { if (SetProperty(ref _folderSelectedIndex, value)) { FolderChanged(); } } } public int OwnershipSelectedIndex { get => _ownershipSelectedIndex; set { if (SetProperty(ref _ownershipSelectedIndex, value)) { OrganizationChanged(); } } } public bool ShowNotesSeparator { get => _showNotesSeparator; set => SetProperty(ref _showNotesSeparator, value); } public bool ShowPassword { get => _showPassword; set => SetProperty(ref _showPassword, value, additionalPropertyNames: new string[] { nameof(ShowPasswordIcon), nameof(PasswordVisibilityAccessibilityText) }); } public bool ShowCardNumber { get => _showCardNumber; set => SetProperty(ref _showCardNumber, value, additionalPropertyNames: new string[] { nameof(ShowCardNumberIcon) }); } public bool ShowCardCode { get => _showCardCode; set => SetProperty(ref _showCardCode, value, additionalPropertyNames: new string[] { nameof(ShowCardCodeIcon) }); } public bool HasCollections { get => _hasCollections; set => SetProperty(ref _hasCollections, value, additionalPropertyNames: new string[] { nameof(ShowCollections) }); } public bool ShowCollections => (!EditMode || CloneMode) && Cipher.OrganizationId != null; public bool EditMode => !string.IsNullOrWhiteSpace(CipherId); public bool ShowOwnershipOptions => !EditMode || CloneMode; public bool OwnershipPolicyInEffect => ShowOwnershipOptions && !AllowPersonal; public bool CloneMode { get; set; } public CipherDetailsPage CipherDetailsPage { get; set; } public bool IsLogin => Cipher?.Type == CipherType.Login; public bool IsIdentity => Cipher?.Type == CipherType.Identity; public bool IsCard => Cipher?.Type == CipherType.Card; public bool IsSecureNote => Cipher?.Type == CipherType.SecureNote; public bool ShowUris => IsLogin && Cipher.Login.HasUris; public bool ShowAttachments => Cipher.HasAttachments; public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public string ShowCardNumberIcon => ShowCardNumber ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public string ShowCardCodeIcon => ShowCardCode ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public int PasswordFieldColSpan => Cipher.ViewPassword ? 1 : 4; public int TotpColumnSpan => Cipher.ViewPassword ? 1 : 2; public bool AllowPersonal { get; set; } public bool PasswordPrompt => Cipher.Reprompt != CipherRepromptType.None; public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; public bool HasTotpValue => IsLogin && !string.IsNullOrEmpty(Cipher?.Login?.Totp); public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTotp}"; public void Init() { PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem; } public async Task LoadAsync(AppOptions appOptions = null) { _fromOtp = appOptions?.OtpData != null; var myEmail = await _stateService.GetEmailAsync(); OwnershipOptions.Add(new KeyValuePair(myEmail, null)); var orgs = await _organizationService.GetAllAsync(); foreach (var org in orgs.OrderBy(o => o.Name)) { if (org.Enabled && org.Status == OrganizationUserStatusType.Confirmed) { OwnershipOptions.Add(new KeyValuePair(org.Name, org.Id)); } } var personalOwnershipPolicyApplies = await _policyService.PolicyAppliesToUser(PolicyType.PersonalOwnership); if (personalOwnershipPolicyApplies && (!EditMode || CloneMode)) { AllowPersonal = false; // Remove personal ownership OwnershipOptions.RemoveAt(0); } var allCollections = await _collectionService.GetAllDecryptedAsync(); _writeableCollections = allCollections.Where(c => !c.ReadOnly).ToList(); if (CollectionIds?.Any() ?? false) { var colId = CollectionIds.First(); var collection = _writeableCollections.FirstOrDefault(c => c.Id == colId); OrganizationId = collection?.OrganizationId; } var folders = await _folderService.GetAllDecryptedAsync(); FolderOptions = folders.Select(f => new KeyValuePair(f.Name, f.Id)).ToList(); if (Cipher == null) { if (EditMode) { var cipher = await _cipherService.GetAsync(CipherId); if (cipher == null) { return false; } Cipher = await cipher.DecryptAsync(); if (CloneMode) { Cipher.Name += " - " + AppResources.Clone; // If not allowing personal ownership, update cipher's org Id to prompt downstream changes if (Cipher.OrganizationId == null && !AllowPersonal) { Cipher.OrganizationId = OrganizationId; } } if (appOptions?.OtpData != null && Cipher.Type == CipherType.Login) { Cipher.Login.Totp = appOptions.OtpData.Value.Uri; } } else { Cipher = new CipherView { Name = DefaultName, OrganizationId = OrganizationId, FolderId = FolderId, Type = Type.GetValueOrDefault(CipherType.Login), Login = new LoginView(), Card = new CardView(), Identity = new IdentityView(), SecureNote = new SecureNoteView() }; Cipher.Login.Uris = new List { new LoginUriView { Uri = DefaultUri } }; Cipher.SecureNote.Type = SecureNoteType.Generic; if (appOptions != null) { Cipher.Type = appOptions.SaveType.GetValueOrDefault(Cipher.Type); Cipher.Login.Username = appOptions.SaveUsername; Cipher.Login.Password = appOptions.SavePassword; Cipher.Login.Totp = appOptions.OtpData?.Uri; Cipher.Card.Code = appOptions.SaveCardCode; if (int.TryParse(appOptions.SaveCardExpMonth, out int month) && month <= 12 && month >= 1) { Cipher.Card.ExpMonth = month.ToString(); } Cipher.Card.ExpYear = appOptions.SaveCardExpYear; Cipher.Card.CardholderName = appOptions.SaveCardName; Cipher.Card.Number = appOptions.SaveCardNumber; } } TypeSelectedIndex = TypeOptions.FindIndex(k => k.Value == Cipher.Type); FolderSelectedIndex = string.IsNullOrWhiteSpace(Cipher.FolderId) ? FolderOptions.Count - 1 : FolderOptions.FindIndex(k => k.Value == Cipher.FolderId); CardBrandSelectedIndex = string.IsNullOrWhiteSpace(Cipher.Card?.Brand) ? 0 : CardBrandOptions.FindIndex(k => k.Value == Cipher.Card.Brand); CardExpMonthSelectedIndex = string.IsNullOrWhiteSpace(Cipher.Card?.ExpMonth) ? 0 : CardExpMonthOptions.FindIndex(k => k.Value == Cipher.Card.ExpMonth); IdentityTitleSelectedIndex = string.IsNullOrWhiteSpace(Cipher.Identity?.Title) ? 0 : IdentityTitleOptions.FindIndex(k => k.Value == Cipher.Identity.Title); OwnershipSelectedIndex = string.IsNullOrWhiteSpace(Cipher.OrganizationId) ? 0 : OwnershipOptions.FindIndex(k => k.Value == Cipher.OrganizationId); // If the selected organization is on Index 0 and we've removed the personal option, force refresh if (!AllowPersonal && OwnershipSelectedIndex == 0) { OrganizationChanged(); } if ((!EditMode || CloneMode) && (CollectionIds?.Any() ?? false)) { foreach (var col in Collections) { col.Checked = CollectionIds.Contains(col.Collection.Id); } } if (Cipher.Login?.Uris != null) { Uris.ResetWithRange(Cipher.Login.Uris); } if (Cipher.Fields != null) { Fields.ResetWithRange(Cipher.Fields?.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, true, Cipher, null, null, FieldOptionsCommand))); } if (appOptions?.OtpData != null) { _platformUtilsService.ShowToast(null, AppResources.AuthenticatorKey, AppResources.AuthenticatorKeyAdded); } } if (EditMode && _previousCipherId != CipherId) { var task = _eventService.CollectAsync(EventType.Cipher_ClientViewed, CipherId); } _previousCipherId = CipherId; return true; } public async Task SubmitAsync() { if (Cipher == null) { return false; } if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); return false; } if (string.IsNullOrWhiteSpace(Cipher.Name)) { await Page.DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.Name), AppResources.Ok); return false; } if ((!EditMode || CloneMode) && !AllowPersonal && string.IsNullOrWhiteSpace(Cipher.OrganizationId)) { await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.PersonalOwnershipSubmitError, AppResources.Ok); return false; } Cipher.Fields = Fields != null && Fields.Any() ? Fields.Where(f => f != null).Select(f => f.Field).ToList() : null; if (Cipher.Login != null) { Cipher.Login.Uris = Uris?.ToList(); if ((!EditMode || CloneMode) && Cipher.Type == CipherType.Login && Cipher.Login.Uris != null && Cipher.Login.Uris.Count == 1 && string.IsNullOrWhiteSpace(Cipher.Login.Uris[0].Uri)) { Cipher.Login.Uris = null; } } if ((!EditMode || CloneMode) && Cipher.OrganizationId != null) { if (Collections == null || !Collections.Any(c => c != null && c.Checked)) { await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection, AppResources.Ok); return false; } Cipher.CollectionIds = Collections.Any() ? new HashSet(Collections.Where(c => c != null && c.Checked && c.Collection?.Id != null) .Select(c => c.Collection.Id)) : null; } if (CloneMode) { Cipher.Id = null; } var cipher = await _cipherService.EncryptAsync(Cipher); if (cipher == null) { return false; } try { await _deviceActionService.ShowLoadingAsync(AppResources.Saving); await _cipherService.SaveWithServerAsync(cipher); Cipher.Id = cipher.Id; await _deviceActionService.HideLoadingAsync(); _platformUtilsService.ShowToast("success", null, EditMode && !CloneMode ? AppResources.ItemUpdated : AppResources.NewItemCreated); _messagingService.Send(EditMode && !CloneMode ? "editedCipher" : "addedCipher", Cipher.Id); _watchDeviceService.SyncDataToWatchAsync().FireAndForget(); if (Page is CipherAddEditPage page && page.FromAutofillFramework) { // Close and go back to app _autofillHandler.CloseAutofill(); } else if (_fromOtp) { await _accountsManager.StartDefaultNavigationFlowAsync(op => op.OtpData = null); } else { if (CloneMode) { CipherDetailsPage?.UpdateCipherId(this.Cipher.Id); } // if the app is tombstoned then PopModalAsync would throw index out of bounds if (Page.Navigation?.ModalStack?.Count > 0) { await Page.Navigation.PopModalAsync(); } } return true; } catch (ApiException apiEx) { await _deviceActionService.HideLoadingAsync(); if (apiEx?.Error != null) { await _platformUtilsService.ShowDialogAsync(apiEx.Error.GetSingleMessage(), AppResources.AnErrorHasOccurred); } } catch (Exception genex) { _logger.Exception(genex); await _deviceActionService.HideLoadingAsync(); } return false; } public async Task DeleteAsync() { if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); return false; } var confirmed = await _platformUtilsService.ShowDialogAsync( AppResources.DoYouReallyWantToSoftDeleteCipher, null, AppResources.Yes, AppResources.Cancel); if (!confirmed) { return false; } try { await _deviceActionService.ShowLoadingAsync(AppResources.SoftDeleting); await _cipherService.SoftDeleteWithServerAsync(Cipher.Id); await _deviceActionService.HideLoadingAsync(); _platformUtilsService.ShowToast("success", null, AppResources.ItemSoftDeleted); _messagingService.Send("softDeletedCipher", Cipher); return true; } catch (ApiException e) { await _deviceActionService.HideLoadingAsync(); if (e?.Error != null) { await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(), AppResources.AnErrorHasOccurred); } } return false; } public async void GeneratePassword() { if (!string.IsNullOrWhiteSpace(Cipher?.Login?.Password)) { var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.PasswordOverrideAlert, null, AppResources.Yes, AppResources.No); if (!confirmed) { return; } } var page = new GeneratorPage(false, async (password) => { Cipher.Login.Password = password; TriggerCipherChanged(); await Page.Navigation.PopModalAsync(); }); await Page.Navigation.PushModalAsync(new NavigationPage(page)); } public async Task GenerateUsernameAsync() { if (!string.IsNullOrWhiteSpace(Cipher?.Login?.Username) && !await _platformUtilsService.ShowDialogAsync(AppResources.AreYouSureYouWantToOverwriteTheCurrentUsername, null, AppResources.Yes, AppResources.No)) { return; } var website = Cipher?.Login?.Uris?.FirstOrDefault()?.Host; var page = new GeneratorPage(false, async (username) => { try { Cipher.Login.Username = username; TriggerCipherChanged(); await Page.Navigation.PopModalAsync(); } catch (Exception ex) { OnGenerateUsernameException(ex); } }, isUsernameGenerator: true, emailWebsite: website, editMode: true); await Page.Navigation.PushModalAsync(new NavigationPage(page)); } public async void UriOptions(LoginUriView uri) { if (!(Page as CipherAddEditPage).DoOnce()) { return; } var selection = await Page.DisplayActionSheet(AppResources.Options, AppResources.Cancel, null, AppResources.MatchDetection, AppResources.Remove); if (selection == AppResources.Remove) { Uris.Remove(uri); } else if (selection == AppResources.MatchDetection) { var options = _matchDetectionOptions.Select(o => o.Key == uri.Match ? $"✓ {o.Value}" : o.Value); var matchSelection = await Page.DisplayActionSheet(AppResources.URIMatchDetection, AppResources.Cancel, null, options.ToArray()); if (matchSelection != null && matchSelection != AppResources.Cancel) { var matchSelectionClean = matchSelection.Replace("✓ ", string.Empty); uri.Match = _matchDetectionOptions.FirstOrDefault(o => o.Value == matchSelectionClean).Key; } } } public void AddUri() { if (Cipher.Type != CipherType.Login) { return; } if (Uris == null) { Uris = new ExtendedObservableCollection(); } Uris.Add(new LoginUriView()); } public async void FieldOptions(ICustomFieldItemViewModel field) { if (!(Page as CipherAddEditPage).DoOnce()) { return; } var selection = await Page.DisplayActionSheet(AppResources.Options, AppResources.Cancel, null, AppResources.Edit, AppResources.MoveUp, AppResources.MoveDown, AppResources.Remove); if (selection == AppResources.Remove) { Fields.Remove(field); } else if (selection == AppResources.Edit) { var name = await _deviceActionService.DisplayPromptAync(AppResources.CustomFieldName, null, field.Field.Name); field.Field.Name = name ?? field.Field.Name; field.TriggerFieldChanged(); } else if (selection == AppResources.MoveUp) { var currentIndex = Fields.IndexOf(field); if (currentIndex > 0) { Fields.Move(currentIndex, currentIndex - 1); } } else if (selection == AppResources.MoveDown) { var currentIndex = Fields.IndexOf(field); if (currentIndex < Fields.Count - 1) { Fields.Move(currentIndex, currentIndex + 1); } } } public async void AddField() { var fieldTypeOptions = new List> { new KeyValuePair(FieldType.Text, AppResources.FieldTypeText), new KeyValuePair(FieldType.Hidden, AppResources.FieldTypeHidden), new KeyValuePair(FieldType.Boolean, AppResources.FieldTypeBoolean), }; if (Cipher.LinkedFieldOptions != null) { fieldTypeOptions.Add(new KeyValuePair(FieldType.Linked, AppResources.FieldTypeLinked)); } var typeSelection = await Page.DisplayActionSheet(AppResources.SelectTypeField, AppResources.Cancel, null, fieldTypeOptions.Select(f => f.Value).ToArray()); if (typeSelection != null && typeSelection != AppResources.Cancel) { var name = await _deviceActionService.DisplayPromptAync(AppResources.CustomFieldName); if (name == null) { return; } if (Fields == null) { Fields = new ExtendedObservableCollection(); } var type = fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key; Fields.Add(_customFieldItemFactory.CreateCustomFieldItem(new FieldView { Type = type, Name = string.IsNullOrWhiteSpace(name) ? null : name, NewField = true, }, true, Cipher, null, null, FieldOptionsCommand)); } } public void TogglePassword() { ShowPassword = !ShowPassword; if (EditMode && ShowPassword) { var task = _eventService.CollectAsync(EventType.Cipher_ClientToggledPasswordVisible, CipherId); } } public void ToggleCardNumber() { ShowCardNumber = !ShowCardNumber; if (EditMode && ShowCardNumber) { var task = _eventService.CollectAsync( Core.Enums.EventType.Cipher_ClientToggledCardNumberVisible, CipherId); } } public void ToggleCardCode() { ShowCardCode = !ShowCardCode; if (EditMode && ShowCardCode) { var task = _eventService.CollectAsync(EventType.Cipher_ClientToggledCardCodeVisible, CipherId); } } public async Task UpdateTotpKeyAsync(string key) { if (Cipher?.Login != null) { if (!string.IsNullOrWhiteSpace(key)) { Cipher.Login.Totp = key; TriggerCipherChanged(); _platformUtilsService.ShowToast("info", null, AppResources.AuthenticatorKeyAdded); } else { await _platformUtilsService.ShowDialogAsync(AppResources.AuthenticatorKeyReadError); } } } public void PasswordPromptHelp() { _platformUtilsService.LaunchUri("https://bitwarden.com/help/managing-items/#protect-individual-items"); } private void TypeChanged() { if (Cipher != null && TypeSelectedIndex > -1) { Cipher.Type = TypeOptions[TypeSelectedIndex].Value; TriggerCipherChanged(); // Linked Custom Fields only apply to a specific item type foreach (var field in Fields.OfType().ToList()) { Fields.Remove(field); } } } private void CardBrandChanged() { if (Cipher?.Card != null && CardBrandSelectedIndex > -1) { Cipher.Card.Brand = CardBrandOptions[CardBrandSelectedIndex].Value; } } private void CardExpMonthChanged() { if (Cipher?.Card != null && CardExpMonthSelectedIndex > -1) { Cipher.Card.ExpMonth = CardExpMonthOptions[CardExpMonthSelectedIndex].Value; } } private void IdentityTitleChanged() { if (Cipher?.Identity != null && IdentityTitleSelectedIndex > -1) { Cipher.Identity.Title = IdentityTitleOptions[IdentityTitleSelectedIndex].Value; } } private void FolderChanged() { if (Cipher != null && FolderSelectedIndex > -1) { Cipher.FolderId = FolderOptions[FolderSelectedIndex].Value; } } private void OrganizationChanged() { if (Cipher != null && OwnershipSelectedIndex > -1) { Cipher.OrganizationId = OwnershipOptions[OwnershipSelectedIndex].Value; TriggerCipherChanged(); } if (Cipher.OrganizationId != null) { var cols = _writeableCollections.Where(c => c.OrganizationId == Cipher.OrganizationId) .Select(c => new CollectionViewModel { Collection = c }).ToList(); Collections.ResetWithRange(cols); } else { Collections.ResetWithRange(new List()); } HasCollections = Collections.Any(); } private void TriggerCipherChanged() { TriggerPropertyChanged(nameof(Cipher), AdditionalPropertiesToRaiseOnCipherChanged); } private async Task CopyTotpClipboardAsync() { try { await _clipboardService.CopyTextAsync(Cipher.Login.Totp); _platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.AuthenticatorKeyScanner)); } catch (Exception ex) { _logger.Exception(ex); } } private async void OnGenerateUsernameException(Exception ex) { _logger.Exception(ex); await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok); } } }