1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-04 02:33:33 +00:00

fix(billing): Add unified upgrade dialog component

This commit is contained in:
Stephon Brown
2025-10-01 18:38:21 -04:00
parent 085c8c7946
commit ab44825ecf
2 changed files with 160 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
@if (step() == PlanSelectionStep) {
<app-upgrade-account (planSelected)="onPlanSelected($event)" (closeClicked)="onCloseClicked()" />
} @else if (step() == PaymentStep && selectedPlan() !== null) {
<app-upgrade-payment
[selectedPlanId]="selectedPlan()"
[account]="account()"
(goBack)="previousStep()"
(complete)="onComplete($event)"
/>
}

View File

@@ -0,0 +1,150 @@
import { DIALOG_DATA } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component, Inject, OnInit, signal } from "@angular/core";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
import {
ButtonModule,
DialogConfig,
DialogModule,
DialogRef,
DialogService,
} from "@bitwarden/components";
import { AccountBillingClient } from "../../../clients";
import { BillingServicesModule } from "../../../services";
import { PersonalSubscriptionPricingTierId } from "../../../types/subscription-pricing-tier";
import { UpgradeAccountComponent } from "../upgrade-account/upgrade-account.component";
import { UpgradePaymentService } from "../upgrade-payment/services/upgrade-payment.service";
import {
UpgradePaymentComponent,
UpgradePaymentResult,
} from "../upgrade-payment/upgrade-payment.component";
export const UnifiedUpgradeDialogStatus = {
Closed: "closed",
UpgradedToPremium: "upgradedToPremium",
UpgradedToFamilies: "upgradedToFamilies",
} as const;
export const UnifiedUpgradeDialogStep = {
PlanSelection: "planSelection",
Payment: "payment",
} as const;
export type UnifiedUpgradeDialogStatus = UnionOfValues<typeof UnifiedUpgradeDialogStatus>;
export type UnifiedUpgradeDialogStep = UnionOfValues<typeof UnifiedUpgradeDialogStep>;
export type UnifiedUpgradeDialogResult = {
status: UnifiedUpgradeDialogStatus;
organizationId?: string | null;
};
/**
* Parameters for the UnifiedUpgradeDialog component.
* In order to open the dialog to a specific step, you must provide the `initialStep` parameter and a `selectedPlan` if the step is `Payment`.
*
* @property {Account} account - The user account information.
* @property {UnifiedUpgradeDialogStep | null} [initialStep] - The initial step to show in the dialog, if any.
* @property {PersonalSubscriptionPricingTierId | null} [selectedPlan] - Pre-selected subscription plan, if any.
*/
export type UnifiedUpgradeDialogParams = {
account: Account;
initialStep?: UnifiedUpgradeDialogStep | null;
selectedPlan?: PersonalSubscriptionPricingTierId | null;
};
@Component({
selector: "app-unified-upgrade-dialog",
imports: [
CommonModule,
DialogModule,
ButtonModule,
UpgradeAccountComponent,
UpgradePaymentComponent,
BillingServicesModule,
],
providers: [UpgradePaymentService, AccountBillingClient],
templateUrl: "./unified-upgrade-dialog.component.html",
})
export class UnifiedUpgradeDialogComponent implements OnInit {
// Use signals for dialog state because inputs depend on parent component
protected step = signal<UnifiedUpgradeDialogStep>(UnifiedUpgradeDialogStep.PlanSelection);
protected selectedPlan = signal<PersonalSubscriptionPricingTierId | null>(null);
protected account = signal<Account | null>(null);
protected readonly PaymentStep = UnifiedUpgradeDialogStep.Payment;
protected readonly PlanSelectionStep = UnifiedUpgradeDialogStep.PlanSelection;
constructor(
private dialogRef: DialogRef<UnifiedUpgradeDialogResult>,
@Inject(DIALOG_DATA) private params: UnifiedUpgradeDialogParams,
) {}
ngOnInit(): void {
this.account.set(this.params.account);
this.step.set(this.params.initialStep ?? UnifiedUpgradeDialogStep.PlanSelection);
this.selectedPlan.set(this.params.selectedPlan ?? null);
}
protected onPlanSelected(planId: PersonalSubscriptionPricingTierId): void {
this.selectedPlan.set(planId);
this.nextStep();
}
protected onCloseClicked(): void {
this.close({ status: UnifiedUpgradeDialogStatus.Closed });
}
private close(result: UnifiedUpgradeDialogResult): void {
this.dialogRef.close(result);
}
protected nextStep() {
if (this.step() === UnifiedUpgradeDialogStep.PlanSelection) {
this.step.set(UnifiedUpgradeDialogStep.Payment);
}
}
protected previousStep(): void {
if (this.step() === UnifiedUpgradeDialogStep.Payment) {
this.step.set(UnifiedUpgradeDialogStep.PlanSelection);
this.selectedPlan.set(null);
}
}
protected onComplete(result: UpgradePaymentResult): void {
let status: UnifiedUpgradeDialogStatus;
switch (result.status) {
case "upgradedToPremium":
status = UnifiedUpgradeDialogStatus.UpgradedToPremium;
break;
case "upgradedToFamilies":
status = UnifiedUpgradeDialogStatus.UpgradedToFamilies;
break;
case "closed":
status = UnifiedUpgradeDialogStatus.Closed;
break;
default:
status = UnifiedUpgradeDialogStatus.Closed;
}
this.close({ status, organizationId: result.organizationId });
}
/**
* Opens the unified upgrade dialog.
*
* @param dialogService - The dialog service used to open the component
* @param dialogConfig - The configuration for the dialog including UnifiedUpgradeDialogParams data
* @returns A dialog reference object of type DialogRef<UnifiedUpgradeDialogResult>
*/
static open(
dialogService: DialogService,
dialogConfig: DialogConfig<UnifiedUpgradeDialogParams>,
): DialogRef<UnifiedUpgradeDialogResult> {
return dialogService.open<UnifiedUpgradeDialogResult>(UnifiedUpgradeDialogComponent, {
data: dialogConfig.data,
height: "auto",
});
}
}