From caf4ca698061f86e88683c868778dedb23b5e900 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 17 Sep 2025 09:30:39 -0700 Subject: [PATCH] [PM-25633] - fix premium upgrade prompt (#16445) * fix premium upgrade prompt * use map instead of adding tap dep * update route --- .../vault-item-dialog.component.ts | 13 ++++++++ .../web-premium-upgrade-prompt.service.ts | 33 +++++++++++-------- .../premium-upgrade-prompt.service.ts | 3 ++ 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 78e71b392e5..88e2a40ec6a 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; import { firstValueFrom, Subject, switchMap } from "rxjs"; import { map } from "rxjs/operators"; @@ -277,6 +278,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { protected attachmentsButtonDisabled = false; + protected confirmedPremiumUpgrade = false; + constructor( @Inject(DIALOG_DATA) protected params: VaultItemDialogParams, private dialogRef: DialogRef, @@ -296,6 +299,12 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { private routedVaultFilterService: RoutedVaultFilterService, ) { this.updateTitle(); + this.premiumUpgradeService.upgradeConfirmed$ + .pipe( + map((c) => c && (this.confirmedPremiumUpgrade = true)), + takeUntilDestroyed(), + ) + .subscribe(); } async ngOnInit() { @@ -339,6 +348,10 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { } ngOnDestroy() { + // If the user already confirmed a premium upgrade, don't emit any other result as it will overwrite the premium upgrade result. + if (this.confirmedPremiumUpgrade) { + return; + } // If the cipher was modified, be sure we emit the saved result in case the dialog was closed with the X button or ESC key. if (this._cipherModified) { this.dialogRef.close(VaultItemDialogResult.Saved); diff --git a/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts index ad34d11f45a..7dfd1146469 100644 --- a/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts +++ b/apps/web/src/app/vault/services/web-premium-upgrade-prompt.service.ts @@ -1,5 +1,6 @@ import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; +import { Subject } from "rxjs"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; @@ -7,11 +8,11 @@ import { DialogRef, DialogService } from "@bitwarden/components"; import { VaultItemDialogResult } from "../components/vault-item-dialog/vault-item-dialog.component"; -/** - * This service is used to prompt the user to upgrade to premium. - */ @Injectable() export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePromptService { + private readonly _upgradeConfirmed$ = new Subject(); + readonly upgradeConfirmed$ = this._upgradeConfirmed$.asObservable(); + constructor( private dialogService: DialogService, private router: Router, @@ -19,34 +20,40 @@ export class WebVaultPremiumUpgradePromptService implements PremiumUpgradePrompt ) {} /** - * Prompts the user to upgrade to premium. - * @param organizationId The ID of the organization to upgrade. + * Prompts the user for a premium upgrade. */ async promptForPremium(organizationId?: OrganizationId) { - let upgradeConfirmed; + let confirmed = false; + let route: string[] | null = null; + if (organizationId) { - upgradeConfirmed = await this.dialogService.openSimpleDialog({ + confirmed = await this.dialogService.openSimpleDialog({ title: { key: "upgradeOrganization" }, content: { key: "upgradeOrganizationDesc" }, acceptButtonText: { key: "upgradeOrganization" }, type: "info", }); - if (upgradeConfirmed) { - await this.router.navigate(["organizations", organizationId, "billing", "subscription"]); + if (confirmed) { + route = ["organizations", organizationId, "billing", "subscription"]; } } else { - upgradeConfirmed = await this.dialogService.openSimpleDialog({ + confirmed = await this.dialogService.openSimpleDialog({ title: { key: "premiumRequired" }, content: { key: "premiumRequiredDesc" }, acceptButtonText: { key: "upgrade" }, type: "success", }); - if (upgradeConfirmed) { - await this.router.navigate(["settings/subscription/premium"]); + if (confirmed) { + route = ["settings/subscription/premium"]; } } - if (upgradeConfirmed) { + this._upgradeConfirmed$.next(confirmed); + + if (route) { + await this.router.navigate(route); + } + if (confirmed) { this.dialog.close(VaultItemDialogResult.PremiumUpgrade); } } diff --git a/libs/common/src/vault/abstractions/premium-upgrade-prompt.service.ts b/libs/common/src/vault/abstractions/premium-upgrade-prompt.service.ts index 8733baaa471..312ad910e4c 100644 --- a/libs/common/src/vault/abstractions/premium-upgrade-prompt.service.ts +++ b/libs/common/src/vault/abstractions/premium-upgrade-prompt.service.ts @@ -1,7 +1,10 @@ +import { Observable } from "rxjs"; + /** * This interface defines the a contract for a service that prompts the user to upgrade to premium. * It ensures that PremiumUpgradePromptService contains a promptForPremium method. */ export abstract class PremiumUpgradePromptService { abstract promptForPremium(organizationId?: string): Promise; + abstract upgradeConfirmed$?: Observable; }