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
This commit is contained in:
Matt Portune 2022-03-07 12:28:06 -05:00 committed by GitHub
parent efd83d07dd
commit 79a76c4638
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 342 additions and 69 deletions

View File

@ -84,7 +84,7 @@
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.2.5.2" /> <PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.2.5.2" />
<PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.8" /> <PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.8" />
<PackageReference Include="Xamarin.Essentials"> <PackageReference Include="Xamarin.Essentials">
<Version>1.7.0</Version> <Version>1.7.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Xamarin.Firebase.Messaging"> <PackageReference Include="Xamarin.Firebase.Messaging">
<Version>122.0.0</Version> <Version>122.0.0</Version>

View File

@ -16,10 +16,10 @@
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="4.4.0" /> <PackageReference Include="Microsoft.AppCenter.Crashes" Version="4.4.0" />
<PackageReference Include="Plugin.Fingerprint" Version="2.1.4" /> <PackageReference Include="Plugin.Fingerprint" Version="2.1.4" />
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" /> <PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="1.3.2" /> <PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.0" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" /> <PackageReference Include="Xamarin.Essentials" Version="1.7.1" />
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" /> <PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2291" /> <PackageReference Include="Xamarin.Forms" Version="5.0.0.2337" />
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" /> <PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" /> <PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
</ItemGroup> </ItemGroup>

View File

