1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +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,
shareReplay,
switchMap,
take,
} from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -182,6 +183,7 @@ export class CloudHostedPremiumVNextComponent {
this.shouldShowUpgradeDialogOnInit$
.pipe(
take(1),
switchMap((shouldShowUpgradeDialogOnInit) => {
if (shouldShowUpgradeDialogOnInit) {
return from(this.openUpgradeDialog("Premium"));

View File

@@ -1,6 +1,6 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<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>
<section>
@if (isFamiliesPlan) {
@@ -50,17 +50,15 @@
</section>
<section>
@if (passwordManager) {
<billing-cart-summary
#cartSummaryComponent
[passwordManager]="passwordManager"
[estimatedTax]="estimatedTax$ | async"
></billing-cart-summary>
@if (isFamiliesPlan) {
<p bitTypography="helper" class="tw-italic tw-text-muted !tw-mb-0">
{{ "paymentChargedWithTrial" | i18n }}
</p>
}
<billing-cart-summary
#cartSummaryComponent
[passwordManager]="passwordManager()"
[estimatedTax]="estimatedTax$ | async"
></billing-cart-summary>
@if (isFamiliesPlan) {
<p bitTypography="helper" class="tw-italic tw-text-muted !tw-mb-0">
{{ "paymentChargedWithTrial" | i18n }}
</p>
}
</section>
</ng-container>

View File

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

View File

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