diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.html b/apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.html deleted file mode 100644 index d7e672427b9..00000000000 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.html +++ /dev/null @@ -1,50 +0,0 @@ - -
-
-

{{ "individualUpgradeWelcomeMessage" | i18n }}

-

- {{ "individualUpgradeDescriptionMessage" | i18n }} -

-
- -
- @if (premiumCardDetails) { - -

- {{ premiumCardDetails.title }} -

-
- } - - @if (familiesCardDetails) { - -

- {{ familiesCardDetails.title }} -

-
- } -
-
-

- {{ "individualUpgradeTaxInformationMessage" | i18n }} -

- -
-
-
diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.html b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.html new file mode 100644 index 00000000000..b1c64ffd792 --- /dev/null +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.html @@ -0,0 +1,68 @@ +@if (!loading()) { +
+
+
+ +
+
+

+ {{ "individualUpgradeWelcomeMessage" | i18n }} +

+

+ {{ "individualUpgradeDescriptionMessage" | i18n }} +

+
+ +
+ @if (premiumCardDetails) { + +

+ {{ premiumCardDetails.title }} +

+
+ } + + @if (familiesCardDetails) { + +

+ {{ familiesCardDetails.title }} +

+
+ } +
+
+

+ {{ "individualUpgradeTaxInformationMessage" | i18n }} +

+ +
+
+
+} diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.spec.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.spec.ts similarity index 67% rename from apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.spec.ts rename to apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.spec.ts index 515c204e8f6..93cfa1da20f 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.spec.ts @@ -1,10 +1,10 @@ +import { CdkTrapFocus } from "@angular/cdk/a11y"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogRef, DialogService } from "@bitwarden/components"; import { PricingCardComponent } from "@bitwarden/pricing"; import { BillingServicesModule } from "../../../services"; @@ -14,19 +14,13 @@ import { PersonalSubscriptionPricingTierIds, } from "../../../types/subscription-pricing-tier"; -import { - UpgradeAccountDialogComponent, - UpgradeAccountDialogResult, - UpgradeAccountDialogStatus, -} from "./upgrade-account-dialog.component"; +import { UpgradeAccountComponent, UpgradeAccountStatus } from "./upgrade-account.component"; -describe("UpgradeAccountDialogComponent", () => { - let sut: UpgradeAccountDialogComponent; - let fixture: ComponentFixture; - const mockDialogRef = mock>(); +describe("UpgradeAccountComponent", () => { + let sut: UpgradeAccountComponent; + let fixture: ComponentFixture; const mockI18nService = mock(); const mockSubscriptionPricingService = mock(); - const mockDialogService = mock(); // Mock pricing tiers data const mockPricingTiers: PersonalSubscriptionPricingTier[] = [ @@ -60,20 +54,19 @@ describe("UpgradeAccountDialogComponent", () => { ); await TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, UpgradeAccountDialogComponent, PricingCardComponent], + imports: [NoopAnimationsModule, UpgradeAccountComponent, PricingCardComponent, CdkTrapFocus], providers: [ - { provide: DialogRef, useValue: mockDialogRef }, { provide: I18nService, useValue: mockI18nService }, { provide: SubscriptionPricingService, useValue: mockSubscriptionPricingService }, ], }) - .overrideComponent(UpgradeAccountDialogComponent, { + .overrideComponent(UpgradeAccountComponent, { // Remove BillingServicesModule to avoid conflicts with mocking SubscriptionPricingService dependencies remove: { imports: [BillingServicesModule] }, }) .compileComponents(); - fixture = TestBed.createComponent(UpgradeAccountDialogComponent); + fixture = TestBed.createComponent(UpgradeAccountComponent); sut = fixture.componentInstance; fixture.detectChanges(); }); @@ -109,40 +102,37 @@ describe("UpgradeAccountDialogComponent", () => { expect(sut["familiesCardDetails"].features).toEqual(["Feature A", "Feature B", "Feature C"]); }); - it("should call dialogRef.close with proceeded-to-payment status and premium pricing tier when premium plan is selected", () => { - sut["onProceedClick"](PersonalSubscriptionPricingTierIds.Premium); + it("should emit planSelected with premium pricing tier when premium plan is selected", () => { + // Arrange + const emitSpy = jest.spyOn(sut.planSelected, "emit"); - expect(mockDialogRef.close).toHaveBeenCalledWith({ - status: UpgradeAccountDialogStatus.ProceededToPayment, - plan: PersonalSubscriptionPricingTierIds.Premium, - }); + // Act + sut.planSelected.emit(PersonalSubscriptionPricingTierIds.Premium); + + // Assert + expect(emitSpy).toHaveBeenCalledWith(PersonalSubscriptionPricingTierIds.Premium); }); - it("should call dialogRef.close with proceeded-to-payment status and families pricing tier when families plan is selected", () => { - sut["onProceedClick"](PersonalSubscriptionPricingTierIds.Families); + it("should emit planSelected with families pricing tier when families plan is selected", () => { + // Arrange + const emitSpy = jest.spyOn(sut.planSelected, "emit"); - expect(mockDialogRef.close).toHaveBeenCalledWith({ - status: UpgradeAccountDialogStatus.ProceededToPayment, - plan: PersonalSubscriptionPricingTierIds.Families, - }); + // Act + sut.planSelected.emit(PersonalSubscriptionPricingTierIds.Families); + + // Assert + expect(emitSpy).toHaveBeenCalledWith(PersonalSubscriptionPricingTierIds.Families); }); - it("should call dialogRef.close with closed status when dialog is closed", () => { - sut["onCloseClick"](); + it("should emit closeClicked with closed status when close button is clicked", () => { + // Arrange + const emitSpy = jest.spyOn(sut.closeClicked, "emit"); - expect(mockDialogRef.close).toHaveBeenCalledWith({ - status: UpgradeAccountDialogStatus.Closed, - plan: null, - }); - }); + // Act + sut.closeClicked.emit(UpgradeAccountStatus.Closed); - it("should return a DialogRef when open static method is called", () => { - mockDialogService.open.mockReturnValue(mockDialogRef); - - const result = UpgradeAccountDialogComponent.open(mockDialogService); - - expect(mockDialogService.open).toHaveBeenCalledWith(UpgradeAccountDialogComponent); - expect(result).toBe(mockDialogRef); + // Assert + expect(emitSpy).toHaveBeenCalledWith(UpgradeAccountStatus.Closed); }); describe("isFamiliesPlan", () => { diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts similarity index 69% rename from apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.ts rename to apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts index 6173cdfd744..a68c64b390e 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts @@ -1,9 +1,11 @@ -import { Component, DestroyRef, OnInit } from "@angular/core"; +import { CdkTrapFocus } from "@angular/cdk/a11y"; +import { CommonModule } from "@angular/common"; +import { Component, DestroyRef, OnInit, output, signal } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; -import { ButtonType, DialogModule, DialogRef, DialogService } from "@bitwarden/components"; +import { ButtonType, DialogModule } from "@bitwarden/components"; import { PricingCardComponent } from "@bitwarden/pricing"; import { SharedModule } from "../../../../shared"; @@ -17,15 +19,15 @@ import { SubscriptionCadenceIds, } from "../../../types/subscription-pricing-tier"; -export const UpgradeAccountDialogStatus = { +export const UpgradeAccountStatus = { Closed: "closed", ProceededToPayment: "proceeded-to-payment", } as const; -export type UpgradeAccountDialogStatus = UnionOfValues; +export type UpgradeAccountStatus = UnionOfValues; -export type UpgradeAccountDialogResult = { - status: UpgradeAccountDialogStatus; +export type UpgradeAccountResult = { + status: UpgradeAccountStatus; plan: PersonalSubscriptionPricingTierId | null; }; @@ -38,20 +40,29 @@ type CardDetails = { }; @Component({ - selector: "app-upgrade-account-dialog", - imports: [DialogModule, SharedModule, BillingServicesModule, PricingCardComponent], - templateUrl: "./upgrade-account-dialog.component.html", + selector: "app-upgrade-account", + imports: [ + CommonModule, + DialogModule, + SharedModule, + BillingServicesModule, + PricingCardComponent, + CdkTrapFocus, + ], + templateUrl: "./upgrade-account.component.html", }) -export class UpgradeAccountDialogComponent implements OnInit { +export class UpgradeAccountComponent implements OnInit { + planSelected = output(); + closeClicked = output(); + protected loading = signal(true); protected premiumCardDetails!: CardDetails; protected familiesCardDetails!: CardDetails; protected familiesPlanType = PersonalSubscriptionPricingTierIds.Families; protected premiumPlanType = PersonalSubscriptionPricingTierIds.Premium; - protected loading = true; + protected closeStatus = UpgradeAccountStatus.Closed; constructor( - private dialogRef: DialogRef, private i18nService: I18nService, private subscriptionPricingService: SubscriptionPricingService, private destroyRef: DestroyRef, @@ -63,7 +74,7 @@ export class UpgradeAccountDialogComponent implements OnInit { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((plans) => { this.setupCardDetails(plans); - this.loading = false; + this.loading.set(false); }); } @@ -108,29 +119,7 @@ export class UpgradeAccountDialogComponent implements OnInit { }; } - protected onProceedClick(plan: PersonalSubscriptionPricingTierId): void { - this.close({ - status: UpgradeAccountDialogStatus.ProceededToPayment, - plan, - }); - } - private isFamiliesPlan(plan: PersonalSubscriptionPricingTierId): boolean { return plan === PersonalSubscriptionPricingTierIds.Families; } - - protected onCloseClick(): void { - this.close({ - status: UpgradeAccountDialogStatus.Closed, - plan: null, - }); - } - - private close(result: UpgradeAccountDialogResult): void { - this.dialogRef.close(result); - } - - static open(dialogService: DialogService): DialogRef { - return dialogService.open(UpgradeAccountDialogComponent); - } }