using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Bit.App.Abstractions; 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.Forms; namespace Bit.App.Pages { public class AddEditPageViewModel : BaseViewModel { private readonly IDeviceActionService _deviceActionService; private readonly ICipherService _cipherService; private readonly IFolderService _folderService; private readonly ICollectionService _collectionService; private readonly IStateService _stateService; private readonly IOrganizationService _organizationService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IAuditService _auditService; private readonly IMessagingService _messagingService; private readonly IEventService _eventService; private readonly IPolicyService _policyService; private readonly ILogger _logger; private CipherView _cipher; 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 string[] _additionalCipherProperties = new string[] { nameof(IsLogin), nameof(IsIdentity), nameof(IsCard), nameof(IsSecureNote), nameof(ShowUris), nameof(ShowAttachments), nameof(ShowCollections), }; 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 AddEditPageViewModel() { _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _cipherService = ServiceContainer.Resolve("cipherService"); _folderService = ServiceContainer.Resolve("folderService"); _stateService = ServiceContainer.Resolve("stateService"); _organizationService = ServiceContainer.Resolve("organizationService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _auditService = ServiceContainer.Resolve("auditService"); _messagingService = ServiceContainer.Resolve("messagingService"); _collectionService = ServiceContainer.Resolve("collectionService"); _eventService = ServiceContainer.Resolve("eventService"); _policyService = ServiceContainer.Resolve("policyService"); _logger = ServiceContainer.Resolve("logger"); GeneratePasswordCommand = new Command(GeneratePassword); TogglePasswordCommand = new Command(TogglePassword); ToggleCardNumberCommand = new Command(ToggleCardNumber); ToggleCardCodeCommand = new Command(ToggleCardCode); CheckPasswordCommand = new Command(CheckPasswordAsync); UriOptionsCommand = new Command(UriOptions); FieldOptionsCommand = new Command(FieldOptions); PasswordPromptHelpCommand = new Command(PasswordPromptHelp); 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.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 CheckPasswordCommand { get; set; } public Command UriOptionsCommand { get; set; } public Command FieldOptionsCommand { get; set; } public Command PasswordPromptHelpCommand { 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 CipherView Cipher { get => _cipher; set => SetProperty(ref _cipher, value, additionalPropertyNames: _additionalCipherProperties); } 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) }); } 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 ViewPage ViewPage { 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 void Init() { PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem; } public async Task LoadAsync(AppOptions appOptions = 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; } } } 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.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 => new AddEditPageFieldViewModel(Cipher, f))); } } 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); if (Page is AddEditPage page && page.FromAutofillFramework) { // Close and go back to app _deviceActionService.CloseAutofill(); } else { if (CloneMode) { ViewPage?.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 void UriOptions(LoginUriView uri) { if (!(Page as AddEditPage).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(AddEditPageFieldViewModel field) { if (!(Page as AddEditPage).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(new AddEditPageFieldViewModel(Cipher, new FieldView { Type = type, Name = string.IsNullOrWhiteSpace(name) ? null : name, NewField = true, })); } } 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.Where(f => f.IsLinkedType).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), _additionalCipherProperties); } private async void CheckPasswordAsync() { if (!(Page as BaseContentPage).DoOnce()) { return; } if (string.IsNullOrWhiteSpace(Cipher.Login?.Password)) { return; } await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword); var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password); await _deviceActionService.HideLoadingAsync(); if (matches > 0) { await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed, matches.ToString("N0"))); } else { await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe); } } } public class AddEditPageFieldViewModel : ExtendedViewModel { private II18nService _i18nService; private FieldView _field; private CipherView _cipher; private bool _showHiddenValue; private bool _booleanValue; private int _linkedFieldOptionSelectedIndex; private string[] _additionalFieldProperties = new string[] { nameof(IsBooleanType), nameof(IsHiddenType), nameof(IsTextType), nameof(IsLinkedType), }; public AddEditPageFieldViewModel(CipherView cipher, FieldView field) { _i18nService = ServiceContainer.Resolve("i18nService"); _cipher = cipher; Field = field; ToggleHiddenValueCommand = new Command(ToggleHiddenValue); BooleanValue = IsBooleanType && field.Value == "true"; LinkedFieldOptionSelectedIndex = !Field.LinkedId.HasValue ? 0 : LinkedFieldOptions.FindIndex(lfo => lfo.Value == Field.LinkedId.Value); } public FieldView Field { get => _field; set => SetProperty(ref _field, value, additionalPropertyNames: _additionalFieldProperties); } public bool ShowHiddenValue { get => _showHiddenValue; set => SetProperty(ref _showHiddenValue, value, additionalPropertyNames: new string[] { nameof(ShowHiddenValueIcon) }); } public bool BooleanValue { get => _booleanValue; set { SetProperty(ref _booleanValue, value); if (IsBooleanType) { Field.Value = value ? "true" : "false"; } } } public int LinkedFieldOptionSelectedIndex { get => _linkedFieldOptionSelectedIndex; set { if (SetProperty(ref _linkedFieldOptionSelectedIndex, value)) { LinkedFieldValueChanged(); } } } public List> LinkedFieldOptions { get => _cipher.LinkedFieldOptions? .Select(kvp => new KeyValuePair(_i18nService.T(kvp.Key), kvp.Value)) .ToList(); } public Command ToggleHiddenValueCommand { get; set; } public string ShowHiddenValueIcon => _showHiddenValue ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public bool IsTextType => _field.Type == FieldType.Text; public bool IsBooleanType => _field.Type == FieldType.Boolean; public bool IsHiddenType => _field.Type == FieldType.Hidden; public bool IsLinkedType => _field.Type == FieldType.Linked; public bool ShowViewHidden => IsHiddenType && (_cipher.ViewPassword || _field.NewField); public void ToggleHiddenValue() { ShowHiddenValue = !ShowHiddenValue; if (ShowHiddenValue && _cipher?.Id != null) { var eventService = ServiceContainer.Resolve("eventService"); var task = eventService.CollectAsync(EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id); } } public void TriggerFieldChanged() { TriggerPropertyChanged(nameof(Field), _additionalFieldProperties); } private void LinkedFieldValueChanged() { if (Field != null && LinkedFieldOptionSelectedIndex > -1) { Field.LinkedId = LinkedFieldOptions.Find(lfo => lfo.Value == LinkedFieldOptions[LinkedFieldOptionSelectedIndex].Value).Value; } } } }