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:
parent
efd83d07dd
commit
79a76c4638
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in New Issue