@ -73,7 +73,11 @@ namespace Bit.App
} }
else if (message.Command == "locked") else if (message.Command == "locked")
{ {
await LockedAsync(!(message.Data as bool?).GetValueOrDefault()); var extras = message.Data as Tuple<string, bool>;
var userId = extras?.Item1;
var userInitiated = extras?.Item2;
Device.BeginInvokeOnMainThread(async () =>
await LockedAsync(userId, userInitiated.GetValueOrDefault()));
} }
else if (message.Command == "lockVault") else if (message.Command == "lockVault")
{ {
@ -283,10 +287,9 @@ namespace Bit.App
private async Task SwitchedAccountAsync() private async Task SwitchedAccountAsync()
{ {
await AppHelpers.OnAccountSwitchAsync(); await AppHelpers.OnAccountSwitchAsync();
var shouldTimeout = await _vaultTimeoutService.ShouldTimeoutAsync();
Device.BeginInvokeOnMainThread(async () => Device.BeginInvokeOnMainThread(async () =>
{ {
if (shouldTimeout) if (await _vaultTimeoutService.ShouldTimeoutAsync())
{ {
await _vaultTimeoutService.ExecuteTimeoutActionAsync(); await _vaultTimeoutService.ExecuteTimeoutActionAsync();
} }
@ -304,24 +307,20 @@ namespace Bit.App
var authed = await _stateService.IsAuthenticatedAsync(); var authed = await _stateService.IsAuthenticatedAsync();
if (authed) if (authed)
{ {
var isLocked = await _vaultTimeoutService.IsLockedAsync(); if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() ||
var shouldTimeout = await _vaultTimeoutService.ShouldTimeoutAsync(); await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
if (isLocked || shouldTimeout)
{ {
var vaultTimeoutAction = await _stateService.GetVaultTimeoutActionAsync(); // TODO implement orgIdentifier flow to SSO Login page, same as email flow below
if (vaultTimeoutAction == VaultTimeoutAction.Logout) // var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
{
// TODO implement orgIdentifier flow to SSO Login page, same as email flow below var email = await _stateService.GetEmailAsync();
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null;
Current.MainPage = new NavigationPage(new LoginPage(email, Options));
var email = await _stateService.GetEmailAsync(); }
Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; else if (await _vaultTimeoutService.IsLockedAsync() ||
Current.MainPage = new NavigationPage(new LoginPage(email, Options)); await _vaultTimeoutService.ShouldLockAsync())
} {
else Current.MainPage = new NavigationPage(new LockPage(Options));
{
Current.MainPage = new NavigationPage(new LockPage(Options));
}
} }
else if (Options.FromAutofillFramework && Options.SaveType.HasValue) else if (Options.FromAutofillFramework && Options.SaveType.HasValue)
{ {
@ -343,7 +342,8 @@ namespace Bit.App
else else
{ {
Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; 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 // TODO implement orgIdentifier flow to SSO Login page, same as email flow below
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); // 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) if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS)
{ {
var vaultTimeout = await _stateService.GetVaultTimeoutAsync(); var vaultTimeout = await _stateService.GetVaultTimeoutAsync();

View File

@ -27,15 +27,17 @@
<ListView <ListView
x:Name="_accountListView" x:Name="_accountListView"
ItemsSource="{Binding BindingContext.AccountViews, Source={x:Reference _mainOverlay}}" ItemsSource="{Binding BindingContext.AccountViews, Source={x:Reference _mainOverlay}}"
ItemSelected="AccountRow_Selected"
BackgroundColor="{DynamicResource BackgroundColor}" BackgroundColor="{DynamicResource BackgroundColor}"
VerticalOptions="Start" VerticalOptions="Start"
RowHeight="{OnPlatform 70, iOS=70, Android=74}" RowHeight="{Binding AccountListRowHeight, Source={x:Reference _mainOverlay}}"
effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never"> effects:ScrollViewContentInsetAdjustmentBehaviorEffect.ContentInsetAdjustmentBehavior="Never">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate x:DataType="view:AccountView"> <DataTemplate x:DataType="view:AccountView">
<controls:AccountViewCell <controls:AccountViewCell
Account="{Binding .}" /> Account="{Binding .}"
SelectAccountCommand="{Binding SelectAccountCommand, Source={x:Reference _mainOverlay}}"
LongPressAccountCommand="{Binding LongPressAccountCommand, Source={x:Reference _mainOverlay}}"
/>
</DataTemplate> </DataTemplate>
</ListView.ItemTemplate> </ListView.ItemTemplate>
<ListView.Effects> <ListView.Effects>

View File

@ -10,12 +10,24 @@ namespace Bit.App.Controls
{ {
public partial class AccountSwitchingOverlayView : ContentView 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( public static readonly BindableProperty MainFabProperty = BindableProperty.Create(
nameof(MainFab), nameof(MainFab),
typeof(View), typeof(View),
typeof(AccountSwitchingOverlayView), typeof(AccountSwitchingOverlayView),
defaultBindingMode: BindingMode.OneWay); defaultBindingMode: BindingMode.OneWay);
public ContentPage MainPage
{
get => (ContentPage)GetValue(MainPageProperty);
set => SetValue(MainPageProperty, value);
}
public View MainFab public View MainFab
{ {
get => (View)GetValue(MainFabProperty); get => (View)GetValue(MainFabProperty);
@ -31,12 +43,26 @@ namespace Bit.App.Controls
ToggleVisibililtyCommand = new AsyncCommand(ToggleVisibilityAsync, ToggleVisibililtyCommand = new AsyncCommand(ToggleVisibilityAsync,
onException: ex => _logger.Value.Exception(ex), onException: ex => _logger.Value.Exception(ex),
allowsMultipleExecutions: false); allowsMultipleExecutions: false);
SelectAccountCommand = new AsyncCommand<AccountViewCellViewModel>(SelectAccountAsync,
onException: ex => _logger.Value.Exception(ex),
allowsMultipleExecutions: false);
LongPressAccountCommand = new AsyncCommand<AccountViewCellViewModel>(LongPressAccountAsync,
onException: ex => _logger.Value.Exception(ex),
allowsMultipleExecutions: false);
} }
public AccountSwitchingOverlayViewModel ViewModel => BindingContext as AccountSwitchingOverlayViewModel; public AccountSwitchingOverlayViewModel ViewModel => BindingContext as AccountSwitchingOverlayViewModel;
public ICommand ToggleVisibililtyCommand { get; } 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() public async Task ToggleVisibilityAsync()
{ {
if (IsVisible) if (IsVisible)
@ -51,13 +77,24 @@ namespace Bit.App.Controls
public async Task ShowAsync() public async Task ShowAsync()
{ {
await ViewModel?.RefreshAccountViewsAsync(); if (ViewModel == null)
{
return;
}
await ViewModel.RefreshAccountViewsAsync();
await Device.InvokeOnMainThreadAsync(async () => await Device.InvokeOnMainThreadAsync(async () =>
{ {
// start listView in default (off-screen) position // start listView in default (off-screen) position
await _accountListContainer.TranslateTo(0, _accountListContainer.Height * -1, 0); 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 // set overlay opacity to zero before making visible and start fade-in
Opacity = 0; Opacity = 0;
IsVisible = true; 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 try
{ {
if (!(e.SelectedItem is AccountViewCellViewModel item))
{
return;
}
((ListView)sender).SelectedItem = null;
await Task.Delay(100); await Task.Delay(100);
await HideAsync(); await HideAsync();
@ -133,5 +164,25 @@ namespace Bit.App.Controls
_logger.Value.Exception(ex); _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<ContentPage, AccountViewCellViewModel>(MainPage, item));
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
}
}
} }
} }

View File

@ -1,6 +1,8 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Bit.App.Utilities;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Models.View; using Bit.Core.Models.View;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -24,6 +26,10 @@ namespace Bit.App.Controls
SelectAccountCommand = new AsyncCommand<AccountViewCellViewModel>(SelectAccountAsync, SelectAccountCommand = new AsyncCommand<AccountViewCellViewModel>(SelectAccountAsync,
onException: ex => logger.Exception(ex), onException: ex => logger.Exception(ex),
allowsMultipleExecutions: false); allowsMultipleExecutions: false);
LongPressAccountCommand = new AsyncCommand<Tuple<ContentPage, AccountViewCellViewModel>>(LongPressAccountAsync,
onException: ex => logger.Exception(ex),
allowsMultipleExecutions: false);
} }
// this needs to be a new list every time for the binding to get updated, // 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 SelectAccountCommand { get; }
public ICommand LongPressAccountCommand { get; }
private async Task SelectAccountAsync(AccountViewCellViewModel item) private async Task SelectAccountAsync(AccountViewCellViewModel item)
{ {
if (item.AccountView.IsAccount) if (item.AccountView.IsAccount)
@ -57,6 +65,15 @@ namespace Bit.App.Controls
} }
} }
private async Task LongPressAccountAsync(Tuple<ContentPage, AccountViewCellViewModel> item)
{
var (page, account) = item;
if (account.AccountView.IsAccount)
{
await AppHelpers.AccountListOptions(page, account);
}
}
public async Task RefreshAccountViewsAsync() public async Task RefreshAccountViewsAsync()
{ {
await _stateService.RefreshAccountViewsAsync(AllowAddAccountRow); await _stateService.RefreshAccountViewsAsync(AllowAddAccountRow);

View File

@ -5,9 +5,15 @@
x:Class="Bit.App.Controls.AccountViewCell" x:Class="Bit.App.Controls.AccountViewCell"
xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:u="clr-namespace:Bit.App.Utilities"
x:Name="_accountView"
x:DataType="controls:AccountViewCellViewModel"> x:DataType="controls:AccountViewCellViewModel">
<Grid RowSpacing="0" <Grid RowSpacing="0"
ColumnSpacing="0"> 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 .}">
<Grid.Resources> <Grid.Resources>
<u:InverseBoolConverter x:Key="inverseBool" /> <u:InverseBoolConverter x:Key="inverseBool" />
@ -71,7 +77,7 @@
<Label <Label
Grid.Row="2" Grid.Row="2"
Text="{u:I18n AccountUnlocked}" Text="{u:I18n AccountUnlocked}"
IsVisible="{Binding IsUnlocked}" IsVisible="{Binding IsUnlockedAndNotActive}"
StyleClass="list-sub" StyleClass="list-sub"
FontAttributes="Italic" FontAttributes="Italic"
TextTransform="Lowercase" TextTransform="Lowercase"
@ -79,7 +85,7 @@
<Label <Label
Grid.Row="2" Grid.Row="2"
Text="{u:I18n AccountLocked}" Text="{u:I18n AccountLocked}"
IsVisible="{Binding IsLocked}" IsVisible="{Binding IsLockedAndNotActive}"
StyleClass="list-sub" StyleClass="list-sub"
FontAttributes="Italic" FontAttributes="Italic"
TextTransform="Lowercase" TextTransform="Lowercase"
@ -87,7 +93,7 @@
<Label <Label
Grid.Row="2" Grid.Row="2"
Text="{u:I18n AccountLoggedOut}" Text="{u:I18n AccountLoggedOut}"
IsVisible="{Binding IsLoggedOut}" IsVisible="{Binding IsLoggedOutAndNotActive}"
StyleClass="list-sub" StyleClass="list-sub"
FontAttributes="Italic" FontAttributes="Italic"
TextTransform="Lowercase" TextTransform="Lowercase"

View File

@ -1,4 +1,5 @@
using Bit.Core.Models.View; using Bit.Core.Models.View;
using System.Windows.Input;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.App.Controls namespace Bit.App.Controls
@ -6,7 +7,13 @@ namespace Bit.App.Controls
public partial class AccountViewCell : ViewCell public partial class AccountViewCell : ViewCell
{ {
public static readonly BindableProperty AccountProperty = BindableProperty.Create( public static readonly BindableProperty AccountProperty = BindableProperty.Create(
nameof(Account), typeof(AccountView), typeof(AccountViewCell), default(AccountView), BindingMode.OneWay); nameof(Account), typeof(AccountView), typeof(AccountViewCell));
public static readonly BindableProperty SelectAccountCommandProperty = BindableProperty.Create(
nameof(SelectAccountCommand), typeof(ICommand), typeof(AccountViewCell));
public static readonly BindableProperty LongPressAccountCommandProperty = BindableProperty.Create(
nameof(LongPressAccountCommand), typeof(ICommand), typeof(AccountViewCell));
public AccountViewCell() public AccountViewCell()
{ {
@ -19,6 +26,18 @@ namespace Bit.App.Controls
set => SetValue(AccountProperty, value); set => SetValue(AccountProperty, value);
} }
public ICommand SelectAccountCommand
{
get => GetValue(SelectAccountCommandProperty) as ICommand;
set => SetValue(SelectAccountCommandProperty, value);
}
public ICommand LongPressAccountCommand
{
get => GetValue(LongPressAccountCommandProperty) as ICommand;
set => SetValue(LongPressAccountCommandProperty, value);
}
protected override void OnPropertyChanged(string propertyName = null) protected override void OnPropertyChanged(string propertyName = null)
{ {
base.OnPropertyChanged(propertyName); base.OnPropertyChanged(propertyName);

View File

@ -48,16 +48,31 @@ namespace Bit.App.Controls
get => AccountView.AuthStatus == AuthenticationStatus.Unlocked; get => AccountView.AuthStatus == AuthenticationStatus.Unlocked;
} }
public bool IsUnlockedAndNotActive
{
get => IsUnlocked && !IsActive;
}
public bool IsLocked public bool IsLocked
{ {
get => AccountView.AuthStatus == AuthenticationStatus.Locked; get => AccountView.AuthStatus == AuthenticationStatus.Locked;
} }
public bool IsLockedAndNotActive
{
get => IsLocked && !IsActive;
}
public bool IsLoggedOut public bool IsLoggedOut
{ {
get => AccountView.AuthStatus == AuthenticationStatus.LoggedOut; get => AccountView.AuthStatus == AuthenticationStatus.LoggedOut;
} }
public bool IsLoggedOutAndNotActive
{
get => IsLoggedOut && !IsActive;
}
public string AuthStatusIconActive public string AuthStatusIconActive
{ {
get => BitwardenIcons.CheckCircle; get => BitwardenIcons.CheckCircle;

View File

@ -7,6 +7,7 @@
xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:HomeViewModel" x:DataType="pages:HomeViewModel"
x:Name="_page"
Title="{Binding PageTitle}"> Title="{Binding PageTitle}">
<ContentPage.BindingContext> <ContentPage.BindingContext>
<pages:HomeViewModel /> <pages:HomeViewModel />
@ -73,6 +74,7 @@
x:Name="_accountListOverlay" x:Name="_accountListOverlay"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1" AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutFlags="All"
MainPage="{Binding Source={x:Reference _page}}"
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/> BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
</AbsoluteLayout> </AbsoluteLayout>

View File

@ -7,6 +7,7 @@
xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:LockPageViewModel" x:DataType="pages:LockPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}"> Title="{Binding PageTitle}">
<ContentPage.BindingContext> <ContentPage.BindingContext>
@ -165,6 +166,7 @@
x:Name="_accountListOverlay" x:Name="_accountListOverlay"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1" AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutFlags="All"
MainPage="{Binding Source={x:Reference _page}}"
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/> BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
</AbsoluteLayout> </AbsoluteLayout>

View File

@ -7,6 +7,7 @@
xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:LoginPageViewModel" x:DataType="pages:LoginPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}"> Title="{Binding PageTitle}">
<ContentPage.BindingContext> <ContentPage.BindingContext>
@ -130,6 +131,7 @@
x:Name="_accountListOverlay" x:Name="_accountListOverlay"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1" AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutFlags="All"
MainPage="{Binding Source={x:Reference _page}}"
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/> BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
</AbsoluteLayout> </AbsoluteLayout>

View File

@ -165,6 +165,7 @@
x:Name="_accountListOverlay" x:Name="_accountListOverlay"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1" AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutFlags="All"
MainPage="{Binding Source={x:Reference _page}}"
MainFab="{Binding Source={x:Reference _fab}, Path=.}" MainFab="{Binding Source={x:Reference _fab}, Path=.}"
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/> BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
</AbsoluteLayout> </AbsoluteLayout>

View File

@ -3773,6 +3773,24 @@ namespace Bit.App.Resources {
} }
} }
public static string AccountLockedSuccessfully {
get {
return ResourceManager.GetString("AccountLockedSuccessfully", resourceCulture);
}
}
public static string AccountLoggedOutSuccessfully {
get {
return ResourceManager.GetString("AccountLoggedOutSuccessfully", resourceCulture);
}
}
public static string AccountRemovedSuccessfully {
get {
return ResourceManager.GetString("AccountRemovedSuccessfully", resourceCulture);
}
}
public static string DeleteAccount { public static string DeleteAccount {
get { get {
return ResourceManager.GetString("DeleteAccount", resourceCulture); return ResourceManager.GetString("DeleteAccount", resourceCulture);
@ -3809,6 +3827,12 @@ namespace Bit.App.Resources {
} }
} }
public static string RequestOTP {
get {
return ResourceManager.GetString("RequestOTP", resourceCulture);
}
}
public static string SendCode { public static string SendCode {
get { get {
return ResourceManager.GetString("SendCode", resourceCulture); return ResourceManager.GetString("SendCode", resourceCulture);

View File

@ -2120,6 +2120,15 @@
<data name="AccountSwitchedAutomatically" xml:space="preserve"> <data name="AccountSwitchedAutomatically" xml:space="preserve">
<value>Switched to next available account</value> <value>Switched to next available account</value>
</data> </data>
<data name="AccountLockedSuccessfully" xml:space="preserve">
<value>Account Locked</value>
</data>
<data name="AccountLoggedOutSuccessfully" xml:space="preserve">
<value>Account logged out successfully</value>
</data>
<data name="AccountRemovedSuccessfully" xml:space="preserve">
<value>Account removed successfully</value>
</data>
<data name="DeleteAccount" xml:space="preserve"> <data name="DeleteAccount" xml:space="preserve">
<value>Delete Account</value> <value>Delete Account</value>
</data> </data>

View File

@ -11,6 +11,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Controls;
using Bit.App.Models; using Bit.App.Models;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -196,6 +197,55 @@ namespace Bit.App.Utilities
return selection; return selection;
} }
public static async Task<string> AccountListOptions(ContentPage page, AccountViewCellViewModel accountViewCell)
{
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var userId = accountViewCell.AccountView.UserId;
List<string> options;
if (await vaultTimeoutService.IsLoggedOutByTimeoutAsync(userId) ||
await vaultTimeoutService.ShouldLogOutByTimeoutAsync(userId))
{
options = new List<string> { AppResources.RemoveAccount };
}
else if (await vaultTimeoutService.IsLockedAsync(userId) ||
await vaultTimeoutService.ShouldLockAsync(userId))
{
options = new List<string> { AppResources.LogOut };
}
else
{
options = new List<string> { AppResources.Lock, AppResources.LogOut };
}
var accountSummary = accountViewCell.AccountView.Email;
if (!string.IsNullOrWhiteSpace(accountViewCell.AccountView.Hostname))
{
accountSummary += "\n" + accountViewCell.AccountView.Hostname;
}
var selection = await page.DisplayActionSheet(accountSummary, AppResources.Cancel, null, options.ToArray());
if (selection == AppResources.Lock)
{
await vaultTimeoutService.LockAsync(true, true, userId);
}
else if (selection == AppResources.LogOut || selection == AppResources.RemoveAccount)
{
var title = selection == AppResources.LogOut ? AppResources.LogOut : AppResources.RemoveAccount;
var text = (selection == AppResources.LogOut ? AppResources.LogoutConfirmation
: AppResources.RemoveAccountConfirmation) + "\n\n" + accountSummary;
var confirmed =
await platformUtilsService.ShowDialogAsync(text, title, AppResources.Yes, AppResources.Cancel);
if (confirmed)
{
await LogOutAsync(userId, true);
}
}
return selection;
}
public static async Task CopySendUrlAsync(SendView send) public static async Task CopySendUrlAsync(SendView send)
{ {
if (await IsSendDisabledByPolicyAsync()) if (await IsSendDisabledByPolicyAsync())
@ -459,6 +509,8 @@ namespace Bit.App.Utilities
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService"); var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
var searchService = ServiceContainer.Resolve<ISearchService>("searchService"); var searchService = ServiceContainer.Resolve<ISearchService>("searchService");
var isActiveAccount = await stateService.IsActiveAccount(userId);
if (userId == null) if (userId == null)
{ {
userId = await stateService.GetActiveUserIdAsync(); userId = await stateService.GetActiveUserIdAsync();
@ -479,14 +531,33 @@ namespace Bit.App.Utilities
searchService.ClearIndex(); searchService.ClearIndex();
// check if we switched accounts automatically if (!userInitiated)
if (userInitiated && await stateService.GetActiveUserIdAsync() != null) {
return;
}
// check if we switched active accounts automatically
if (isActiveAccount && await stateService.GetActiveUserIdAsync() != null)
{ {
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService"); var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
messagingService.Send("switchedAccount"); messagingService.Send("switchedAccount");
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"); var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
platformUtilsService.ShowToast("info", null, AppResources.AccountSwitchedAutomatically); platformUtilsService.ShowToast("info", null, AppResources.AccountSwitchedAutomatically);
return;
}
// check if we logged out a non-active account
if (!isActiveAccount)
{
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
if (await vaultTimeoutService.IsLoggedOutByTimeoutAsync(userId) ||
await vaultTimeoutService.ShouldLogOutByTimeoutAsync())
{
platformUtilsService.ShowToast("info", null, AppResources.AccountRemovedSuccessfully);
return;
}
platformUtilsService.ShowToast("info", null, AppResources.AccountLoggedOutSuccessfully);
} }
} }

View File

@ -12,6 +12,7 @@ namespace Bit.Core.Abstractions
{ {
List<AccountView> AccountViews { get; } List<AccountView> AccountViews { get; }
Task<string> GetActiveUserIdAsync(); Task<string> GetActiveUserIdAsync();
Task<bool> IsActiveAccount(string userId = null);
Task SetActiveUserAsync(string userId); Task SetActiveUserAsync(string userId);
Task<bool> IsAuthenticatedAsync(string userId = null); Task<bool> IsAuthenticatedAsync(string userId = null);
Task<string> GetUserIdAsync(string email); Task<string> GetUserIdAsync(string email);

View File

@ -13,7 +13,9 @@ namespace Bit.Core.Abstractions
Task ExecuteTimeoutActionAsync(string userId = null); Task ExecuteTimeoutActionAsync(string userId = null);
Task ClearAsync(string userId = null); Task ClearAsync(string userId = null);
Task<bool> IsLockedAsync(string userId = null); Task<bool> IsLockedAsync(string userId = null);
Task<bool> ShouldLockAsync(string userId = null);
Task<bool> IsLoggedOutByTimeoutAsync(string userId = null); Task<bool> IsLoggedOutByTimeoutAsync(string userId = null);
Task<bool> ShouldLogOutByTimeoutAsync(string userId = null);
Task<Tuple<bool, bool>> IsPinLockSetAsync(string userId = null); Task<Tuple<bool, bool>> IsPinLockSetAsync(string userId = null);
Task<bool> IsBiometricLockSetAsync(string userId = null); Task<bool> IsBiometricLockSetAsync(string userId = null);
Task LockAsync(bool allowSoftLock = false, bool userInitiated = false, string userId = null); Task LockAsync(bool allowSoftLock = false, bool userInitiated = false, string userId = null);

View File

@ -68,6 +68,7 @@ namespace Bit.Core.Services
_apiService.SetUrls(envUrls); _apiService.SetUrls(envUrls);
return; return;
} }
BaseUrl = urls.Base;
WebVaultUrl = urls.WebVault; WebVaultUrl = urls.WebVault;
ApiUrl = envUrls.Api = urls.Api; ApiUrl = envUrls.Api = urls.Api;
IdentityUrl = envUrls.Identity = urls.Identity; IdentityUrl = envUrls.Identity = urls.Identity;

View File

@ -42,6 +42,16 @@ namespace Bit.Core.Services
return activeUserId; return activeUserId;
} }
public async Task<bool> IsActiveAccount(string userId = null)
{
if (userId == null)
{
return true;
}
await CheckStateAsync();
return userId == await GetActiveUserIdAsync();
}
public async Task SetActiveUserAsync(string userId) public async Task SetActiveUserAsync(string userId)
{ {
if (userId != null) if (userId != null)
@ -110,18 +120,16 @@ namespace Bit.Core.Services
{ {
var isActiveAccount = account.Profile.UserId == _state.ActiveUserId; var isActiveAccount = account.Profile.UserId == _state.ActiveUserId;
var accountView = new AccountView(account, isActiveAccount); var accountView = new AccountView(account, isActiveAccount);
if (isActiveAccount)
if (await vaultTimeoutService.IsLoggedOutByTimeoutAsync(accountView.UserId) ||
await vaultTimeoutService.ShouldLogOutByTimeoutAsync(accountView.UserId))
{ {
AccountViews.Add(accountView); accountView.AuthStatus = AuthenticationStatus.LoggedOut;
continue;
} }
var isLocked = await vaultTimeoutService.IsLockedAsync(accountView.UserId); else if (await vaultTimeoutService.IsLockedAsync(accountView.UserId) ||
var shouldTimeout = await vaultTimeoutService.ShouldTimeoutAsync(accountView.UserId); await vaultTimeoutService.ShouldLockAsync(accountView.UserId))
if (isLocked || shouldTimeout)
{ {
var action = account.Settings.VaultTimeoutAction; accountView.AuthStatus = AuthenticationStatus.Locked;
accountView.AuthStatus = action == VaultTimeoutAction.Logout ? AuthenticationStatus.LoggedOut
: AuthenticationStatus.Locked;
} }
else else
{ {

View File

@ -19,7 +19,7 @@ namespace Bit.Core.Services
private readonly ITokenService _tokenService; private readonly ITokenService _tokenService;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IKeyConnectorService _keyConnectorService; private readonly IKeyConnectorService _keyConnectorService;
private readonly Action<bool> _lockedCallback; private readonly Func<Tuple<string, bool>, Task> _lockedCallback;
private readonly Func<Tuple<string, bool, bool>, Task> _loggedOutCallback; private readonly Func<Tuple<string, bool, bool>, Task> _loggedOutCallback;
public VaultTimeoutService( public VaultTimeoutService(
@ -34,7 +34,7 @@ namespace Bit.Core.Services
ITokenService tokenService, ITokenService tokenService,
IPolicyService policyService, IPolicyService policyService,
IKeyConnectorService keyConnectorService, IKeyConnectorService keyConnectorService,
Action<bool> lockedCallback, Func<Tuple<string, bool>, Task> lockedCallback,
Func<Tuple<string, bool, bool>, Task> loggedOutCallback) Func<Tuple<string, bool, bool>, Task> loggedOutCallback)
{ {
_cryptoService = cryptoService; _cryptoService = cryptoService;
@ -67,7 +67,17 @@ namespace Bit.Core.Services
} }
return !hasKey; return !hasKey;
} }
public async Task<bool> ShouldLockAsync(string userId = null)
{
if (await ShouldTimeoutAsync(userId))
{
var action = await _stateService.GetVaultTimeoutActionAsync(userId);
return action == VaultTimeoutAction.Lock;
}
return false;
}
public async Task<bool> IsLoggedOutByTimeoutAsync(string userId = null) public async Task<bool> IsLoggedOutByTimeoutAsync(string userId = null)
{ {
var authed = await _stateService.IsAuthenticatedAsync(userId); var authed = await _stateService.IsAuthenticatedAsync(userId);
@ -75,6 +85,16 @@ namespace Bit.Core.Services
return !authed && !string.IsNullOrWhiteSpace(email); return !authed && !string.IsNullOrWhiteSpace(email);
} }
public async Task<bool> ShouldLogOutByTimeoutAsync(string userId = null)
{
if (await ShouldTimeoutAsync(userId))
{
var action = await _stateService.GetVaultTimeoutActionAsync(userId);
return action == VaultTimeoutAction.Logout;
}
return false;
}
public async Task CheckVaultTimeoutAsync() public async Task CheckVaultTimeoutAsync()
{ {
if (_platformUtilsService.IsViewOpen()) if (_platformUtilsService.IsViewOpen())
@ -144,6 +164,13 @@ namespace Bit.Core.Services
return; return;
} }
var isActiveAccount = await _stateService.IsActiveAccount(userId);
if (userId == null)
{
userId = await _stateService.GetActiveUserIdAsync();
}
if (await _keyConnectorService.GetUsesKeyConnector()) { if (await _keyConnectorService.GetUsesKeyConnector()) {
var (isPinProtected, isPinProtectedWithKey) = await IsPinLockSetAsync(userId); var (isPinProtected, isPinProtectedWithKey) = await IsPinLockSetAsync(userId);
var pinLock = (isPinProtected && await _stateService.GetPinProtectedKeyAsync(userId) != null) || var pinLock = (isPinProtected && await _stateService.GetPinProtectedKeyAsync(userId) != null) ||
@ -162,8 +189,7 @@ namespace Bit.Core.Services
await _stateService.SetBiometricLockedAsync(isBiometricLockSet, userId); await _stateService.SetBiometricLockedAsync(isBiometricLockSet, userId);
if (isBiometricLockSet) if (isBiometricLockSet)
{ {
_messagingService.Send("locked", userInitiated); _lockedCallback?.Invoke(new Tuple<string, bool>(userId, userInitiated));
_lockedCallback?.Invoke(userInitiated);
return; return;
} }
} }
@ -173,12 +199,14 @@ namespace Bit.Core.Services
_cryptoService.ClearKeyPairAsync(true, userId), _cryptoService.ClearKeyPairAsync(true, userId),
_cryptoService.ClearEncKeyAsync(true, userId)); _cryptoService.ClearEncKeyAsync(true, userId));
_folderService.ClearCache(); if (isActiveAccount)
await _cipherService.ClearCacheAsync(); {
_collectionService.ClearCache(); _folderService.ClearCache();
_searchService.ClearIndex(); await _cipherService.ClearCacheAsync();
_messagingService.Send("locked", userInitiated); _collectionService.ClearCache();
_lockedCallback?.Invoke(userInitiated); _searchService.ClearIndex();
}
_lockedCallback?.Invoke(new Tuple<string, bool>(userId, userInitiated));
} }
public async Task LogOutAsync(bool userInitiated = true, string userId = null) public async Task LogOutAsync(bool userInitiated = true, string userId = null)

View File

@ -52,7 +52,13 @@ namespace Bit.Core.Utilities
organizationService); organizationService);
var vaultTimeoutService = new VaultTimeoutService(cryptoService, stateService, platformUtilsService, var vaultTimeoutService = new VaultTimeoutService(cryptoService, stateService, platformUtilsService,
folderService, cipherService, collectionService, searchService, messagingService, tokenService, folderService, cipherService, collectionService, searchService, messagingService, tokenService,
policyService, keyConnectorService, null, (extras) => policyService, keyConnectorService,
(extras) =>
{
messagingService.Send("locked", extras);
return Task.FromResult(0);
},
(extras) =>
{ {
messagingService.Send("logout", extras); messagingService.Send("logout", extras);
return Task.FromResult(0); return Task.FromResult(0);

View File

@ -167,7 +167,7 @@
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2125" /> <PackageReference Include="Xamarin.Forms" Version="5.0.0.2337" />
<PackageReference Include="Microsoft.AppCenter.Crashes"> <PackageReference Include="Microsoft.AppCenter.Crashes">
<Version>4.4.0</Version> <Version>4.4.0</Version>
</PackageReference> </PackageReference>

View File

@ -183,7 +183,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Xamarin.Essentials"> <PackageReference Include="Xamarin.Essentials">
<Version>1.7.0</Version> <Version>1.7.1</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />