diff --git a/src/App/Pages/Accounts/LoginPage.xaml b/src/App/Pages/Accounts/LoginPage.xaml
index 05c852f74a0..b7395be1ca8 100644
--- a/src/App/Pages/Accounts/LoginPage.xaml
+++ b/src/App/Pages/Accounts/LoginPage.xaml
@@ -37,7 +37,12 @@
x:Key="getPasswordHint"
x:Name="_getPasswordHint"
Clicked="Hint_Clicked"
- Order="Secondary"/>
+ Order="Secondary" />
+
diff --git a/src/App/Pages/Accounts/LoginPage.xaml.cs b/src/App/Pages/Accounts/LoginPage.xaml.cs
index 2439b090f99..bde7ba0f787 100644
--- a/src/App/Pages/Accounts/LoginPage.xaml.cs
+++ b/src/App/Pages/Accounts/LoginPage.xaml.cs
@@ -53,6 +53,11 @@ public LoginPage(string email = null, AppOptions appOptions = null)
ToolbarItems.Add(_getPasswordHint);
}
+ if (Device.RuntimePlatform == Device.Android && !_email.IsEnabled)
+ {
+ ToolbarItems.Add(_removeAccount);
+ }
+
if (_appOptions?.IosExtension ?? false)
{
_vm.ShowCancelButton = true;
@@ -105,7 +110,7 @@ private async void LogIn_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
- await _vm.LogInAsync();
+ await _vm.LogInAsync(true, _email.IsEnabled);
}
}
@@ -117,6 +122,15 @@ private void Hint_Clicked(object sender, EventArgs e)
}
}
+ private async void RemoveAccount_Clicked(object sender, EventArgs e)
+ {
+ await _accountListOverlay.HideAsync();
+ if (DoOnce())
+ {
+ await _vm.RemoveAccountAsync();
+ }
+ }
+
private void Cancel_Clicked(object sender, EventArgs e)
{
if (DoOnce())
@@ -134,12 +148,16 @@ private async void More_Clicked(object sender, System.EventArgs e)
}
var selection = await DisplayActionSheet(AppResources.Options,
- AppResources.Cancel, null, AppResources.GetPasswordHint);
+ AppResources.Cancel, null, AppResources.GetPasswordHint, AppResources.RemoveAccount);
if (selection == AppResources.GetPasswordHint)
{
await Navigation.PushModalAsync(new NavigationPage(new HintPage()));
}
+ else if (selection == AppResources.RemoveAccount)
+ {
+ await _vm.RemoveAccountAsync();
+ }
}
private async Task StartTwoFactorAsync()
diff --git a/src/App/Pages/Accounts/LoginPageViewModel.cs b/src/App/Pages/Accounts/LoginPageViewModel.cs
index dcfae45f265..df2641b64e5 100644
--- a/src/App/Pages/Accounts/LoginPageViewModel.cs
+++ b/src/App/Pages/Accounts/LoginPageViewModel.cs
@@ -8,6 +8,9 @@
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
+#if !FDROID
+using Microsoft.AppCenter.Crashes;
+#endif
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -101,7 +104,7 @@ public async Task InitAsync()
}
}
- public async Task LogInAsync(bool showLoading = true)
+ public async Task LogInAsync(bool showLoading = true, bool checkForExistingAccount = false)
{
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
@@ -133,6 +136,23 @@ await _platformUtilsService.ShowDialogAsync(
ShowPassword = false;
try
{
+ if (checkForExistingAccount)
+ {
+ var userId = await _stateService.GetUserIdAsync(Email);
+ if (!string.IsNullOrWhiteSpace(userId))
+ {
+ var switchToAccount = await _platformUtilsService.ShowDialogAsync(
+ AppResources.SwitchToAlreadyAddedAccountConfirmation,
+ AppResources.AccountAlreadyAdded, AppResources.Yes, AppResources.Cancel);
+ if (switchToAccount)
+ {
+ await _stateService.SetActiveUserAsync(userId);
+ _messagingService.Send("switchedAccount");
+ }
+ return;
+ }
+ }
+
if (showLoading)
{
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
@@ -190,5 +210,24 @@ public void TogglePassword()
entry.Focus();
entry.CursorPosition = String.IsNullOrEmpty(MasterPassword) ? 0 : MasterPassword.Length;
}
+
+ public async Task RemoveAccountAsync()
+ {
+ try
+ {
+ var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.RemoveAccountConfirmation,
+ AppResources.RemoveAccount, AppResources.Yes, AppResources.Cancel);
+ if (confirmed)
+ {
+ _messagingService.Send("logout");
+ }
+ }
+ catch (Exception e)
+ {
+#if !FDROID
+ Crashes.TrackError(e);
+#endif
+ }
+ }
}
}
diff --git a/src/App/Pages/Settings/OptionsPageViewModel.cs b/src/App/Pages/Settings/OptionsPageViewModel.cs
index 4ce6d39c800..63d2e86b63e 100644
--- a/src/App/Pages/Settings/OptionsPageViewModel.cs
+++ b/src/App/Pages/Settings/OptionsPageViewModel.cs
@@ -206,6 +206,7 @@ private async Task SaveThemeAsync()
await _stateService.SetThemeAsync(theme);
ThemeManager.SetTheme(Application.Current.Resources);
_messagingService.Send("updatedTheme");
+ _stateService.ApplyThemeGloballyAsync(theme).FireAndForget();
}
}
diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs
index 40596075f51..af92ac166c1 100644
--- a/src/App/Resources/AppResources.Designer.cs
+++ b/src/App/Resources/AppResources.Designer.cs
@@ -311,6 +311,30 @@ public static string LogoutConfirmation {
}
}
+ public static string RemoveAccount {
+ get {
+ return ResourceManager.GetString("RemoveAccount", resourceCulture);
+ }
+ }
+
+ public static string RemoveAccountConfirmation {
+ get {
+ return ResourceManager.GetString("RemoveAccountConfirmation", resourceCulture);
+ }
+ }
+
+ public static string AccountAlreadyAdded {
+ get {
+ return ResourceManager.GetString("AccountAlreadyAdded", resourceCulture);
+ }
+ }
+
+ public static string SwitchToAlreadyAddedAccountConfirmation {
+ get {
+ return ResourceManager.GetString("SwitchToAlreadyAddedAccountConfirmation", resourceCulture);
+ }
+ }
+
public static string MasterPassword {
get {
return ResourceManager.GetString("MasterPassword", resourceCulture);
diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx
index 693dbb5edf5..5055b1b8040 100644
--- a/src/App/Resources/AppResources.resx
+++ b/src/App/Resources/AppResources.resx
@@ -275,6 +275,18 @@
Are you sure you want to log out?
+
+ Remove Account
+
+
+ Are you sure you want to remove this account?
+
+
+ Account Already Added
+
+
+ Would you like to switch to it now?
+
Master Password
Label for a master password.
diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs
index 98a7dd0e1b5..dd8e9559089 100644
--- a/src/Core/Abstractions/IStateService.cs
+++ b/src/Core/Abstractions/IStateService.cs
@@ -15,6 +15,7 @@ public interface IStateService
Task GetActiveUserIdAsync();
Task SetActiveUserAsync(string userId);
Task IsAuthenticatedAsync(string userId = null);
+ Task GetUserIdAsync(string email);
Task RefreshAccountViewsAsync(bool allowAddAccountRow);
Task AddAccountAsync(Account account);
Task LogoutAccountAsync(string userId, bool userInitiated);
@@ -105,6 +106,7 @@ public interface IStateService
Task SetRememberedOrgIdentifierAsync(string value);
Task GetThemeAsync(string userId = null);
Task SetThemeAsync(string value, string userId = null);
+ Task ApplyThemeGloballyAsync(string value);
Task GetAddSitePromptShownAsync(string userId = null);
Task SetAddSitePromptShownAsync(bool? value, string userId = null);
Task GetPushInitialPromptShownAsync();
diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs
index 235bb6b50bb..ee3fd05e7e9 100644
--- a/src/Core/Services/StateService.cs
+++ b/src/Core/Services/StateService.cs
@@ -67,6 +67,28 @@ public async Task IsAuthenticatedAsync(string userId = null)
return await GetAccessTokenAsync(userId) != null;
}
+ public async Task GetUserIdAsync(string email)
+ {
+ if (string.IsNullOrWhiteSpace(email))
+ {
+ throw new ArgumentNullException(nameof(email));
+ }
+
+ await CheckStateAsync();
+ if (_state?.Accounts != null)
+ {
+ foreach (var account in _state.Accounts)
+ {
+ var accountEmail = account.Value?.Profile?.Email;
+ if (accountEmail == email)
+ {
+ return account.Value.Profile.UserId;
+ }
+ }
+ }
+ return null;
+ }
+
public async Task RefreshAccountViewsAsync(bool allowAddAccountRow)
{
await CheckStateAsync();
@@ -124,9 +146,9 @@ public async Task AddAccountAsync(Account account)
public async Task LogoutAccountAsync(string userId, bool userInitiated)
{
- if (userId == null)
+ if (string.IsNullOrWhiteSpace(userId))
{
- throw new Exception("userId cannot be null");
+ throw new ArgumentNullException(nameof(userId));
}
await CheckStateAsync();
@@ -843,6 +865,26 @@ public async Task SetThemeAsync(string value, string userId = null)
await SetValueAsync(key, value, reconciledOptions);
}
+ public async Task ApplyThemeGloballyAsync(string value)
+ {
+ // TODO remove this method (ApplyThemeGlobally) to restore per-account theme support
+ await CheckStateAsync();
+ if (_state?.Accounts == null)
+ {
+ return;
+ }
+ var activeUserId = await GetActiveUserIdAsync();
+ foreach (var account in _state.Accounts)
+ {
+ var uid = account.Value?.Profile?.UserId;
+ // skip active user (theme already set)
+ if (uid != null && uid != activeUserId)
+ {
+ await SetThemeAsync(value, uid);
+ }
+ }
+ }
+
public async Task GetAddSitePromptShownAsync(string userId = null)
{
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
@@ -1225,9 +1267,9 @@ private async Task SaveAccountAsync(Account account, StorageOptions options = nu
private async Task RemoveAccountAsync(string userId, bool userInitiated)
{
- if (userId == null)
+ if (string.IsNullOrWhiteSpace(userId))
{
- throw new Exception("userId cannot be null");
+ throw new ArgumentNullException(nameof(userId));
}
var email = await GetEmailAsync(userId);
@@ -1470,9 +1512,9 @@ private async Task CheckStateAsync()
private async Task ValidateUserAsync(string userId)
{
- if (string.IsNullOrEmpty(userId))
+ if (string.IsNullOrWhiteSpace(userId))
{
- throw new Exception("userId cannot be null or empty");
+ throw new ArgumentNullException(nameof(userId));
}
await CheckStateAsync();
var accounts = _state?.Accounts;