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.Migration" Version="1.0.8" />
<PackageReference Include="Xamarin.Essentials">
<Version>1.7.0</Version>
<Version>1.7.1</Version>
</PackageReference>
<PackageReference Include="Xamarin.Firebase.Messaging">
<Version>122.0.0</Version>

View File

@ -16,10 +16,10 @@
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="4.4.0" />
<PackageReference Include="Plugin.Fingerprint" Version="2.1.4" />
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="1.3.2" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.0" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.1" />
<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.Forms" Version="2.4.1" />
</ItemGroup>

View File

@ -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<string, bool>;
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();

View File

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

View File

@ -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<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 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<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.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<AccountViewCellViewModel>(SelectAccountAsync,
onException: ex => logger.Exception(ex),
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,
@ -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<ContentPage, AccountViewCellViewModel> item)
{
var (page, account) = item;
if (account.AccountView.IsAccount)
{
await AppHelpers.AccountListOptions(page, account);
}
}
public async Task RefreshAccountViewsAsync()
{
await _stateService.RefreshAccountViewsAsync(AllowAddAccountRow);

View File

@ -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">
<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>
<u:InverseBoolConverter x:Key="inverseBool" />
@ -71,7 +77,7 @@
<Label
Grid.Row="2"
Text="{u:I18n AccountUnlocked}"
IsVisible="{Binding IsUnlocked}"
IsVisible="{Binding IsUnlockedAndNotActive}"
StyleClass="list-sub"
FontAttributes="Italic"
TextTransform="Lowercase"
@ -79,7 +85,7 @@
<Label
Grid.Row="2"
Text="{u:I18n AccountLocked}"
IsVisible="{Binding IsLocked}"
IsVisible="{Binding IsLockedAndNotActive}"
StyleClass="list-sub"
FontAttributes="Italic"
TextTransform="Lowercase"
@ -87,7 +93,7 @@
<Label
Grid.Row="2"
Text="{u:I18n AccountLoggedOut}"
IsVisible="{Binding IsLoggedOut}"
IsVisible="{Binding IsLoggedOutAndNotActive}"
StyleClass="list-sub"
FontAttributes="Italic"
TextTransform="Lowercase"

View File

@ -1,4 +1,5 @@
using Bit.Core.Models.View;
using System.Windows.Input;
using Xamarin.Forms;
namespace Bit.App.Controls
@ -6,7 +7,13 @@ namespace Bit.App.Controls
public partial class AccountViewCell : ViewCell
{
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()
{
@ -19,6 +26,18 @@ namespace Bit.App.Controls
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)
{
base.OnPropertyChanged(propertyName);

View File

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

View File

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

View File

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

View File

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

View File

@ -165,6 +165,7 @@
x:Name="_accountListOverlay"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All"
MainPage="{Binding Source={x:Reference _page}}"
MainFab="{Binding Source={x:Reference _fab}, Path=.}"
BindingContext="{Binding AccountSwitchingOverlayViewModel}"/>
</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 {
get {
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 {
get {
return ResourceManager.GetString("SendCode", resourceCulture);

View File

@ -2120,6 +2120,15 @@
<data name="AccountSwitchedAutomatically" xml:space="preserve">
<value>Switched to next available account</value>
</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">
<value>Delete Account</value>
</data>

View File

@ -11,6 +11,7 @@ using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Controls;
using Bit.App.Models;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -196,6 +197,55 @@ namespace Bit.App.Utilities
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)
{
if (await IsSendDisabledByPolicyAsync())
@ -459,6 +509,8 @@ namespace Bit.App.Utilities
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
var searchService = ServiceContainer.Resolve<ISearchService>("searchService");
var isActiveAccount = await stateService.IsActiveAccount(userId);
if (userId == null)
{
userId = await stateService.GetActiveUserIdAsync();
@ -479,14 +531,33 @@ namespace Bit.App.Utilities
searchService.ClearIndex();
// check if we switched accounts automatically
if (userInitiated && await stateService.GetActiveUserIdAsync() != null)
if (!userInitiated)
{
return;
}
// check if we switched active accounts automatically
if (isActiveAccount && await stateService.GetActiveUserIdAsync() != null)
{
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
messagingService.Send("switchedAccount");
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
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; }
Task<string> GetActiveUserIdAsync();
Task<bool> IsActiveAccount(string userId = null);
Task SetActiveUserAsync(string userId);
Task<bool> IsAuthenticatedAsync(string userId = null);
Task<string> GetUserIdAsync(string email);

View File

@ -13,7 +13,9 @@ namespace Bit.Core.Abstractions
Task ExecuteTimeoutActionAsync(string userId = null);
Task ClearAsync(string userId = null);
Task<bool> IsLockedAsync(string userId = null);
Task<bool> ShouldLockAsync(string userId = null);
Task<bool> IsLoggedOutByTimeoutAsync(string userId = null);
Task<bool> ShouldLogOutByTimeoutAsync(string userId = null);
Task<Tuple<bool, bool>> IsPinLockSetAsync(string userId = null);
Task<bool> IsBiometricLockSetAsync(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);
return;
}
BaseUrl = urls.Base;
WebVaultUrl = urls.WebVault;
ApiUrl = envUrls.Api = urls.Api;
IdentityUrl = envUrls.Identity = urls.Identity;

View File

@ -42,6 +42,16 @@ namespace Bit.Core.Services
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)
{
if (userId != null)
@ -110,18 +120,16 @@ namespace Bit.Core.Services
{
var isActiveAccount = account.Profile.UserId == _state.ActiveUserId;
var accountView = new AccountView(account, isActiveAccount);
if (isActiveAccount)
if (await vaultTimeoutService.IsLoggedOutByTimeoutAsync(accountView.UserId) ||
await vaultTimeoutService.ShouldLogOutByTimeoutAsync(accountView.UserId))
{
AccountViews.Add(accountView);
continue;
accountView.AuthStatus = AuthenticationStatus.LoggedOut;
}
var isLocked = await vaultTimeoutService.IsLockedAsync(accountView.UserId);
var shouldTimeout = await vaultTimeoutService.ShouldTimeoutAsync(accountView.UserId);
if (isLocked || shouldTimeout)
else if (await vaultTimeoutService.IsLockedAsync(accountView.UserId) ||
await vaultTimeoutService.ShouldLockAsync(accountView.UserId))
{
var action = account.Settings.VaultTimeoutAction;
accountView.AuthStatus = action == VaultTimeoutAction.Logout ? AuthenticationStatus.LoggedOut
: AuthenticationStatus.Locked;
accountView.AuthStatus = AuthenticationStatus.Locked;
}
else
{

View File

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

View File

@ -52,7 +52,13 @@ namespace Bit.Core.Utilities
organizationService);
var vaultTimeoutService = new VaultTimeoutService(cryptoService, stateService, platformUtilsService,
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);
return Task.FromResult(0);

View File

@ -167,7 +167,7 @@
<Reference Include="Microsoft.CSharp" />
</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">
<Version>4.4.0</Version>
</PackageReference>

View File

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