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;