1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-26 21:23:46 +00:00

Added account deletion feature on settings (#1621)

* Added account deletion feature on settings

* Disabled using Microsoft.AppCenter.Crashes for FDroid

* Moved drawable on Android.csproj to be with the others

Co-authored-by: Federico Maccaroni <fmaccaroni@bitwarden.com>
This commit is contained in:
Federico Maccaroni
2021-11-24 16:09:39 -03:00
committed by GitHub
parent 833103b2a0
commit 9fdf2ada6f
22 changed files with 884 additions and 20 deletions

View File

@@ -7,6 +7,8 @@ namespace Bit.App.Abstractions
string[] ProtectedFields { get; }
Task<bool> ShowPasswordPromptAsync();
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
Task<bool> Enabled();
}

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.Accounts.DeleteAccountPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:DeleteAccountViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:DeleteAccountViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<StackLayout Padding="20, 30" Spacing="0">
<Image
Source="ic_warning"
WidthRequest="28"
HeightRequest="25"
HorizontalOptions="Start" />
<Label
Text="{u:I18n DeletingYourAccountIsPermanent}"
HorizontalOptions="Start"
StyleClass="text-body"
Margin="0,15,0,0"/>
<Label
Text="{u:I18n DeleteAccountExplanation}"
HorizontalOptions="Start"
Margin="0,6,50,0"
Opacity="0.6" />
<Button
Text="{u:I18n Cancel}"
StyleClass="btn-primary"
HorizontalOptions="Start"
Margin="0,26,0,0"
Padding="16,6"
CornerRadius="2"
TextTransform="Uppercase"
Clicked="Close_Clicked" />
<Button
Text="{u:I18n DeleteAccount}"
StyleClass="btn-secondary"
TextColor="#99000000"
HorizontalOptions="Start"
Margin="0,12,0,0"
Padding="16,6"
CornerRadius="2"
TextTransform="Uppercase"
Clicked="DeleteAccount_Clicked"/>
</StackLayout>
</ContentPage.Content>
</pages:BaseContentPage>

View File

@@ -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();
}
}
}
}

View File

@@ -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<IApiService>("apiService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("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);
}
}
}
}

View File

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

View File

@@ -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<SettingsPageListGroup>
{

View File

@@ -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);

View File

@@ -2093,6 +2093,21 @@
<data name="DisablePersonalVaultExportPolicyInEffect">
<value>One or more organization policies prevents your from exporting your personal vault.</value>
</data>
<data name="DeleteAccount" xml:space="preserve">
<value>Delete Account</value>
</data>
<data name="DeletingYourAccountIsPermanent" xml:space="preserve">
<value>Deleting your account is permanent</value>
</data>
<data name="DeleteAccountExplanation" xml:space="preserve">
<value>Your account and all associated data will be erased and unrecoverable. Are you sure you want to continue?</value>
</data>
<data name="DeletingYourAccount" xml:space="preserve">
<value>Deleting your account</value>
</data>
<data name="YourAccountHasBeenPermanentlyDeleted" xml:space="preserve">
<value>Your account has been permanently deleted</value>
</data>
<data name="InvalidVerificationCode" xml:space="preserve">
<value>Invalid Verification Code.</value>
</data>

View File

@@ -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<bool> ShowPasswordPromptAsync()
{
if (!await Enabled())
{
return true;
}
return await _platformUtilsService.ShowPasswordDialogAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, ValidatePasswordAsync);
}
Func<string, Task<bool>> 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<bool> 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<bool> Enabled()

View File

@@ -167,13 +167,18 @@ namespace Bit.App.Services
}
public async Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator)
{
return (await ShowPasswordDialogAndGetItAsync(title, body, validator)).valid;
}
public async Task<(string password, bool valid)> ShowPasswordDialogAndGetItAsync(string title, string body, Func<string, Task<bool>> 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()

View File

@@ -151,6 +151,39 @@
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Button"
Class="btn-secondary">
<Setter Property="BackgroundColor"
Value="Transparent" />
<Setter Property="BorderColor"
Value="{DynamicResource ButtonBorderColor}" />
<Setter Property="BorderWidth"
Value="1" />
<Setter Property="TextColor"
Value="{DynamicResource ButtonTextColor}" />
<Setter Property="FontSize"
Value="Medium" />
<Setter Property="CornerRadius"
Value="5" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{DynamicResource ButtonTextColorDisabled}" />
<Setter Property="BackgroundColor"
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
<Setter Property="BorderColor"
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Button"
ApplyToDerivedTypes="True"
Class="btn-icon-platform">

View File

@@ -67,6 +67,13 @@
<Setter Property="TextType"
Value="Html" />
</Style>
<Style TargetType="Label"
Class="text-body">
<Setter Property="FontSize"
Value="Body" />
<Setter Property="TextColor"
Value="{DynamicResource TextColor}" />
</Style>
<!-- Pages -->
<Style TargetType="TabbedPage"

View File

@@ -172,6 +172,44 @@
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Button"
Class="btn-secondary">
<Setter Property="BackgroundColor"
Value="Transparent" />
<Setter Property="BorderColor"
Value="{DynamicResource ButtonBorderColor}" />
<Setter Property="BorderWidth"
Value="1" />
<Setter Property="TextColor"
Value="{DynamicResource ButtonTextColor}" />
<Setter Property="FontSize"
Value="Medium" />
<Setter Property="CornerRadius"
Value="5" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{DynamicResource ButtonBackgroundColorPressed}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{DynamicResource ButtonTextColorDisabled}" />
<Setter Property="BackgroundColor"
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
<Setter Property="BorderColor"
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Button"
ApplyToDerivedTypes="True"
Class="btn-icon-platform">