diff --git a/apps/desktop/src/app/accounts/delete-account.component.html b/apps/desktop/src/app/accounts/delete-account.component.html new file mode 100644 index 00000000000..1371cee162f --- /dev/null +++ b/apps/desktop/src/app/accounts/delete-account.component.html @@ -0,0 +1,38 @@ + diff --git a/apps/desktop/src/app/accounts/delete-account.component.ts b/apps/desktop/src/app/accounts/delete-account.component.ts new file mode 100644 index 00000000000..c708ba57416 --- /dev/null +++ b/apps/desktop/src/app/accounts/delete-account.component.ts @@ -0,0 +1,48 @@ +import { Component } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; + +import { AccountService } from "@bitwarden/common/abstractions/account/account.service.abstraction"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; + +import { Verification } from "../../../../../libs/common/src/types/verification"; + +@Component({ + selector: "app-delete-account", + templateUrl: "delete-account.component.html", +}) +export class DeleteAccountComponent { + formPromise: Promise; + + deleteForm = this.formBuilder.group({ + verification: undefined as Verification | undefined, + }); + + constructor( + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private formBuilder: FormBuilder, + private accountService: AccountService, + private logService: LogService + ) {} + + get secret() { + return this.deleteForm.get("verification")?.value?.secret; + } + + async submit() { + try { + const verification = this.deleteForm.get("verification").value; + this.formPromise = this.accountService.delete(verification); + await this.formPromise; + this.platformUtilsService.showToast( + "success", + this.i18nService.t("accountDeleted"), + this.i18nService.t("accountDeletedDesc") + ); + } catch (e) { + this.logService.error(e); + } + } +} diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index e9b509e6be8..f775eb9bb44 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -108,6 +108,14 @@ + +
+ + + {{ "deleteAccountDesc" | i18n }} + {{ "deleteAccount" | i18n }} + +
diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 6d5f46fe032..94f49c07f8d 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -18,6 +18,8 @@ import { isWindowsStore } from "@bitwarden/electron/utils"; import { SetPinComponent } from "../components/set-pin.component"; +import { DeleteAccountComponent } from "./delete-account.component"; + @Component({ selector: "app-settings", templateUrl: "settings.component.html", @@ -437,4 +439,8 @@ export class SettingsComponent implements OnInit { this.enableBrowserIntegrationFingerprint ); } + + async openDeleteAccount() { + this.modalService.open(DeleteAccountComponent, { replaceTopModal: true }); + } } diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 41d1a626017..ebcc86847a9 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -40,6 +40,7 @@ import { CipherType } from "@bitwarden/common/enums/cipherType"; import { MenuUpdateRequest } from "../main/menu/menu.updater"; +import { DeleteAccountComponent } from "./accounts/delete-account.component"; import { PremiumComponent } from "./accounts/premium.component"; import { SettingsComponent } from "./accounts/settings.component"; import { ExportComponent } from "./vault/export.component"; @@ -153,9 +154,7 @@ export class AppComponent implements OnInit { this.systemService.cancelProcessReload(); break; case "loggedOut": - if (this.modal != null) { - this.modal.close(); - } + this.modalService.closeAll(); this.notificationsService.updateConnection(); this.updateAppMenu(); await this.systemService.clearPendingClipboard(); @@ -180,9 +179,7 @@ export class AppComponent implements OnInit { } break; case "locked": - if (this.modal != null) { - this.modal.close(); - } + this.modalService.closeAll(); if ( message.userId == null || message.userId === (await this.stateService.getUserId()) @@ -223,6 +220,9 @@ export class AppComponent implements OnInit { } break; } + case "deleteAccount": + this.modalService.open(DeleteAccountComponent, { replaceTopModal: true }); + break; case "openPasswordHistory": await this.openModal( PasswordGeneratorHistoryComponent, @@ -368,9 +368,7 @@ export class AppComponent implements OnInit { } async openExportVault() { - if (this.modal != null) { - this.modal.close(); - } + this.modalService.closeAll(); const [modal, childComponent] = await this.modalService.openViewRef( ExportComponent, @@ -388,9 +386,7 @@ export class AppComponent implements OnInit { } async addFolder() { - if (this.modal != null) { - this.modal.close(); - } + this.modalService.closeAll(); const [modal, childComponent] = await this.modalService.openViewRef( FolderAddEditComponent, @@ -410,9 +406,7 @@ export class AppComponent implements OnInit { } async openGenerator() { - if (this.modal != null) { - this.modal.close(); - } + this.modalService.closeAll(); [this.modal] = await this.modalService.openViewRef( GeneratorComponent, @@ -542,9 +536,7 @@ export class AppComponent implements OnInit { } private async openModal(type: Type, ref: ViewContainerRef) { - if (this.modal != null) { - this.modal.close(); - } + this.modalService.closeAll(); [this.modal] = await this.modalService.openViewRef(type, ref); diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index 23e44d85a2a..a7e46b711ab 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -57,6 +57,7 @@ import localeZhTw from "@angular/common/locales/zh-Hant"; import { NgModule } from "@angular/core"; import { AccessibilityCookieComponent } from "./accounts/accessibility-cookie.component"; +import { DeleteAccountComponent } from "./accounts/delete-account.component"; import { EnvironmentComponent } from "./accounts/environment.component"; import { HintComponent } from "./accounts/hint.component"; import { LockComponent } from "./accounts/lock.component"; @@ -165,6 +166,7 @@ registerLocaleData(localeZhTw, "zh-TW"); AttachmentsComponent, CiphersComponent, CollectionsComponent, + DeleteAccountComponent, EnvironmentComponent, ExportComponent, FolderAddEditComponent, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 53fcb31879d..170ddb72145 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1393,6 +1393,21 @@ "lockWithMasterPassOnRestart": { "message": "Lock with master password on restart" }, + "deleteAccount": { + "message": "Delete account" + }, + "deleteAccountDesc": { + "message": "Proceed below to delete your account and all vault data." + }, + "deleteAccountWarning": { + "message": "Deleting your account is permanent. It cannot be undone." + }, + "accountDeleted": { + "message": "Account deleted" + }, + "accountDeletedDesc": { + "message": "Your account has been closed and all associated data has been deleted." + }, "preferences": { "message": "Preferences" }, @@ -1975,5 +1990,5 @@ }, "cardBrandMir": { "message": "Mir" - } + } } diff --git a/apps/desktop/src/main/menu/menu.account.ts b/apps/desktop/src/main/menu/menu.account.ts index ef85f7462f6..b7c4b1f693f 100644 --- a/apps/desktop/src/main/menu/menu.account.ts +++ b/apps/desktop/src/main/menu/menu.account.ts @@ -19,6 +19,8 @@ export class AccountMenu implements IMenubarMenu { this.changeMasterPassword, this.twoStepLogin, this.fingerprintPhrase, + this.separator, + this.deleteAccount, ]; } @@ -105,6 +107,19 @@ export class AccountMenu implements IMenubarMenu { }; } + private get deleteAccount(): MenuItemConstructorOptions { + return { + label: this.localize("deleteAccount"), + id: "deleteAccount", + click: () => this.sendMessage("deleteAccount"), + enabled: !this._isLocked, + }; + } + + private get separator(): MenuItemConstructorOptions { + return { type: "separator" }; + } + private localize(s: string) { return this._i18nService.t(s); } diff --git a/apps/desktop/src/scss/box.scss b/apps/desktop/src/scss/box.scss index 4f05b873b64..aa11b408a42 100644 --- a/apps/desktop/src/scss/box.scss +++ b/apps/desktop/src/scss/box.scss @@ -102,6 +102,10 @@ color: themed("mutedColor"); } } + + &.last { + margin-bottom: 15px; + } } .box-content-row { diff --git a/apps/desktop/src/scss/misc.scss b/apps/desktop/src/scss/misc.scss index 7982129316c..5e5e341b30f 100644 --- a/apps/desktop/src/scss/misc.scss +++ b/apps/desktop/src/scss/misc.scss @@ -336,6 +336,25 @@ form, @include themify($themes) { color: themed("mutedColor"); } + + a { + @extend .btn; + @extend .link; + + padding: 0; + font-size: inherit; + font-weight: bold; + + @include themify($themes) { + color: themed("mutedColor"); + } + + &:hover { + @include themify($themes) { + color: darken(themed("mutedColor"), 6%); + } + } + } } } diff --git a/apps/desktop/src/scss/pages.scss b/apps/desktop/src/scss/pages.scss index c3626c40c8d..6c839826ea9 100644 --- a/apps/desktop/src/scss/pages.scss +++ b/apps/desktop/src/scss/pages.scss @@ -71,10 +71,6 @@ .box { margin-bottom: 20px; - - &.last { - margin-bottom: 15px; - } } .buttons { diff --git a/apps/web/src/app/settings/delete-account.component.html b/apps/web/src/app/settings/delete-account.component.html index 6d53b4170f8..77b9df007bc 100644 --- a/apps/web/src/app/settings/delete-account.component.html +++ b/apps/web/src/app/settings/delete-account.component.html @@ -6,6 +6,7 @@ (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate + [formGroup]="deleteForm" >