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 new file mode 100644 index 00000000000..3dc345d0903 --- /dev/null +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.html @@ -0,0 +1,48 @@ + + + + {{ "individualUpgradeWelcomeMessage" | i18n }} + + {{ "individualUpgradeDescriptionMessage" | i18n }} + + + + + @if (premiumCardDetails) { + + + {{ premiumCardDetails.title }} + + + } + + @if (familiesCardDetails) { + + + {{ familiesCardDetails.title }} + + + } + + + + + 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-dialog/upgrade-account-dialog.component.spec.ts new file mode 100644 index 00000000000..cd3a893ec4d --- /dev/null +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { UpgradeAccountDialogComponent } from "./upgrade-account-dialog.component"; + +describe("UpgradeAccountDialogComponent", () => { + let component: UpgradeAccountDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UpgradeAccountDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(UpgradeAccountDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); 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-dialog/upgrade-account-dialog.component.ts new file mode 100644 index 00000000000..264b7c0a7ce --- /dev/null +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-account-dialog/upgrade-account-dialog.component.ts @@ -0,0 +1,140 @@ +import { Component, DestroyRef, OnInit, OnDestroy } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Subject } from "rxjs"; + +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 { PricingCardComponent } from "@bitwarden/pricing"; + +import { SharedModule } from "../../../../shared"; +import { BillingServicesModule } from "../../../services"; +import { SubscriptionPricingService } from "../../../services/subscription-pricing.service"; +import { + PersonalSubscriptionPricingTier, + PersonalSubscriptionPricingTierId, + PersonalSubscriptionPricingTierIds, +} from "../../../types/subscription-pricing-tier"; + +export const UpgradeAccountDialogStatus = { + Closed: "closed", + ProceededToPayment: "proceeded-to-payment", +} as const; + +export type UpgradeAccountDialogStatus = UnionOfValues; + +export type UpgradeAccountDialogResult = { + status: UpgradeAccountDialogStatus; + plan: PersonalSubscriptionPricingTierId | null; +}; + +type CardDetails = { + title: string; + tagline: string; + price: { amount: number; cadence: "monthly" | "annually" }; + button: { text: string; type: ButtonType }; + features: string[]; +}; + +@Component({ + selector: "app-upgrade-account-dialog", + imports: [DialogModule, SharedModule, BillingServicesModule, PricingCardComponent], + templateUrl: "./upgrade-account-dialog.component.html", +}) +export class UpgradeAccountDialogComponent implements OnInit, OnDestroy { + protected premiumCardDetails!: CardDetails; + protected familiesCardDetails!: CardDetails; + + protected familiesPlanType = PersonalSubscriptionPricingTierIds.Families; + protected premiumPlanType = PersonalSubscriptionPricingTierIds.Premium; + + private destroy$ = new Subject(); + + constructor( + private dialogRef: DialogRef, + private i18nService: I18nService, + private subscriptionPricingService: SubscriptionPricingService, + private destroyRef: DestroyRef, + ) {} + + ngOnInit(): void { + this.subscriptionPricingService + .getPersonalSubscriptionPricingTiers$() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((plans) => { + this.setupCardDetails(plans); + }); + } + + /** Setup card details for the pricing tiers. + * This can be extended in the future for business plans, etc. + */ + private setupCardDetails(plans: PersonalSubscriptionPricingTier[]): void { + const premiumTier = plans.find( + (tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium, + ); + const familiesTier = plans.find( + (tier) => tier.id === PersonalSubscriptionPricingTierIds.Families, + ); + + if (premiumTier) { + this.premiumCardDetails = this.createCardDetails(premiumTier, "primary"); + } + + if (familiesTier) { + this.familiesCardDetails = this.createCardDetails(familiesTier, "secondary"); + } + } + + private createCardDetails( + tier: PersonalSubscriptionPricingTier, + buttonType: ButtonType, + ): CardDetails { + return { + title: tier.name, + tagline: tier.description, + price: { + amount: tier.passwordManager.annualPrice / 12, + cadence: "monthly", + }, + button: { + text: this.i18nService.t( + this.isFamiliesPlan(tier.id) ? "upgradeToFamilies" : "upgradeToPremium", + ), + type: buttonType, + }, + features: tier.passwordManager.features.map((f: any) => f.value), + }; + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + 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); + } +}
+ {{ "individualUpgradeDescriptionMessage" | i18n }} +