diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index 2e25bc1cc..f4fc640d0 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -170,7 +170,7 @@ namespace Bit.Droid { if (intent?.GetStringExtra("uri") is string uri) { - _messagingService.Send("popAllAndGoToAutofillCiphers"); + _messagingService.Send(App.App.POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE); if (_appOptions != null) { _appOptions.Uri = uri; @@ -178,7 +178,7 @@ namespace Bit.Droid } else if (intent.GetBooleanExtra("generatorTile", false)) { - _messagingService.Send("popAllAndGoToTabGenerator"); + _messagingService.Send(App.App.POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE); if (_appOptions != null) { _appOptions.GeneratorTile = true; @@ -186,7 +186,7 @@ namespace Bit.Droid } else if (intent.GetBooleanExtra("myVaultTile", false)) { - _messagingService.Send("popAllAndGoToTabMyVault"); + _messagingService.Send(App.App.POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE); if (_appOptions != null) { _appOptions.MyVaultTile = true; @@ -198,7 +198,7 @@ namespace Bit.Droid { _appOptions.CreateSend = GetCreateSendRequest(intent); } - _messagingService.Send("popAllAndGoToTabSend"); + _messagingService.Send(App.App.POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE); } else { diff --git a/src/App/Abstractions/IAccountsManager.cs b/src/App/Abstractions/IAccountsManager.cs index 532b75958..ab78c6ca4 100644 --- a/src/App/Abstractions/IAccountsManager.cs +++ b/src/App/Abstractions/IAccountsManager.cs @@ -8,6 +8,7 @@ namespace Bit.App.Abstractions { void Init(Func getOptionsFunc, IAccountsManagerHost accountsManagerHost); Task NavigateOnAccountChangeAsync(bool? isAuthed = null); + Task StartDefaultNavigationFlowAsync(Action appOptionsAction); Task LogOutAsync(string userId, bool userInitiated, bool expired); Task PromptToSwitchToExistingAccountAsync(string userId); } diff --git a/src/App/Abstractions/IDeepLinkContext.cs b/src/App/Abstractions/IDeepLinkContext.cs new file mode 100644 index 000000000..345596d50 --- /dev/null +++ b/src/App/Abstractions/IDeepLinkContext.cs @@ -0,0 +1,9 @@ +using System; + +namespace Bit.App.Abstractions +{ + public interface IDeepLinkContext + { + bool OnNewUri(Uri uri); + } +} diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index 095d5778a..3cbc4ff52 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -23,6 +23,11 @@ namespace Bit.App { public partial class App : Application, IAccountsManagerHost { + public const string POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE = "popAllAndGoToTabGenerator"; + public const string POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE = "popAllAndGoToTabMyVault"; + public const string POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE = "popAllAndGoToTabSend"; + public const string POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE = "popAllAndGoToAutofillCiphers"; + private readonly IBroadcasterService _broadcasterService; private readonly IMessagingService _messagingService; private readonly IStateService _stateService; @@ -103,12 +108,18 @@ namespace Bit.App await Task.Delay(1000); await _accountsManager.NavigateOnAccountChangeAsync(); } - else if (message.Command == "popAllAndGoToTabGenerator" || - message.Command == "popAllAndGoToTabMyVault" || - message.Command == "popAllAndGoToTabSend" || - message.Command == "popAllAndGoToAutofillCiphers") + else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE || + message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE || + message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE || + message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE || + message.Command == DeepLinkContext.NEW_OTP_MESSAGE) { - Device.BeginInvokeOnMainThread(async () => + if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE) + { + Options.OtpData = new OtpData((string)message.Data); + } + + await Device.InvokeOnMainThreadAsync(async () => { if (Current.MainPage is TabsPage tabsPage) { @@ -116,24 +127,29 @@ namespace Bit.App { await tabsPage.Navigation.PopModalAsync(false); } - if (message.Command == "popAllAndGoToAutofillCiphers") + if (message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE) { - Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options)); + Current.MainPage = new NavigationPage(new CipherSelectionPage(Options)); } - else if (message.Command == "popAllAndGoToTabMyVault") + else if (message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE) { Options.MyVaultTile = false; tabsPage.ResetToVaultPage(); } - else if (message.Command == "popAllAndGoToTabGenerator") + else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE) { Options.GeneratorTile = false; tabsPage.ResetToGeneratorPage(); } - else if (message.Command == "popAllAndGoToTabSend") + else if (message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE) { tabsPage.ResetToSendPage(); } + else if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE) + { + tabsPage.ResetToVaultPage(); + await tabsPage.Navigation.PushModalAsync(new NavigationPage(new CipherSelectionPage(Options))); + } } }); } @@ -494,7 +510,8 @@ namespace Bit.App Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options)); break; case NavigationTarget.AutofillCiphers: - Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options)); + case NavigationTarget.OtpCipherSelection: + Current.MainPage = new NavigationPage(new CipherSelectionPage(Options)); break; case NavigationTarget.SendAddEdit: Current.MainPage = new NavigationPage(new SendAddEditPage(Options)); diff --git a/src/App/Models/AppOptions.cs b/src/App/Models/AppOptions.cs index 0a251d7cd..58fe79d49 100644 --- a/src/App/Models/AppOptions.cs +++ b/src/App/Models/AppOptions.cs @@ -1,5 +1,6 @@ using System; using Bit.Core.Enums; +using Bit.Core.Utilities; namespace Bit.App.Models { @@ -23,6 +24,7 @@ namespace Bit.App.Models public Tuple CreateSend { get; set; } public bool CopyInsteadOfShareAfterSaving { get; set; } public bool HideAccountSwitcher { get; set; } + public OtpData? OtpData { get; set; } public void SetAllFrom(AppOptions o) { @@ -48,6 +50,7 @@ namespace Bit.App.Models CreateSend = o.CreateSend; CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving; HideAccountSwitcher = o.HideAccountSwitcher; + OtpData = o.OtpData; } } } diff --git a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs index 525f7dea1..aade36dd6 100644 --- a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs +++ b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs @@ -1,91 +1,29 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Bit.App.Abstractions; -using Bit.App.Controls; using Bit.App.Models; using Bit.App.Resources; using Bit.App.Utilities; 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 AutofillCiphersPageViewModel : BaseViewModel + public class AutofillCiphersPageViewModel : CipherSelectionPageViewModel { - private readonly IPlatformUtilsService _platformUtilsService; - private readonly IDeviceActionService _deviceActionService; - private readonly IAutofillHandler _autofillHandler; - private readonly ICipherService _cipherService; - private readonly IStateService _stateService; - private readonly IPasswordRepromptService _passwordRepromptService; - private readonly IMessagingService _messagingService; - private readonly ILogger _logger; + private CipherType? _fillType; - private bool _showNoData; - private bool _showList; - private string _noDataText; - private bool _websiteIconsEnabled; - - public AutofillCiphersPageViewModel() - { - _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - _cipherService = ServiceContainer.Resolve("cipherService"); - _deviceActionService = ServiceContainer.Resolve("deviceActionService"); - _autofillHandler = ServiceContainer.Resolve(); - _stateService = ServiceContainer.Resolve("stateService"); - _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); - _messagingService = ServiceContainer.Resolve("messagingService"); - _logger = ServiceContainer.Resolve("logger"); - - GroupedItems = new ObservableRangeCollection(); - CipherOptionsCommand = new Command(CipherOptionsAsync); - - AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger) - { - AllowAddAccountRow = false - }; - } - - public string Name { get; set; } public string Uri { get; set; } - public Command CipherOptionsCommand { get; set; } - public bool LoadedOnce { get; set; } - public ObservableRangeCollection GroupedItems { get; set; } - public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; } - public bool ShowNoData - { - get => _showNoData; - set => SetProperty(ref _showNoData, value); - } - - public bool ShowList - { - get => _showList; - set => SetProperty(ref _showList, value); - } - - public string NoDataText - { - get => _noDataText; - set => SetProperty(ref _noDataText, value); - } - public bool WebsiteIconsEnabled - { - get => _websiteIconsEnabled; - set => SetProperty(ref _websiteIconsEnabled, value); - } - - public void Init(AppOptions appOptions) + public override void Init(AppOptions appOptions) { Uri = appOptions?.Uri; + _fillType = appOptions.FillType; + string name = null; if (Uri?.StartsWith(Constants.AndroidAppProtocol) ?? false) { @@ -104,14 +42,11 @@ namespace Bit.App.Pages NoDataText = string.Format(AppResources.NoItemsForUri, Name ?? "--"); } - public async Task LoadAsync() + protected override async Task> LoadGroupedItemsAsync() { - LoadedOnce = true; - ShowList = false; - ShowNoData = false; - WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault(); var groupedItems = new List(); var ciphers = await _cipherService.GetAllDecryptedByUrlAsync(Uri, null); + var matching = ciphers.Item1?.Select(c => new GroupingsPageListItem { Cipher = c }).ToList(); var hasMatching = matching?.Any() ?? false; if (matching?.Any() ?? false) @@ -119,6 +54,7 @@ namespace Bit.App.Pages groupedItems.Add( new GroupingsPageListGroup(matching, AppResources.MatchingItems, matching.Count, false, true)); } + var fuzzy = ciphers.Item2?.Select(c => new GroupingsPageListItem { Cipher = c, FuzzyAutofill = true }).ToList(); if (fuzzy?.Any() ?? false) @@ -128,123 +64,88 @@ namespace Bit.App.Pages !hasMatching)); } - // TODO: refactor this - if (Device.RuntimePlatform == Device.Android - || - GroupedItems.Any()) - { - var items = new List(); - foreach (var itemGroup in groupedItems) - { - items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount)); - items.AddRange(itemGroup); - } - - GroupedItems.ReplaceRange(items); - } - else - { - // HACK: we need this on iOS, so that it doesn't crash when adding coming from an empty list - var first = true; - var items = new List(); - foreach (var itemGroup in groupedItems) - { - if (!first) - { - items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount)); - } - else - { - first = false; - } - items.AddRange(itemGroup); - } - - if (groupedItems.Any()) - { - GroupedItems.ReplaceRange(new List { new GroupingsPageHeaderListItem(groupedItems[0].Name, groupedItems[0].ItemCount) }); - GroupedItems.AddRange(items); - } - else - { - GroupedItems.Clear(); - } - } - ShowList = groupedItems.Any(); - ShowNoData = !ShowList; + return groupedItems; } - public async Task SelectCipherAsync(CipherView cipher, bool fuzzy) + protected override async Task SelectCipherAsync(IGroupingsPageListItem item) { - if (cipher == null) + if (!(item is GroupingsPageListItem listItem) || listItem.Cipher is null) { return; } + + var cipher = listItem.Cipher; + if (_deviceActionService.SystemMajorVersion() < 21) { await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); + return; } - else + + if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync()) { - if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync()) + return; + } + var autofillResponse = AppResources.Yes; + if (listItem.FuzzyAutofill) + { + var options = new List { AppResources.Yes }; + if (cipher.Type == CipherType.Login && + Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None) { - return; + options.Add(AppResources.YesAndSave); } - var autofillResponse = AppResources.Yes; - if (fuzzy) + autofillResponse = await _deviceActionService.DisplayAlertAsync(null, + string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, Name), AppResources.No, + options.ToArray()); + } + if (autofillResponse == AppResources.YesAndSave && cipher.Type == CipherType.Login) + { + var uris = cipher.Login?.Uris?.ToList(); + if (uris == null) { - var options = new List { AppResources.Yes }; - if (cipher.Type == CipherType.Login && - Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None) - { - options.Add(AppResources.YesAndSave); - } - autofillResponse = await _deviceActionService.DisplayAlertAsync(null, - string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, Name), AppResources.No, - options.ToArray()); + uris = new List(); } - if (autofillResponse == AppResources.YesAndSave && cipher.Type == CipherType.Login) + uris.Add(new LoginUriView { - var uris = cipher.Login?.Uris?.ToList(); - if (uris == null) + Uri = Uri, + Match = null + }); + cipher.Login.Uris = uris; + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Saving); + await _cipherService.SaveWithServerAsync(await _cipherService.EncryptAsync(cipher)); + await _deviceActionService.HideLoadingAsync(); + } + catch (ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + if (e?.Error != null) { - uris = new List(); - } - uris.Add(new LoginUriView - { - Uri = Uri, - Match = null - }); - cipher.Login.Uris = uris; - try - { - await _deviceActionService.ShowLoadingAsync(AppResources.Saving); - await _cipherService.SaveWithServerAsync(await _cipherService.EncryptAsync(cipher)); - await _deviceActionService.HideLoadingAsync(); - } - catch (ApiException e) - { - await _deviceActionService.HideLoadingAsync(); - if (e?.Error != null) - { - await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(), - AppResources.AnErrorHasOccurred); - } + await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(), + AppResources.AnErrorHasOccurred); } } - if (autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave) - { - _autofillHandler.Autofill(cipher); - } + } + if (autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave) + { + _autofillHandler.Autofill(cipher); } } - private async void CipherOptionsAsync(CipherView cipher) + protected override async Task AddCipherAsync() { - if ((Page as BaseContentPage).DoOnce()) + if (_fillType.HasValue && _fillType != CipherType.Login) { - await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); + var pageForOther = new CipherAddEditPage(type: _fillType, fromAutofill: true); + await Page.Navigation.PushModalAsync(new NavigationPage(pageForOther)); + return; } + + var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: Uri, name: Name, + fromAutofill: true); + await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin)); } } } diff --git a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs index cbc3838e5..e0a6cc226 100644 --- a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs +++ b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs @@ -2,6 +2,7 @@ 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; @@ -30,6 +31,7 @@ namespace Bit.App.Pages private readonly IClipboardService _clipboardService; private readonly IAutofillHandler _autofillHandler; private readonly IWatchDeviceService _watchDeviceService; + private readonly IAccountsManager _accountsManager; private bool _showNotesSeparator; private bool _showPassword; @@ -44,6 +46,8 @@ namespace Bit.App.Pages private bool _hasCollections; private string _previousCipherId; private List _writeableCollections; + private bool _fromOtp; + protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[] { nameof(IsLogin), @@ -82,6 +86,8 @@ namespace Bit.App.Pages _clipboardService = ServiceContainer.Resolve("clipboardService"); _autofillHandler = ServiceContainer.Resolve(); _watchDeviceService = ServiceContainer.Resolve(); + _accountsManager = ServiceContainer.Resolve(); + GeneratePasswordCommand = new Command(GeneratePassword); TogglePasswordCommand = new Command(TogglePassword); @@ -302,6 +308,7 @@ namespace Bit.App.Pages 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; @@ -309,6 +316,8 @@ namespace Bit.App.Pages 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(); @@ -358,6 +367,10 @@ namespace Bit.App.Pages Cipher.OrganizationId = OrganizationId; } } + if (appOptions?.OtpData != null && Cipher.Type == CipherType.Login) + { + Cipher.Login.Totp = appOptions.OtpData.Value.Uri; + } } else { @@ -380,6 +393,7 @@ namespace Bit.App.Pages 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) { @@ -424,6 +438,11 @@ namespace Bit.App.Pages { 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) @@ -517,6 +536,10 @@ namespace Bit.App.Pages // Close and go back to app _autofillHandler.CloseAutofill(); } + else if (_fromOtp) + { + await _accountsManager.StartDefaultNavigationFlowAsync(op => op.OtpData = null); + } else { if (CloneMode) diff --git a/src/App/Pages/Vault/AutofillCiphersPage.xaml b/src/App/Pages/Vault/CipherSelectionPage.xaml similarity index 68% rename from src/App/Pages/Vault/AutofillCiphersPage.xaml rename to src/App/Pages/Vault/CipherSelectionPage.xaml index e1cc992ff..979392e3b 100644 --- a/src/App/Pages/Vault/AutofillCiphersPage.xaml +++ b/src/App/Pages/Vault/CipherSelectionPage.xaml @@ -1,30 +1,26 @@  - - - - - - @@ -32,6 +28,21 @@ + + + + @@ -70,23 +81,44 @@ Padding="20, 0" Spacing="20" IsVisible="{Binding ShowNoData}"> + + Command="{Binding AddCipherCommand}" /> + + @@ -104,9 +136,10 @@