From 2cfbfcbdfe5052e7cc06fa2630df27d598c104f0 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:03:39 -0400 Subject: [PATCH 1/7] Run prettier (#10993) --- .../app/billing/organizations/change-plan-dialog.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index c893f3832dc..14b6864d643 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -62,7 +62,7 @@ class="tw-px-2 tw-py-4" [ngClass]="{ 'tw-py-1': !(selectableProduct === selectedPlan), - 'tw-py-0': selectableProduct === selectedPlan + 'tw-py-0': selectableProduct === selectedPlan, }" >

From 89751f46d6f3d68e2f08630d0448c6becc4268a0 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 11 Sep 2024 15:27:53 -0400 Subject: [PATCH 2/7] [PM-254] Set PDF Attachments in Web to download, add success toast (#10757) * add success toast to pdf attachment download in web * update desktop attachments for toastService * removed trailing comma --------- Co-authored-by: gbubemismith Co-authored-by: SmithThe4th --- apps/browser/src/_locales/en/messages.json | 3 +++ .../vault/popup/components/vault/attachments.component.ts | 4 +++- apps/desktop/src/locales/en/messages.json | 3 +++ apps/desktop/src/vault/app/vault/attachments.component.ts | 4 +++- .../attachments/emergency-access-attachments.component.ts | 4 +++- apps/web/src/app/core/web-file-download.service.ts | 6 ++---- .../app/vault/individual-vault/attachments.component.ts | 4 +++- apps/web/src/app/vault/org-vault/attachments.component.ts | 4 +++- apps/web/src/locales/en/messages.json | 3 +++ .../angular/src/vault/components/attachments.component.ts | 8 +++++++- 10 files changed, 33 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 05525be6ffd..4e6f27bdd2c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4308,6 +4308,9 @@ }, "enterprisePolicyRequirementsApplied": { "message": "Enterprise policy requirements have been applied to this setting" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." }, "showCharacterCount": { "message": "Show character count" diff --git a/apps/browser/src/vault/popup/components/vault/attachments.component.ts b/apps/browser/src/vault/popup/components/vault/attachments.component.ts index ee6f1ac7d07..75819689b44 100644 --- a/apps/browser/src/vault/popup/components/vault/attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault/attachments.component.ts @@ -14,7 +14,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-vault-attachments", @@ -38,6 +38,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, + toastService: ToastService, ) { super( cipherService, @@ -52,6 +53,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On dialogService, billingAccountProfileStateService, accountService, + toastService, ); } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 5345c6e15a3..4ef70887a45 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3061,5 +3061,8 @@ }, "ssoError": { "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/vault/app/vault/attachments.component.ts b/apps/desktop/src/vault/app/vault/attachments.component.ts index b1ddcbc7e7d..2e25d390872 100644 --- a/apps/desktop/src/vault/app/vault/attachments.component.ts +++ b/apps/desktop/src/vault/app/vault/attachments.component.ts @@ -11,7 +11,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-vault-attachments", @@ -30,6 +30,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, + toastService: ToastService, ) { super( cipherService, @@ -44,6 +45,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { dialogService, billingAccountProfileStateService, accountService, + toastService, ); } } diff --git a/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts b/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts index 9d763886fb4..e0a6f6c53d5 100644 --- a/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts @@ -12,7 +12,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "emergency-access-attachments", @@ -34,6 +34,7 @@ export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponen dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, + toastService: ToastService, ) { super( cipherService, @@ -48,6 +49,7 @@ export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponen dialogService, billingAccountProfileStateService, accountService, + toastService, ); } diff --git a/apps/web/src/app/core/web-file-download.service.ts b/apps/web/src/app/core/web-file-download.service.ts index 743048dc3b9..ad034702a55 100644 --- a/apps/web/src/app/core/web-file-download.service.ts +++ b/apps/web/src/app/core/web-file-download.service.ts @@ -12,14 +12,12 @@ export class WebFileDownloadService implements FileDownloadService { download(request: FileDownloadRequest): void { const builder = new FileDownloadBuilder(request); const a = window.document.createElement("a"); - if (builder.downloadMethod === "save") { - a.download = request.fileName; - } else if (!this.platformUtilsService.isSafari()) { + if (!this.platformUtilsService.isSafari()) { a.rel = "noreferrer"; a.target = "_blank"; } a.href = URL.createObjectURL(builder.blob); - a.style.position = "fixed"; + a.download = request.fileName; window.document.body.appendChild(a); a.click(); window.document.body.removeChild(a); diff --git a/apps/web/src/app/vault/individual-vault/attachments.component.ts b/apps/web/src/app/vault/individual-vault/attachments.component.ts index 7a5706319ee..b578efcae67 100644 --- a/apps/web/src/app/vault/individual-vault/attachments.component.ts +++ b/apps/web/src/app/vault/individual-vault/attachments.component.ts @@ -12,7 +12,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-vault-attachments", @@ -33,6 +33,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, + toastService: ToastService, ) { super( cipherService, @@ -47,6 +48,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { dialogService, billingAccountProfileStateService, accountService, + toastService, ); } diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index 9ebb917aaf5..2bba4d389c0 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -15,7 +15,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { AttachmentsComponent as BaseAttachmentsComponent } from "../individual-vault/attachments.component"; @@ -39,6 +39,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, + toastService: ToastService, ) { super( cipherService, @@ -52,6 +53,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On dialogService, billingAccountProfileStateService, accountService, + toastService, ); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 6834e350155..ffb82cd8160 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9070,6 +9070,9 @@ "secretsManagerWithFreePasswordManagerInfo": { "message": "Your complementary one year Password Manager subscription will upgrade to the selected plan. You will not be charged until the complimentary period is over." }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, "publicApi": { "message": "Public API", "description": "The text, 'API', is an acronymn and should not be translated." diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index e377427eb89..4ae68c9ca9d 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -17,7 +17,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Directive() export class AttachmentsComponent implements OnInit { @@ -49,6 +49,7 @@ export class AttachmentsComponent implements OnInit { protected dialogService: DialogService, protected billingAccountProfileStateService: BillingAccountProfileStateService, protected accountService: AccountService, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -182,6 +183,11 @@ export class AttachmentsComponent implements OnInit { fileName: attachment.fileName, blobData: decBuf, }); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("fileSavedToDevice"), + }); } catch (e) { this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); } From 3be5c4800b3a281bd43e6141f77111c7eafc7d05 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 12 Sep 2024 10:21:23 -0700 Subject: [PATCH 4/7] Do not test napi crate on windows (#11003) * Do not test napi crate on windows possibly related to https://github.com/napi-rs/napi-rs/issues/1405. We are seeing buffer overflows in ci due to repeated Node-API GetProcAddress failures. We don't have any tests in the napi crate, so there's no harm in removing those tests right now. If we have tests there in the future, we'll need to actually fix this. However, the napi crate is just a wiring crate, so maybe we won't ever have any unit tests there. * include crate in name * Remove crate axis --- .github/workflows/test.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b4cd52ac8e..8d4067c1167 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -138,7 +138,12 @@ jobs: eval "$(printf '\n' | /usr/bin/gnome-keyring-daemon --start)" cargo test -- --test-threads=1 - - name: Test Windows / macOS - if: ${{ matrix.os!='ubuntu-latest' }} + - name: Test macOS + if: ${{ matrix.os=='macos-latest' }} working-directory: ./apps/desktop/desktop_native run: cargo test -- --test-threads=1 + + - name: Test Windows + if: ${{ matrix.os=='windows-latest'}} + working-directory: ./apps/desktop/desktop_native/core + run: cargo test -- --test-threads=1 From 07d2e364963569974a685cc328b628bcd823ab4c Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Thu, 12 Sep 2024 13:47:35 -0400 Subject: [PATCH 5/7] [PM-10914] add option to delete all folders if migration fails (#10983) * add option to delete all folders if migration fails * update text and flow to reattempt migration * clear encrypted folders as well on delete all * Update messaging --- .../migrate-legacy-encryption.component.ts | 24 ++++++++++++++++--- apps/web/src/locales/en/messages.json | 6 +++++ .../folder/folder-api.service.abstraction.ts | 1 + .../services/folder/folder-api.service.ts | 5 ++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts index 68eaae618fd..6c894f4fa85 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts @@ -7,9 +7,9 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { ToastService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../shared"; import { UserKeyRotationModule } from "../key-rotation/user-key-rotation.module"; @@ -31,12 +31,13 @@ export class MigrateFromLegacyEncryptionComponent { private accountService: AccountService, private keyRotationService: UserKeyRotationService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService, private messagingService: MessagingService, private logService: LogService, private syncService: SyncService, private toastService: ToastService, + private dialogService: DialogService, + private folderApiService: FolderApiServiceAbstraction, ) {} submit = async () => { @@ -69,6 +70,23 @@ export class MigrateFromLegacyEncryptionComponent { }); this.messagingService.send("logout"); } catch (e) { + // If the error is due to missing folders, we can delete all folders and try again + if (e.message === "All existing folders must be included in the rotation.") { + const deleteFolders = await this.dialogService.openSimpleDialog({ + type: "warning", + title: { key: "encryptionKeyUpdateCannotProceed" }, + content: { key: "keyUpdateFoldersFailed" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: { key: "cancel" }, + }); + + if (deleteFolders) { + await this.folderApiService.deleteAll(); + await this.syncService.fullSync(true, true); + await this.submit(); + return; + } + } this.logService.error(e); throw e; } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index ffb82cd8160..c74fd3386ab 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3894,6 +3894,12 @@ } } }, + "encryptionKeyUpdateCannotProceed": { + "message": "Encryption key update cannot proceed" + }, + "keyUpdateFoldersFailed": { + "message": "When updating your encryption key, your folders could not be decrypted. To continue with the update, your folders must be deleted. No vault items will be deleted if you proceed." + }, "keyUpdated": { "message": "Key updated" }, diff --git a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts index d29ff71290a..1762400b3dd 100644 --- a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts @@ -5,4 +5,5 @@ export class FolderApiServiceAbstraction { save: (folder: Folder) => Promise; delete: (id: string) => Promise; get: (id: string) => Promise; + deleteAll: () => Promise; } diff --git a/libs/common/src/vault/services/folder/folder-api.service.ts b/libs/common/src/vault/services/folder/folder-api.service.ts index c618c958720..e46df37c176 100644 --- a/libs/common/src/vault/services/folder/folder-api.service.ts +++ b/libs/common/src/vault/services/folder/folder-api.service.ts @@ -32,6 +32,11 @@ export class FolderApiService implements FolderApiServiceAbstraction { await this.folderService.delete(id); } + async deleteAll(): Promise { + await this.apiService.send("DELETE", "/folders/all", null, true, false); + await this.folderService.clear(); + } + async get(id: string): Promise { const r = await this.apiService.send("GET", "/folders/" + id, null, true, true); return new FolderResponse(r); From f70b3df2d2bff4a0aeb5a1f5940e2f4f894b5d26 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:04:39 +0200 Subject: [PATCH 6/7] [PM-11949] Fix generating and copying export password (#10999) * Use password field value instead of local variable for copy to clipboard Use appCopyClick directive instead of manually copying and showing success toast * Add missing "copySuccessful" message key to desktop and web * Remove whitespace from web en/messages.json --------- Co-authored-by: Daniel James Smith --- apps/desktop/src/locales/en/messages.json | 3 +++ apps/web/src/locales/en/messages.json | 3 +++ .../src/components/export.component.html | 4 +++- .../src/components/export.component.ts | 16 +++------------- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 4ef70887a45..9194fd7c22a 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1285,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index c74fd3386ab..45c2ac45b61 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -562,6 +562,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "copyValue": { "message": "Copy value", "description": "Copy value to clipboard" diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html index 626934b20e8..7555b206976 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html @@ -87,7 +87,9 @@ [disabled]="!filePassword" appStopClick bitSuffix - (click)="copyPasswordToClipboard()" + [appCopyClick]="filePassword" + [valueLabel]="'password' | i18n" + showToast > {{ "exportPasswordDescription" | i18n }} diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index d83d189cd79..e4f5ec9d32d 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -121,7 +121,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { encryptedExportType = EncryptedExportType; protected showFilePassword: boolean; - filePasswordValue: string = null; private _disabledByPolicy = false; organizations$: Observable; @@ -278,18 +277,9 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { generatePassword = async () => { const [options] = await this.passwordGenerationService.getOptions(); - this.filePasswordValue = await this.passwordGenerationService.generatePassword(options); - this.exportForm.get("filePassword").setValue(this.filePasswordValue); - this.exportForm.get("confirmFilePassword").setValue(this.filePasswordValue); - }; - - copyPasswordToClipboard = async () => { - this.platformUtilsService.copyToClipboard(this.filePasswordValue); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("valueCopied", this.i18nService.t("password")), - }); + const generatedPassword = await this.passwordGenerationService.generatePassword(options); + this.exportForm.get("filePassword").setValue(generatedPassword); + this.exportForm.get("confirmFilePassword").setValue(generatedPassword); }; submit = async () => { From fc2c83f0d35343d3c59fd7a736e1caf299d53f37 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:12:38 +0100 Subject: [PATCH 7/7] [AC-3022]Selecting a plan makes the plan cards content to auto-adjusts (#10992) * Resolve the recommended issue * Resolve the discount display issues * remove unused tw property * Resolve all the outstanding bugs * Fix the A11y bug * Resolve the base storage issue * Rename service account in the summary * changes for the A11y bug * Fix the improper keyboard navigation in modal * Add some additional ui changes --- .../change-plan-dialog.component.html | 266 ++++++++++++++---- .../change-plan-dialog.component.ts | 71 +++-- ...nization-subscription-cloud.component.html | 35 ++- ...ganization-subscription-cloud.component.ts | 13 + apps/web/src/locales/en/messages.json | 4 +- 5 files changed, 303 insertions(+), 86 deletions(-) diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index 14b6864d643..766646003ba 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -7,31 +7,37 @@

{{ "upgradePlans" | i18n }}

{{ "selectAPlan" | i18n }} +
{{ "upgradeDiscount" | i18n - : (this.discountPercentageFromSub > 0 - ? discountPercentageFromSub - : this.discountPercentage) + : (selectedInterval === planIntervals.Annually + ? discountPercentageFromSub + this.discountPercentage + : this.discountPercentageFromSub) }} - +
{{ planInterval.name }} @@ -40,6 +46,7 @@
+
{{ "recommended" | i18n }}
-

+

{{ selectableProduct.nameLocalizationKey | i18n }} - + {{ "current" | i18n }}

@@ -133,10 +151,13 @@ else nonEnterprisePlans " > -

+

{{ "bitwardenPasswordManager" | i18n }}

-

{{ "enterprisePlanUpgradeMessage" | i18n }}

+

{{ "enterprisePlanUpgradeMessage" | i18n }}

  • @@ -157,7 +178,10 @@
-

+

{{ "bitwardenSecretsManager" | i18n }}

    @@ -195,25 +219,25 @@

    {{ "bitwardenPasswordManager" | i18n }}

    {{ "teamsPlanUpgradeMessage" | i18n }}

    {{ "familyPlanUpgradeMessage" | i18n }}

    • @@ -247,7 +271,7 @@

    {{ "secretsManagerSubInfo" | i18n }} - {{ "secretsManagerWithFreePasswordManagerInfo" | i18n }} + {{ "secretsManagerComplimentaryPasswordManager" | i18n }}
    @@ -392,23 +416,37 @@

    - {{ organization.maxStorageGb }} + {{ storageGb }} {{ "additionalStorageGbMessage" | i18n }} × {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }} /{{ "year" | i18n }} - {{ - organization.maxStorageGb * selectedPlan.PasswordManager.additionalStoragePricePerGb - | currency: "$" - }} + {{ additionalStorageTotal(selectedPlan) | currency: "$" }} +

    + +

    + + + {{ + "providerDiscount" + | i18n: this.discountPercentageFromSub + this.discountPercentage + | lowercase + }} + + {{ + calculateTotalAppliedDiscount( + passwordManagerSeatTotal(selectedPlan) + additionalStorageTotal(selectedPlan) + ) | currency: "$" + }} +

    @@ -459,18 +497,40 @@ bitTypography="body2" *ngIf=" selectedPlan?.SecretsManager?.hasAdditionalServiceAccountOption && - additionalServiceAccount + additionalServiceAccount > 0 " > {{ additionalServiceAccount }} - {{ "additionalStorageGbMessage" | i18n }} + {{ "serviceAccounts" | i18n | lowercase }} × {{ selectedPlan?.SecretsManager?.additionalPricePerServiceAccount | currency: "$" }} /{{ "month" | i18n }} {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}

    + +

    + + + {{ + "providerDiscount" + | i18n: this.discountPercentageFromSub + this.discountPercentage + | lowercase + }} + + {{ + calculateTotalAppliedDiscount( + additionalServiceAccountTotal(selectedPlan) + + secretsManagerSeatTotal(selectedPlan, sub.smSeats) + ) | currency: "$" + }} + +

    @@ -512,24 +572,39 @@

    - {{ organization.maxStorageGb }} + {{ storageGb }} {{ "additionalStorageGbMessage" | i18n }} × {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }} /{{ "month" | i18n }} {{ - organization.maxStorageGb * selectedPlan.PasswordManager.additionalStoragePricePerGb - | currency: "$" + storageGb * selectedPlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}

    + +

    + + + {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }} + + {{ calculateTotalAppliedDiscount(total) | currency: "$" }} + +

    {{ "secretsManager" | i18n }} @@ -575,18 +650,41 @@ bitTypography="body2" *ngIf=" selectedPlan.SecretsManager.hasAdditionalServiceAccountOption && - additionalServiceAccount + additionalServiceAccount > 0 " > {{ additionalServiceAccount }} - {{ "additionalStorageGbMessage" | i18n }} + {{ "serviceAccounts" | i18n | lowercase }} × {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} /{{ "month" | i18n }} {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}

    + +

    + + + {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }} + + {{ + additionalServiceAccountTotal(selectedPlan) + + secretsManagerSeatTotal(selectedPlan, sub?.smSeats) | currency: "$" + }} + +

