1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-05 23:53:33 +00:00
Files
mobile/src/App/Pages/Vault/CipherAddEditPageViewModel.cs
Álison Fernandes fa6bac3b43 EC-395 Apple Watch MVP (#2228)
* [EC-426] Add watchOS PoC app (#2054)

* EC-426 Added watchOS app, configured iOS.csproj to bundle the output of XCode build into the Xamarin iOS app and added some custom logic to use WCSession to communicate between the iOS and the watchOS apps

* EC-426 Removed Info.plist from iOS.Core project given that it's not needed

* [EC-426] Added new encrypted watch app profiles

* EC-426 added configuration for building watchApp and bundle it up on the iOS one

* EC-426 Fix build for watchOS

* EC-426 Fix build for watchOS applied shell bash

* EC-426 Fix build for watchOS echo

* EC-426 Fix build for watchOS simplify

* EC-426 Fix build for watchOS added workspace path

* EC-426 Changed code sign identity of watchOS project to Apple Distribution

* EC-426 added manual code sign style and specified the provisioning profile for the targets on the watch xcode project

* EC-426 updated path to watchOS on release on iOS.csproj and disabled android and f-.droid

* EC-426 fix build

* EC-426 fix path and check listing of directory of watchOS output just in case

* EC-426 Fix Apple Watch build to list the folder recursively just in case we need to change the path for the watch bundle

* EC-426 TEMP Change texts on input on login and lock to show that the app is for the Watch PoC testing

* EC-426 Fix WatchApp build path

* EC-426 Added WatchOS AppIcons

* EC-426 added gitignore for XCode project removed files supposed to be ignored

* EC-426 Cleaned the code a bit to avoid misbehavior

* EC-426 Code cleanup

Co-authored-by: Joseph Flinn <joseph.s.flinn@gmail.com>

* [EC-585] Added data, encryption and some helpers and structure to the Watch app (#2164)

* [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data

* EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode

* EC-585 Fixed CipherServiceMock to implement protocol

* EC-585 Fixed DeviceActionService duplicated services

* [EC-614] Apple Watch MVP Cipher list UI (#2175)

* [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data

* EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode

* EC-585 Fixed CipherServiceMock to implement protocol

* EC-585 Fixed DeviceActionService duplicated services

* EC-614 Implemented watch ciphers list UI

* [EC-615] Apple Watch MVP Cipher details UI (#2192)

* [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data

* EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode

* EC-585 Fixed CipherServiceMock to implement protocol

* EC-585 Fixed DeviceActionService duplicated services

* EC-614 Implemented watch ciphers list UI

* EC-615 Added cipher details UI to watch and also implemented logic and helpers to generate the TOTPs

* EC-615 Added value transformer to login uris on the cipher entity

* EC-617 Added state view on watch app and some state helpers and wired it on the CipherListView. Also added some images (#2195)

* [EC-581] Implement Apple Watch MVP Sync (#2206)

* EC-581 Implemented sync iPhone -> watchOS, fix some issues with the watch database and sync flows for login/locks/multiple accounts

* EC-581 Added watch sync on unlocking and need setup state when no user is synced and the session is not active

* EC-581 Removed unused method

* EC-581 Fix format

* EC-759 Added avatar row on cipher list header to display avatar icon and email (#2213)

* [EC-786] Apple Watch MVP Sync fixes (#2214)

* EC-786 Commented things that are not going to be included on the MVP and fixed issue on the dictionary sent on the applicationContext to have a changing key based on time

* EC-786 Commented need unlock state

* EC-579 Added logic for Connect To Watch on iOS settings and moved it to the correct place. Also improved the synchronization and watch session activation logic (#2218)

* EC-616 Added search header for ciphers and polished the code (#2226)

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: Joseph Flinn <joseph.s.flinn@gmail.com>
2022-12-07 11:39:20 -05:00

886 lines
38 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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 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<Core.Models.View.CollectionView> _writeableCollections;
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
{
nameof(IsLogin),
nameof(IsIdentity),
nameof(IsCard),
nameof(IsSecureNote),
nameof(ShowUris),
nameof(ShowAttachments),
nameof(ShowCollections),
nameof(HasTotpValue)
};
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
new List<KeyValuePair<UriMatchType?, string>>
{
new KeyValuePair<UriMatchType?, string>(null, AppResources.Default),
new KeyValuePair<UriMatchType?, string>(UriMatchType.Domain, AppResources.BaseDomain),
new KeyValuePair<UriMatchType?, string>(UriMatchType.Host, AppResources.Host),
new KeyValuePair<UriMatchType?, string>(UriMatchType.StartsWith, AppResources.StartsWith),
new KeyValuePair<UriMatchType?, string>(UriMatchType.RegularExpression, AppResources.RegEx),
new KeyValuePair<UriMatchType?, string>(UriMatchType.Exact, AppResources.Exact),
new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never)
};
public CipherAddEditPageViewModel()
{
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
ToggleCardNumberCommand = new Command(ToggleCardNumber);
ToggleCardCodeCommand = new Command(ToggleCardCode);
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
FieldOptionsCommand = new Command<ICustomFieldItemViewModel>(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<LoginUriView>();
Fields = new ExtendedObservableCollection<ICustomFieldItemViewModel>();
Collections = new ExtendedObservableCollection<CollectionViewModel>();
AllowPersonal = true;
TypeOptions = new List<KeyValuePair<string, CipherType>>
{
new KeyValuePair<string, CipherType>(AppResources.TypeLogin, CipherType.Login),
new KeyValuePair<string, CipherType>(AppResources.TypeCard, CipherType.Card),
new KeyValuePair<string, CipherType>(AppResources.TypeIdentity, CipherType.Identity),
new KeyValuePair<string, CipherType>(AppResources.TypeSecureNote, CipherType.SecureNote),
};
CardBrandOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>($"-- {AppResources.Select} --", null),
new KeyValuePair<string, string>("Visa", "Visa"),
new KeyValuePair<string, string>("Mastercard", "Mastercard"),
new KeyValuePair<string, string>("American Express", "Amex"),
new KeyValuePair<string, string>("Discover", "Discover"),
new KeyValuePair<string, string>("Diners Club", "Diners Club"),
new KeyValuePair<string, string>("JCB", "JCB"),
new KeyValuePair<string, string>("Maestro", "Maestro"),
new KeyValuePair<string, string>("UnionPay", "UnionPay"),
new KeyValuePair<string, string>(AppResources.Other, "Other")
};
CardExpMonthOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>($"-- {AppResources.Select} --", null),
new KeyValuePair<string, string>($"01 - {AppResources.January}", "1"),
new KeyValuePair<string, string>($"02 - {AppResources.February}", "2"),
new KeyValuePair<string, string>($"03 - {AppResources.March}", "3"),
new KeyValuePair<string, string>($"04 - {AppResources.April}", "4"),
new KeyValuePair<string, string>($"05 - {AppResources.May}", "5"),
new KeyValuePair<string, string>($"06 - {AppResources.June}", "6"),
new KeyValuePair<string, string>($"07 - {AppResources.July}", "7"),
new KeyValuePair<string, string>($"08 - {AppResources.August}", "8"),
new KeyValuePair<string, string>($"09 - {AppResources.September}", "9"),
new KeyValuePair<string, string>($"10 - {AppResources.October}", "10"),
new KeyValuePair<string, string>($"11 - {AppResources.November}", "11"),
new KeyValuePair<string, string>($"12 - {AppResources.December}", "12")
};
IdentityTitleOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>($"-- {AppResources.Select} --", null),
new KeyValuePair<string, string>(AppResources.Mr, AppResources.Mr),
new KeyValuePair<string, string>(AppResources.Mrs, AppResources.Mrs),
new KeyValuePair<string, string>(AppResources.Ms, AppResources.Ms),
new KeyValuePair<string, string>(AppResources.Dr, AppResources.Dr),
};
FolderOptions = new List<KeyValuePair<string, string>>();
OwnershipOptions = new List<KeyValuePair<string, string>>();
}
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<string> CollectionIds { get; set; }
public string DefaultName { get; set; }
public string DefaultUri { get; set; }
public List<KeyValuePair<string, CipherType>> TypeOptions { get; set; }
public List<KeyValuePair<string, string>> CardBrandOptions { get; set; }
public List<KeyValuePair<string, string>> CardExpMonthOptions { get; set; }
public List<KeyValuePair<string, string>> IdentityTitleOptions { get; set; }
public List<KeyValuePair<string, string>> FolderOptions { get; set; }
public List<KeyValuePair<string, string>> OwnershipOptions { get; set; }
public ExtendedObservableCollection<LoginUriView> Uris { get; set; }
public ExtendedObservableCollection<ICustomFieldItemViewModel> Fields { get; set; }
public ExtendedObservableCollection<CollectionViewModel> 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<bool> LoadAsync(AppOptions appOptions = null)
{
var myEmail = await _stateService.GetEmailAsync();
OwnershipOptions.Add(new KeyValuePair<string, string>(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<string, string>(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<string, string>(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<LoginUriView> { 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 => _customFieldItemFactory.CreateCustomFieldItem(f, true, Cipher, null, null, FieldOptionsCommand)));
}
}
if (EditMode && _previousCipherId != CipherId)
{
var task = _eventService.CollectAsync(EventType.Cipher_ClientViewed, CipherId);
}
_previousCipherId = CipherId;
return true;
}
public async Task<bool> 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<string>(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 (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<bool> 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<LoginUriView>();
}
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<KeyValuePair<FieldType, string>>
{
new KeyValuePair<FieldType, string>(FieldType.Text, AppResources.FieldTypeText),
new KeyValuePair<FieldType, string>(FieldType.Hidden, AppResources.FieldTypeHidden),
new KeyValuePair<FieldType, string>(FieldType.Boolean, AppResources.FieldTypeBoolean),
};
if (Cipher.LinkedFieldOptions != null)
{
fieldTypeOptions.Add(new KeyValuePair<FieldType, string>(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<ICustomFieldItemViewModel>();
}
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<LinkedCustomFieldItemViewModel>().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<CollectionViewModel>());
}
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);
}
}
}