diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj
index 0c2c1be45..e6370b934 100644
--- a/src/Android/Android.csproj
+++ b/src/Android/Android.csproj
@@ -172,6 +172,7 @@
+
diff --git a/src/Android/Resources/drawable/ic_warning.xml b/src/Android/Resources/drawable/ic_warning.xml
new file mode 100644
index 000000000..b2df24021
--- /dev/null
+++ b/src/Android/Resources/drawable/ic_warning.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/App/Abstractions/IPasswordRepromptService.cs b/src/App/Abstractions/IPasswordRepromptService.cs
index 47fc5930d..6acd0ddd2 100644
--- a/src/App/Abstractions/IPasswordRepromptService.cs
+++ b/src/App/Abstractions/IPasswordRepromptService.cs
@@ -7,6 +7,8 @@ namespace Bit.App.Abstractions
string[] ProtectedFields { get; }
Task ShowPasswordPromptAsync();
+
+ Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
Task Enabled();
}
diff --git a/src/App/Pages/Accounts/DeleteAccountPage.xaml b/src/App/Pages/Accounts/DeleteAccountPage.xaml
new file mode 100644
index 000000000..be7f8c68b
--- /dev/null
+++ b/src/App/Pages/Accounts/DeleteAccountPage.xaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App/Pages/Accounts/DeleteAccountPage.xaml.cs b/src/App/Pages/Accounts/DeleteAccountPage.xaml.cs
new file mode 100644
index 000000000..16587cb8d
--- /dev/null
+++ b/src/App/Pages/Accounts/DeleteAccountPage.xaml.cs
@@ -0,0 +1,33 @@
+using System;
+using Xamarin.Forms;
+
+namespace Bit.App.Pages.Accounts
+{
+ public partial class DeleteAccountPage : BaseContentPage
+ {
+ DeleteAccountViewModel _vm;
+
+ public DeleteAccountPage()
+ {
+ InitializeComponent();
+ _vm = BindingContext as DeleteAccountViewModel;
+ _vm.Page = this;
+ }
+
+ private async void Close_Clicked(object sender, EventArgs e)
+ {
+ if (DoOnce())
+ {
+ await Navigation.PopModalAsync();
+ }
+ }
+
+ private async void DeleteAccount_Clicked(object sender, EventArgs e)
+ {
+ if (DoOnce())
+ {
+ await _vm.DeleteAccountAsync();
+ }
+ }
+ }
+}
diff --git a/src/App/Pages/Accounts/DeleteAccountViewModel.cs b/src/App/Pages/Accounts/DeleteAccountViewModel.cs
new file mode 100644
index 000000000..3fa0dd3e1
--- /dev/null
+++ b/src/App/Pages/Accounts/DeleteAccountViewModel.cs
@@ -0,0 +1,84 @@
+using System.Threading.Tasks;
+using Bit.App.Abstractions;
+using Bit.App.Resources;
+using Bit.Core.Abstractions;
+using Bit.Core.Exceptions;
+using Bit.Core.Utilities;
+#if !FDROID
+using Microsoft.AppCenter.Crashes;
+#endif
+
+namespace Bit.App.Pages
+{
+ public class DeleteAccountViewModel : BaseViewModel
+ {
+ readonly IApiService _apiService;
+ readonly IPasswordRepromptService _passwordRepromptService;
+ readonly IMessagingService _messagingService;
+ readonly ICryptoService _cryptoService;
+ readonly IPlatformUtilsService _platformUtilsService;
+ readonly IDeviceActionService _deviceActionService;
+
+ public DeleteAccountViewModel()
+ {
+ _apiService = ServiceContainer.Resolve("apiService");
+ _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService");
+ _messagingService = ServiceContainer.Resolve("messagingService");
+ _cryptoService = ServiceContainer.Resolve("cryptoService");
+ _platformUtilsService = ServiceContainer.Resolve("platformUtilsService");
+ _deviceActionService = ServiceContainer.Resolve("deviceActionService");
+
+ PageTitle = AppResources.DeleteAccount;
+ }
+
+ public async Task DeleteAccountAsync()
+ {
+ try
+ {
+ if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
+ {
+ await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
+ AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
+ return;
+ }
+
+ var (password, valid) = await _passwordRepromptService.ShowPasswordPromptAndGetItAsync();
+ if (!valid)
+ {
+ return;
+ }
+
+ await _deviceActionService.ShowLoadingAsync(AppResources.DeletingYourAccount);
+
+ var masterPasswordHashKey = await _cryptoService.HashPasswordAsync(password, null);
+ await _apiService.DeleteAccountAsync(new Core.Models.Request.DeleteAccountRequest
+ {
+ MasterPasswordHash = masterPasswordHashKey
+ });
+
+ await _deviceActionService.HideLoadingAsync();
+
+ _messagingService.Send("logout");
+
+ await _platformUtilsService.ShowDialogAsync(AppResources.YourAccountHasBeenPermanentlyDeleted);
+ }
+ catch (ApiException apiEx)
+ {
+ await _deviceActionService.HideLoadingAsync();
+
+ if (apiEx?.Error != null)
+ {
+ await _platformUtilsService.ShowDialogAsync(apiEx.Error.GetSingleMessage(), AppResources.AnErrorHasOccurred);
+ }
+ }
+ catch (System.Exception ex)
+ {
+ await _deviceActionService.HideLoadingAsync();
+#if !FDROID
+ Crashes.TrackError(ex);
+#endif
+ await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
+ }
+ }
+ }
+}
diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs
index 25daa689e..9fb7b7524 100644
--- a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs
+++ b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs
@@ -1,10 +1,11 @@
using System.ComponentModel;
-using Bit.App.Abstractions;
-using Bit.App.Resources;
-using Bit.Core.Utilities;
using System.Linq;
using System.Threading.Tasks;
+using Bit.App.Abstractions;
using Bit.App.Controls;
+using Bit.App.Pages.Accounts;
+using Bit.App.Resources;
+using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -134,6 +135,10 @@ namespace Bit.App.Pages
{
await _vm.LogOutAsync();
}
+ else if (item.Name == AppResources.DeleteAccount)
+ {
+ await Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage()));
+ }
else if (item.Name == AppResources.LockNow)
{
await _vm.LockAsync();
diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs
index a81d0a703..2d079ad14 100644
--- a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs
+++ b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs
@@ -490,7 +490,8 @@ namespace Bit.App.Pages
new SettingsPageListItem { Name = AppResources.Options },
new SettingsPageListItem { Name = AppResources.About },
new SettingsPageListItem { Name = AppResources.HelpAndFeedback },
- new SettingsPageListItem { Name = AppResources.RateTheApp }
+ new SettingsPageListItem { Name = AppResources.RateTheApp },
+ new SettingsPageListItem { Name = AppResources.DeleteAccount }
};
GroupedItems.ResetWithRange(new List
{
diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs
index 3f38e52fc..e32b4c101 100644
--- a/src/App/Resources/AppResources.Designer.cs
+++ b/src/App/Resources/AppResources.Designer.cs
@@ -3719,6 +3719,36 @@ namespace Bit.App.Resources {
}
}
+ public static string DeleteAccount {
+ get {
+ return ResourceManager.GetString("DeleteAccount", resourceCulture);
+ }
+ }
+
+ public static string DeletingYourAccountIsPermanent {
+ get {
+ return ResourceManager.GetString("DeletingYourAccountIsPermanent", resourceCulture);
+ }
+ }
+
+ public static string DeleteAccountExplanation {
+ get {
+ return ResourceManager.GetString("DeleteAccountExplanation", resourceCulture);
+ }
+ }
+
+ public static string DeletingYourAccount {
+ get {
+ return ResourceManager.GetString("DeletingYourAccount", resourceCulture);
+ }
+ }
+
+ public static string YourAccountHasBeenPermanentlyDeleted {
+ get {
+ return ResourceManager.GetString("YourAccountHasBeenPermanentlyDeleted", resourceCulture);
+ }
+ }
+
public static string InvalidVerificationCode {
get {
return ResourceManager.GetString("InvalidVerificationCode", resourceCulture);
diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx
index 4b1edf4a5..e357191bc 100644
--- a/src/App/Resources/AppResources.resx
+++ b/src/App/Resources/AppResources.resx
@@ -2093,6 +2093,21 @@
One or more organization policies prevents your from exporting your personal vault.
+
+ Delete Account
+
+
+ Deleting your account is permanent
+
+
+ Your account and all associated data will be erased and unrecoverable. Are you sure you want to continue?
+
+
+ Deleting your account
+
+
+ Your account has been permanently deleted
+
Invalid Verification Code.
diff --git a/src/App/Services/MobilePasswordRepromptService.cs b/src/App/Services/MobilePasswordRepromptService.cs
index 1c9794cd8..8f1b7a04c 100644
--- a/src/App/Services/MobilePasswordRepromptService.cs
+++ b/src/App/Services/MobilePasswordRepromptService.cs
@@ -1,7 +1,7 @@
using System.Threading.Tasks;
-using Bit.Core.Abstractions;
using Bit.App.Abstractions;
using Bit.App.Resources;
+using Bit.Core.Abstractions;
using System;
using Bit.Core.Utilities;
@@ -22,23 +22,23 @@ namespace Bit.App.Services
public async Task ShowPasswordPromptAsync()
{
- if (!await Enabled())
- {
- return true;
- }
+ return await _platformUtilsService.ShowPasswordDialogAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, ValidatePasswordAsync);
+ }
- Func> validator = async (string password) =>
- {
- // Assume user has canceled.
- if (string.IsNullOrWhiteSpace(password))
- {
- return false;
- };
+ public async Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync()
+ {
+ return await _platformUtilsService.ShowPasswordDialogAndGetItAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, ValidatePasswordAsync);
+ }
- return await _cryptoService.CompareAndUpdateKeyHashAsync(password, null);
+ private async Task ValidatePasswordAsync(string password)
+ {
+ // Assume user has canceled.
+ if (string.IsNullOrWhiteSpace(password))
+ {
+ return false;
};
- return await _platformUtilsService.ShowPasswordDialogAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, validator);
+ return await _cryptoService.CompareAndUpdateKeyHashAsync(password, null);
}
public async Task Enabled()
diff --git a/src/App/Services/MobilePlatformUtilsService.cs b/src/App/Services/MobilePlatformUtilsService.cs
index 84017b118..a9a4f1088 100644
--- a/src/App/Services/MobilePlatformUtilsService.cs
+++ b/src/App/Services/MobilePlatformUtilsService.cs
@@ -167,13 +167,18 @@ namespace Bit.App.Services
}
public async Task ShowPasswordDialogAsync(string title, string body, Func> validator)
+ {
+ return (await ShowPasswordDialogAndGetItAsync(title, body, validator)).valid;
+ }
+
+ public async Task<(string password, bool valid)> ShowPasswordDialogAndGetItAsync(string title, string body, Func> validator)
{
var password = await _deviceActionService.DisplayPromptAync(AppResources.PasswordConfirmation,
AppResources.PasswordConfirmationDesc, null, AppResources.Submit, AppResources.Cancel, password: true);
if (password == null)
{
- return false;
+ return (password, false);
}
var valid = await validator(password);
@@ -183,7 +188,7 @@ namespace Bit.App.Services
await ShowDialogAsync(AppResources.InvalidMasterPassword, null, AppResources.Ok);
}
- return valid;
+ return (password, valid);
}
public bool IsDev()
diff --git a/src/App/Styles/Android.xaml b/src/App/Styles/Android.xaml
index 8bdc018fd..66f889746 100644
--- a/src/App/Styles/Android.xaml
+++ b/src/App/Styles/Android.xaml
@@ -151,6 +151,39 @@
+
+
+
+