@@ -641,18 +739,40 @@ bitTypography="body2" *ngIf=" selectedPlan.SecretsManager.hasAdditionalServiceAccountOption && - additionalServiceAccount + additionalServiceAccount > 0 " > {{ additionalServiceAccount }} - {{ "additionalStorageGbMessage" | i18n }} + {{ "serviceAccounts" | i18n }} × {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} /{{ "month" | i18n }} {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}

+ +

+ + + {{ + "providerDiscount" + | i18n: this.discountPercentageFromSub + this.discountPercentage + | lowercase + }} + + {{ + calculateTotalAppliedDiscount( + additionalServiceAccountTotal(selectedPlan) + + secretsManagerSeatTotal(selectedPlan, sub.smSeats) + ) | currency: "$" + }} + +

{{ "passwordManager" | i18n }} @@ -663,7 +783,7 @@ *ngIf="selectedPlan.PasswordManager.basePrice" > - {{ organization.seats }} + {{ sub?.seats }} {{ "members" | i18n }} × {{ (selectedPlan.isAnnual @@ -694,7 +814,7 @@ {{ "additionalUsers" | i18n }}: - {{ organization.seats || 0 }}  + {{ sub?.seats || 0 }}  {{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} @@ -756,12 +876,12 @@ bitTypography="body2" *ngIf=" selectedPlan.SecretsManager.hasAdditionalServiceAccountOption && - additionalServiceAccount + additionalServiceAccount > 0 " > {{ additionalServiceAccount }} - {{ "additionalStorageGbMessage" | i18n }} + {{ "serviceAccounts" | i18n }} × {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} /{{ "month" | i18n }} @@ -795,7 +915,7 @@ {{ "additionalUsers" | i18n }}: - {{ organization.seats }}  + {{ sub?.seats }}  {{ "members" | i18n }} × {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} @@ -811,6 +931,46 @@

+ +
+ +

+ + + {{ + "providerDiscount" + | i18n: this.discountPercentageFromSub + this.discountPercentage + | lowercase + }} + + {{ + calculateTotalAppliedDiscount(total) | currency: "$" + }} + + + + {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }} + + {{ calculateTotalAppliedDiscount(total) | currency: "$" }} + +

+
+

{{ total | currency: "USD" : "$" }} - / {{ selectedPlanInterval | i18n }} + + / {{ selectedPlanInterval | i18n }}

diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 9a20fe38efc..dc9f6cce688 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -246,27 +246,28 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { selected: false, }, ]; - this.discountPercentageFromSub = this.sub?.customerDiscount?.percentOff; + this.discountPercentageFromSub = this.isSecretsManagerTrial() + ? 0 + : (this.sub?.customerDiscount?.percentOff ?? 0); this.setInitialPlanSelection(); this.loading = false; } setInitialPlanSelection() { - if ( - this.organization.useSecretsManager && - this.currentPlan.productTier == ProductTierType.Free - ) { - this.selectPlan(this.getPlanByType(ProductTierType.Teams)); - } else { - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); - } + this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); } getPlanByType(productTier: ProductTierType) { return this.selectableProducts.find((product) => product.productTier === productTier); } + secretsManagerTrialDiscount() { + return this.sub?.customerDiscount?.appliesTo?.includes("sm-standalone") + ? this.discountPercentage + : this.discountPercentageFromSub + this.discountPercentage; + } + isSecretsManagerTrial(): boolean { return ( this.sub?.subscription?.items?.some((item) => @@ -276,14 +277,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } planTypeChanged() { - if ( - this.organization.useSecretsManager && - this.currentPlan.productTier == ProductTierType.Free - ) { - this.selectPlan(this.getPlanByType(ProductTierType.Teams)); - } else { - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); - } + this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); } updateInterval(event: number) { @@ -304,6 +298,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ]; } + optimizedNgForRender(index: number) { + return index; + } + protected getPlanCardContainerClasses(plan: PlanResponse, index: number) { let cardState: PlanCardState; @@ -370,6 +368,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ) { return; } + + if (plan === this.currentPlan) { + return; + } this.selectedPlan = plan; this.formGroup.patchValue({ productTier: plan.productTier }); } @@ -463,6 +465,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return result; } + get storageGb() { + return this.sub?.maxStorageGb - 1; + } + passwordManagerSeatTotal(plan: PlanResponse): number { if (!plan.PasswordManager.hasAdditionalSeatsOption || this.isSecretsManagerTrial()) { return 0; @@ -486,8 +492,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } return ( - plan.PasswordManager.additionalStoragePricePerGb * - Math.abs(this.organization.maxStorageGb || 0) + plan.PasswordManager.additionalStoragePricePerGb * Math.abs(this.sub?.maxStorageGb - 1 || 0) ); } @@ -499,7 +504,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } additionalServiceAccountTotal(plan: PlanResponse): number { - if (!plan.SecretsManager.hasAdditionalServiceAccountOption || this.additionalServiceAccount) { + if ( + !plan.SecretsManager.hasAdditionalServiceAccountOption || + this.additionalServiceAccount == 0 + ) { return 0; } @@ -541,7 +549,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { if (this.selectedPlan.productTier === ProductTierType.Families) { return this.selectedPlan.PasswordManager.baseSeats; } - return this.organization.seats; + return this.sub?.seats; } get total() { @@ -565,7 +573,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get additionalServiceAccount() { - const baseServiceAccount = this.selectedPlan.SecretsManager?.baseServiceAccount || 0; + const baseServiceAccount = this.currentPlan.SecretsManager?.baseServiceAccount || 0; const usedServiceAccounts = this.sub?.smServiceAccounts || 0; const additionalServiceAccounts = baseServiceAccount - usedServiceAccounts; @@ -652,7 +660,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { if (!this.acceptingSponsorship && !this.isInTrialFlow) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/organizations/" + orgId]); + this.router.navigate(["/organizations/" + orgId + "/members"]); } if (this.isInTrialFlow) { @@ -676,11 +684,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private async updateOrganization() { const request = new OrganizationUpgradeRequest(); if (this.selectedPlan.productTier !== ProductTierType.Families) { - request.additionalSeats = this.organization.seats; + request.additionalSeats = this.sub?.seats; } - if (this.organization.maxStorageGb > this.selectedPlan.PasswordManager.baseStorageGb) { + if (this.sub?.maxStorageGb > this.selectedPlan.PasswordManager.baseStorageGb) { request.additionalStorageGb = - this.organization.maxStorageGb - this.selectedPlan.PasswordManager.baseStorageGb; + this.sub?.maxStorageGb - this.selectedPlan.PasswordManager.baseStorageGb; } request.premiumAccessAddon = this.selectedPlan.PasswordManager.hasPremiumAccessOption && @@ -768,6 +776,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { request.additionalSmSeats = this.organization.seats; } else { request.additionalSmSeats = this.sub?.smSeats; + request.additionalServiceAccounts = this.additionalServiceAccount; } } @@ -812,6 +821,16 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.totalOpened = !this.totalOpened; } + calculateTotalAppliedDiscount(total: number) { + const discountPercent = + this.selectedInterval == PlanInterval.Annually + ? this.discountPercentage + this.discountPercentageFromSub + : this.discountPercentageFromSub; + + const discountedTotal = total / (1 - discountPercent / 100); + return discountedTotal; + } + get paymentSourceClasses() { if (this.billing.paymentSource == null) { return []; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 25c8c547b2b..341324c4a2a 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -69,14 +69,25 @@ >
- {{ - "details" | i18n - }} + {{ "details" | i18n + }}{{ "providerDiscount" | i18n: customerDiscount?.percentOff }} - + {{ i.productName | i18n }} - {{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @ {{ i.amount | currency: "$" }} @@ -91,7 +102,19 @@ {{ "freeForOneYear" | i18n }} - {{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }} +
+ + {{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }} + + {{ + calculateTotalAppliedDiscount(i.quantity * i.amount) | currency: "$" + }} + / {{ "year" | i18n }} +
@@ -112,7 +135,7 @@ -
+