1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00

[PM-28034] Pre-Launch Payment Defect Solution (#17331)

* fix(billing): update to password manager to signal

* fix(billing): take first value so the dialog doesn't show again

* fix(billing): add families plan to request builder

* fix(billing): feedback and type update

* fix(billing): fix selectedplan call
This commit is contained in:
Stephon Brown
2025-11-11 15:51:01 -05:00
committed by GitHub
parent 785b1cfdd2
commit 421edfb020
4 changed files with 41 additions and 36 deletions

View File

@@ -11,6 +11,7 @@ import {
of, of,
shareReplay, shareReplay,
switchMap, switchMap,
take,
} from "rxjs"; } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -182,6 +183,7 @@ export class CloudHostedPremiumVNextComponent {
this.shouldShowUpgradeDialogOnInit$ this.shouldShowUpgradeDialogOnInit$
.pipe( .pipe(
take(1),
switchMap((shouldShowUpgradeDialogOnInit) => { switchMap((shouldShowUpgradeDialogOnInit) => {
if (shouldShowUpgradeDialogOnInit) { if (shouldShowUpgradeDialogOnInit) {
return from(this.openUpgradeDialog("Premium")); return from(this.openUpgradeDialog("Premium"));

View File

@@ -1,6 +1,6 @@
<form [formGroup]="formGroup" [bitSubmit]="submit"> <form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="large" [loading]="loading()"> <bit-dialog dialogSize="large" [loading]="loading()">
<span bitDialogTitle class="tw-font-medium">{{ upgradeToMessage }}</span> <span bitDialogTitle class="tw-font-medium">{{ upgradeToMessage() }}</span>
<ng-container bitDialogContent> <ng-container bitDialogContent>
<section> <section>
@if (isFamiliesPlan) { @if (isFamiliesPlan) {
@@ -50,10 +50,9 @@
</section> </section>
<section> <section>
@if (passwordManager) {
<billing-cart-summary <billing-cart-summary
#cartSummaryComponent #cartSummaryComponent
[passwordManager]="passwordManager" [passwordManager]="passwordManager()"
[estimatedTax]="estimatedTax$ | async" [estimatedTax]="estimatedTax$ | async"
></billing-cart-summary> ></billing-cart-summary>
@if (isFamiliesPlan) { @if (isFamiliesPlan) {
@@ -61,7 +60,6 @@
{{ "paymentChargedWithTrial" | i18n }} {{ "paymentChargedWithTrial" | i18n }}
</p> </p>
} }
}
</section> </section>
</ng-container> </ng-container>

View File

@@ -1,6 +1,7 @@
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
computed,
DestroyRef, DestroyRef,
input, input,
OnInit, OnInit,
@@ -34,7 +35,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
import { ButtonModule, DialogModule, ToastService } from "@bitwarden/components"; import { ButtonModule, DialogModule, ToastService } from "@bitwarden/components";
import { LogService } from "@bitwarden/logging"; import { LogService } from "@bitwarden/logging";
import { CartSummaryComponent, LineItem } from "@bitwarden/pricing"; import { CartSummaryComponent } from "@bitwarden/pricing";
import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { import {
@@ -104,8 +105,6 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
protected readonly account = input.required<Account>(); protected readonly account = input.required<Account>();
protected goBack = output<void>(); protected goBack = output<void>();
protected complete = output<UpgradePaymentResult>(); protected complete = output<UpgradePaymentResult>();
protected selectedPlan: PlanDetails | null = null;
protected hasEnoughAccountCredit$!: Observable<boolean>;
readonly paymentComponent = viewChild.required(EnterPaymentMethodComponent); readonly paymentComponent = viewChild.required(EnterPaymentMethodComponent);
readonly cartSummaryComponent = viewChild.required(CartSummaryComponent); readonly cartSummaryComponent = viewChild.required(CartSummaryComponent);
@@ -116,15 +115,26 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
billingAddress: EnterBillingAddressComponent.getFormGroup(), billingAddress: EnterBillingAddressComponent.getFormGroup(),
}); });
protected readonly selectedPlan = signal<PlanDetails | null>(null);
protected readonly loading = signal(true); protected readonly loading = signal(true);
private pricingTiers$!: Observable<PersonalSubscriptionPricingTier[]>; protected readonly upgradeToMessage = signal("");
// Cart Summary data // Cart Summary data
protected passwordManager!: LineItem; protected readonly passwordManager = computed(() => {
protected estimatedTax$!: Observable<number>; if (!this.selectedPlan()) {
return { name: "", cost: 0, quantity: 0, cadence: "year" as const };
}
// Display data return {
protected upgradeToMessage = ""; name: this.isFamiliesPlan ? "familiesMembership" : "premiumMembership",
cost: this.selectedPlan()!.details.passwordManager.annualPrice,
quantity: 1,
cadence: "year" as const,
};
});
protected hasEnoughAccountCredit$!: Observable<boolean>;
private pricingTiers$!: Observable<PersonalSubscriptionPricingTier[]>;
protected estimatedTax$!: Observable<number>;
constructor( constructor(
private i18nService: I18nService, private i18nService: I18nService,
@@ -162,19 +172,13 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
const planDetails = plans.find((plan) => plan.id === this.selectedPlanId()); const planDetails = plans.find((plan) => plan.id === this.selectedPlanId());
if (planDetails) { if (planDetails) {
this.selectedPlan = { this.selectedPlan.set({
tier: this.selectedPlanId(), tier: this.selectedPlanId(),
details: planDetails, details: planDetails,
}; });
this.passwordManager = {
name: this.isFamiliesPlan ? "familiesMembership" : "premiumMembership",
cost: this.selectedPlan.details.passwordManager.annualPrice,
quantity: 1,
cadence: "year",
};
this.upgradeToMessage = this.i18nService.t( this.upgradeToMessage.set(
this.isFamiliesPlan ? "startFreeFamiliesTrial" : "upgradeToPremium", this.i18nService.t(this.isFamiliesPlan ? "startFreeFamiliesTrial" : "upgradeToPremium"),
); );
} else { } else {
this.complete.emit({ status: UpgradePaymentStatus.Closed, organizationId: null }); this.complete.emit({ status: UpgradePaymentStatus.Closed, organizationId: null });
@@ -228,7 +232,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
return; return;
} }
if (!this.selectedPlan) { if (!this.selectedPlan()) {
throw new Error("No plan selected"); throw new Error("No plan selected");
} }
@@ -260,7 +264,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
} }
private async processUpgrade(): Promise<UpgradePaymentResult> { private async processUpgrade(): Promise<UpgradePaymentResult> {
if (!this.selectedPlan) { if (!this.selectedPlan()) {
throw new Error("No plan selected"); throw new Error("No plan selected");
} }
@@ -308,7 +312,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
const response = await this.upgradePaymentService.upgradeToFamilies( const response = await this.upgradePaymentService.upgradeToFamilies(
this.account(), this.account(),
this.selectedPlan!, this.selectedPlan()!,
paymentMethod, paymentMethod,
paymentFormValues, paymentFormValues,
); );
@@ -344,7 +348,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
// Create an observable for tax calculation // Create an observable for tax calculation
private refreshSalesTax$(): Observable<number> { private refreshSalesTax$(): Observable<number> {
if (this.formGroup.invalid || !this.selectedPlan) { if (this.formGroup.invalid || !this.selectedPlan()) {
return of(this.INITIAL_TAX_VALUE); return of(this.INITIAL_TAX_VALUE);
} }
@@ -353,7 +357,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
return of(this.INITIAL_TAX_VALUE); return of(this.INITIAL_TAX_VALUE);
} }
return from( return from(
this.upgradePaymentService.calculateEstimatedTax(this.selectedPlan, billingAddress), this.upgradePaymentService.calculateEstimatedTax(this.selectedPlan()!, billingAddress),
).pipe( ).pipe(
catchError((error: unknown) => { catchError((error: unknown) => {
this.logService.error("Tax calculation failed:", error); this.logService.error("Tax calculation failed:", error);

View File

@@ -135,6 +135,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
case PlanType.Free: case PlanType.Free:
case PlanType.FamiliesAnnually: case PlanType.FamiliesAnnually:
case PlanType.FamiliesAnnually2019: case PlanType.FamiliesAnnually2019:
case PlanType.FamiliesAnnually2025:
case PlanType.TeamsStarter2023: case PlanType.TeamsStarter2023:
case PlanType.TeamsStarter: case PlanType.TeamsStarter:
return true; return true;