1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

-[PM-27123] Update Signals and Update Estimated Tax and Credit Logic (#17055)

* billing(fix): update signals and update estimated tax and credit logic

* fix(billing): update with claude feedback and expose total observable
This commit is contained in:
Stephon Brown
2025-10-28 11:25:07 -04:00
committed by GitHub
parent 714daa5779
commit bf66b5ac19
3 changed files with 48 additions and 49 deletions

View File

@@ -54,7 +54,7 @@
<billing-cart-summary
#cartSummaryComponent
[passwordManager]="passwordManager"
[estimatedTax]="estimatedTax"
[estimatedTax]="estimatedTax$ | async"
></billing-cart-summary>
@if (isFamiliesPlan) {
<p bitTypography="helper" class="tw-italic tw-text-muted !tw-mb-0">

View File

@@ -1,12 +1,12 @@
import {
AfterViewChecked,
AfterViewInit,
Component,
DestroyRef,
input,
OnInit,
output,
signal,
ViewChild,
viewChild,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, FormGroup, Validators } from "@angular/forms";
@@ -19,6 +19,8 @@ import {
catchError,
of,
combineLatest,
map,
shareReplay,
} from "rxjs";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
@@ -96,7 +98,8 @@ export type UpgradePaymentParams = {
providers: [UpgradePaymentService],
templateUrl: "./upgrade-payment.component.html",
})
export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
export class UpgradePaymentComponent implements OnInit, AfterViewInit {
private readonly INITIAL_TAX_VALUE = 0;
protected readonly selectedPlanId = input.required<PersonalSubscriptionPricingTierId>();
protected readonly account = input.required<Account>();
protected goBack = output<void>();
@@ -104,12 +107,8 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
protected selectedPlan: PlanDetails | null = null;
protected hasEnoughAccountCredit$!: Observable<boolean>;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild(EnterPaymentMethodComponent) paymentComponent!: EnterPaymentMethodComponent;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild(CartSummaryComponent) cartSummaryComponent!: CartSummaryComponent;
readonly paymentComponent = viewChild.required(EnterPaymentMethodComponent);
readonly cartSummaryComponent = viewChild.required(CartSummaryComponent);
protected formGroup = new FormGroup({
organizationName: new FormControl<string>("", [Validators.required]),
@@ -118,12 +117,11 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
});
protected readonly loading = signal(true);
private cartSummaryConfigured = false;
private pricingTiers$!: Observable<PersonalSubscriptionPricingTier[]>;
// Cart Summary data
protected passwordManager!: LineItem;
protected estimatedTax = 0;
protected estimatedTax$!: Observable<number>;
// Display data
protected upgradeToMessage = "";
@@ -165,49 +163,42 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
this.upgradeToMessage = this.i18nService.t(
this.isFamiliesPlan ? "upgradeToFamilies" : "upgradeToPremium",
);
this.estimatedTax = 0;
} else {
this.complete.emit({ status: UpgradePaymentStatus.Closed, organizationId: null });
return;
}
});
this.formGroup.controls.billingAddress.valueChanges
.pipe(
debounceTime(1000),
// Only proceed when form has required values
switchMap(() => this.refreshSalesTax$()),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((tax) => {
this.estimatedTax = tax;
});
// Check if user has enough account credit for the purchase
this.hasEnoughAccountCredit$ = combineLatest([
this.upgradePaymentService.accountCredit$,
this.formGroup.valueChanges.pipe(startWith(this.formGroup.value)),
]).pipe(
switchMap(([credit, formValue]) => {
const selectedPaymentType = formValue.paymentForm?.type;
if (selectedPaymentType !== NonTokenizablePaymentMethods.accountCredit) {
return of(true); // Not using account credit, so this check doesn't apply
}
return credit ? of(credit >= this.cartSummaryComponent.total()) : of(false);
}),
this.estimatedTax$ = this.formGroup.controls.billingAddress.valueChanges.pipe(
startWith(this.formGroup.controls.billingAddress.value),
debounceTime(1000),
// Only proceed when form has required values
switchMap(() => this.refreshSalesTax$()),
);
this.loading.set(false);
}
ngAfterViewChecked(): void {
// Configure cart summary only once when it becomes available
if (this.cartSummaryComponent && !this.cartSummaryConfigured) {
this.cartSummaryComponent.isExpanded.set(false);
this.cartSummaryConfigured = true;
}
ngAfterViewInit(): void {
const cartSummaryComponent = this.cartSummaryComponent();
cartSummaryComponent.isExpanded.set(false);
this.hasEnoughAccountCredit$ = combineLatest([
cartSummaryComponent.total$,
this.upgradePaymentService.accountCredit$,
this.formGroup.controls.paymentForm.valueChanges.pipe(
startWith(this.formGroup.controls.paymentForm.value),
),
]).pipe(
map(([total, credit, currentFormValue]) => {
const selectedPaymentType = currentFormValue?.type;
if (selectedPaymentType !== NonTokenizablePaymentMethods.accountCredit) {
return true; // Not using account credit, so this check doesn't apply
}
return credit ? credit >= total : false;
}),
shareReplay({ bufferSize: 1, refCount: true }), // Cache the latest for two async pipes
);
}
protected get isPremiumPlan(): boolean {
@@ -252,7 +243,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
};
protected isFormValid(): boolean {
return this.formGroup.valid && this.paymentComponent?.validate();
return this.formGroup.valid && this.paymentComponent().validate();
}
private async processUpgrade(): Promise<UpgradePaymentResult> {
@@ -335,17 +326,19 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
return { type: NonTokenizablePaymentMethods.accountCredit };
}
return await this.paymentComponent?.tokenize();
return await this.paymentComponent().tokenize();
}
// Create an observable for tax calculation
private refreshSalesTax$(): Observable<number> {
if (this.formGroup.invalid || !this.selectedPlan) {
return of(0);
return of(this.INITIAL_TAX_VALUE);
}
const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress);
if (!billingAddress.country || !billingAddress.postalCode) {
return of(this.INITIAL_TAX_VALUE);
}
return from(
this.upgradePaymentService.calculateEstimatedTax(this.selectedPlan, billingAddress),
).pipe(
@@ -355,7 +348,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
variant: "error",
message: this.i18nService.t("taxCalculationError"),
});
return of(0); // Return default value on error
return of(this.INITIAL_TAX_VALUE); // Return default value on error
}),
);
}