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 @@