From 79a76c46385d61fad6d707561035d15c0771d15e Mon Sep 17 00:00:00 2001 From: Matt Portune <59324545+mportune-bw@users.noreply.github.com> Date: Mon, 7 Mar 2022 12:28:06 -0500 Subject: [PATCH] Support for lock/logout/remove accounts from account list (#1826) * Support for lock/logout/remove accounts via long-press * establish and set listview height before showing * undo modification --- src/Android/Android.csproj | 2 +- src/App/App.csproj | 6 +- src/App/App.xaml.cs | 50 ++++++------ .../AccountSwitchingOverlayView.xaml | 8 +- .../AccountSwitchingOverlayView.xaml.cs | 67 ++++++++++++++-- .../AccountSwitchingOverlayViewModel.cs | 19 ++++- .../AccountViewCell/AccountViewCell.xaml | 14 +++- .../AccountViewCell/AccountViewCell.xaml.cs | 21 ++++- .../AccountViewCellViewModel.cs | 15 ++++ src/App/Pages/Accounts/HomePage.xaml | 2 + src/App/Pages/Accounts/LockPage.xaml | 2 + src/App/Pages/Accounts/LoginPage.xaml | 2 + .../Vault/GroupingsPage/GroupingsPage.xaml | 1 + src/App/Resources/AppResources.Designer.cs | 24 ++++++ src/App/Resources/AppResources.resx | 9 +++ src/App/Utilities/AppHelpers.cs | 77 ++++++++++++++++++- src/Core/Abstractions/IStateService.cs | 1 + src/Core/Abstractions/IVaultTimeoutService.cs | 2 + src/Core/Services/EnvironmentService.cs | 1 + src/Core/Services/StateService.cs | 26 ++++--- src/Core/Services/VaultTimeoutService.cs | 50 +++++++++--- src/Core/Utilities/ServiceContainer.cs | 8 +- .../iOS.ShareExtension.csproj | 2 +- src/iOS/iOS.csproj | 2 +- 24 files changed, 342 insertions(+), 69 deletions(-) diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 52e4f9646..bd3ef91fa 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -84,7 +84,7 @@ - 1.7.0 + 1.7.1 122.0.0 diff --git a/src/App/App.csproj b/src/App/App.csproj index b335c3a11..8894ca5ae 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -16,10 +16,10 @@ - - + + - + diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index 0dada2232..bb0083155 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -73,7 +73,11 @@ namespace Bit.App } else if (message.Command == "locked") { - await LockedAsync(!(message.Data as bool?).GetValueOrDefault()); + var extras = message.Data as Tuple; + var userId = extras?.Item1; + var userInitiated = extras?.Item2; + Device.BeginInvokeOnMainThread(async () => + await LockedAsync(userId, userInitiated.GetValueOrDefault())); } else if (message.Command == "lockVault") { @@ -283,10 +287,9 @@ namespace Bit.App private async Task SwitchedAccountAsync() { await AppHelpers.OnAccountSwitchAsync(); - var shouldTimeout = await _vaultTimeoutService.ShouldTimeoutAsync(); Device.BeginInvokeOnMainThread(async () => { - if (shouldTimeout) + if (await _vaultTimeoutService.ShouldTimeoutAsync()) { await _vaultTimeoutService.ExecuteTimeoutActionAsync(); } @@ -304,24 +307,20 @@ namespace Bit.App var authed = await _stateService.IsAuthenticatedAsync(); if (authed) { - var isLocked = await _vaultTimeoutService.IsLockedAsync(); - var shouldTimeout = await _vaultTimeoutService.ShouldTimeoutAsync(); - if (isLocked || shouldTimeout) + if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() || + await _vaultTimeoutService.ShouldLogOutByTimeoutAsync()) { - var vaultTimeoutAction = await _stateService.GetVaultTimeoutActionAsync(); - if (vaultTimeoutAction == VaultTimeoutAction.Logout) - { - // TODO implement orgIdentifier flow to SSO Login page, same as email flow below - // var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); - - var email = await _stateService.GetEmailAsync(); - Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; - Current.MainPage = new NavigationPage(new LoginPage(email, Options)); - } - else - { - Current.MainPage = new NavigationPage(new LockPage(Options)); - } + // TODO implement orgIdentifier flow to SSO Login page, same as email flow below + // var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); + + var email = await _stateService.GetEmailAsync(); + Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; + Current.MainPage = new NavigationPage(new LoginPage(email, Options)); + } + else if (await _vaultTimeoutService.IsLockedAsync() || + await _vaultTimeoutService.ShouldLockAsync()) + { + Current.MainPage = new NavigationPage(new LockPage(Options)); } else if (Options.FromAutofillFramework && Options.SaveType.HasValue) { @@ -343,7 +342,8 @@ namespace Bit.App else { Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; - if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync()) + if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() || + await _vaultTimeoutService.ShouldLogOutByTimeoutAsync()) { // TODO implement orgIdentifier flow to SSO Login page, same as email flow below // var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); @@ -430,8 +430,14 @@ namespace Bit.App }); } - private async Task LockedAsync(bool autoPromptBiometric) + private async Task LockedAsync(string userId, bool autoPromptBiometric) { + if (!await _stateService.IsActiveAccount(userId)) + { + _platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully); + return; + } + if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS) { var vaultTimeout = await _stateService.GetVaultTimeoutAsync(); diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml index 0edbf4503..f0c52d0d4 100644 --- a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml @@ -27,15 +27,17 @@ + Account="{Binding .}" + SelectAccountCommand="{Binding SelectAccountCommand, Source={x:Reference _mainOverlay}}" + LongPressAccountCommand="{Binding LongPressAccountCommand, Source={x:Reference _mainOverlay}}" + /> diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs index b4af21f98..33b07ec8e 100644 --- a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs @@ -10,12 +10,24 @@ namespace Bit.App.Controls { public partial class AccountSwitchingOverlayView : ContentView { + public static readonly BindableProperty MainPageProperty = BindableProperty.Create( + nameof(MainPage), + typeof(ContentPage), + typeof(AccountSwitchingOverlayView), + defaultBindingMode: BindingMode.OneWay); + public static readonly BindableProperty MainFabProperty = BindableProperty.Create( nameof(MainFab), typeof(View), typeof(AccountSwitchingOverlayView), defaultBindingMode: BindingMode.OneWay); + public ContentPage MainPage + { + get => (ContentPage)GetValue(MainPageProperty); + set => SetValue(MainPageProperty, value); + } + public View MainFab { get => (View)GetValue(MainFabProperty); @@ -31,12 +43,26 @@ namespace Bit.App.Controls ToggleVisibililtyCommand = new AsyncCommand(ToggleVisibilityAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false); + + SelectAccountCommand = new AsyncCommand(SelectAccountAsync, + onException: ex => _logger.Value.Exception(ex), + allowsMultipleExecutions: false); + + LongPressAccountCommand = new AsyncCommand(LongPressAccountAsync, + onException: ex => _logger.Value.Exception(ex), + allowsMultipleExecutions: false); } public AccountSwitchingOverlayViewModel ViewModel => BindingContext as AccountSwitchingOverlayViewModel; public ICommand ToggleVisibililtyCommand { get; } + public ICommand SelectAccountCommand { get; } + + public ICommand LongPressAccountCommand { get; } + + public int AccountListRowHeight => Device.RuntimePlatform == Device.Android ? 74 : 70; + public async Task ToggleVisibilityAsync() { if (IsVisible) @@ -51,13 +77,24 @@ namespace Bit.App.Controls public async Task ShowAsync() { - await ViewModel?.RefreshAccountViewsAsync(); + if (ViewModel == null) + { + return; + } + + await ViewModel.RefreshAccountViewsAsync(); await Device.InvokeOnMainThreadAsync(async () => { // start listView in default (off-screen) position await _accountListContainer.TranslateTo(0, _accountListContainer.Height * -1, 0); + // re-measure in case accounts have been removed without changing screens + if (ViewModel.AccountViews != null) + { + _accountListView.HeightRequest = AccountListRowHeight * ViewModel.AccountViews.Count; + } + // set overlay opacity to zero before making visible and start fade-in Opacity = 0; IsVisible = true; @@ -113,16 +150,10 @@ namespace Bit.App.Controls } } - async void AccountRow_Selected(object sender, SelectedItemChangedEventArgs e) + private async Task SelectAccountAsync(AccountViewCellViewModel item) { try { - if (!(e.SelectedItem is AccountViewCellViewModel item)) - { - return; - } - - ((ListView)sender).SelectedItem = null; await Task.Delay(100); await HideAsync(); @@ -133,5 +164,25 @@ namespace Bit.App.Controls _logger.Value.Exception(ex); } } + + private async Task LongPressAccountAsync(AccountViewCellViewModel item) + { + if (!item.IsAccount) + { + return; + } + try + { + await Task.Delay(100); + await HideAsync(); + + ViewModel?.LongPressAccountCommand?.Execute( + new Tuple(MainPage, item)); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } + } } } diff --git a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs index ed6f521ca..496496035 100644 --- a/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs +++ b/src/App/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayViewModel.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using System.Windows.Input; +using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Models.View; using Bit.Core.Utilities; @@ -24,6 +26,10 @@ namespace Bit.App.Controls SelectAccountCommand = new AsyncCommand(SelectAccountAsync, onException: ex => logger.Exception(ex), allowsMultipleExecutions: false); + + LongPressAccountCommand = new AsyncCommand>(LongPressAccountAsync, + onException: ex => logger.Exception(ex), + allowsMultipleExecutions: false); } // this needs to be a new list every time for the binding to get updated, @@ -37,6 +43,8 @@ namespace Bit.App.Controls public ICommand SelectAccountCommand { get; } + public ICommand LongPressAccountCommand { get; } + private async Task SelectAccountAsync(AccountViewCellViewModel item) { if (item.AccountView.IsAccount) @@ -57,6 +65,15 @@ namespace Bit.App.Controls } } + private async Task LongPressAccountAsync(Tuple item) + { + var (page, account) = item; + if (account.AccountView.IsAccount) + { + await AppHelpers.AccountListOptions(page, account); + } + } + public async Task RefreshAccountViewsAsync() { await _stateService.RefreshAccountViewsAsync(AllowAddAccountRow); diff --git a/src/App/Controls/AccountViewCell/AccountViewCell.xaml b/src/App/Controls/AccountViewCell/AccountViewCell.xaml index 77773919a..c3eb578b7 100644 --- a/src/App/Controls/AccountViewCell/AccountViewCell.xaml +++ b/src/App/Controls/AccountViewCell/AccountViewCell.xaml @@ -5,9 +5,15 @@ x:Class="Bit.App.Controls.AccountViewCell" xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:u="clr-namespace:Bit.App.Utilities" + x:Name="_accountView" x:DataType="controls:AccountViewCellViewModel"> + ColumnSpacing="0" + xct:TouchEffect.NativeAnimation="True" + xct:TouchEffect.Command="{Binding SelectAccountCommand, Source={x:Reference _accountView}}" + xct:TouchEffect.CommandParameter="{Binding .}" + xct:TouchEffect.LongPressCommand="{Binding LongPressAccountCommand, Source={x:Reference _accountView}}" + xct:TouchEffect.LongPressCommandParameter="{Binding .}"> @@ -71,7 +77,7 @@