mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +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:
@@ -54,7 +54,7 @@
|
|||||||
<billing-cart-summary
|
<billing-cart-summary
|
||||||
#cartSummaryComponent
|
#cartSummaryComponent
|
||||||
[passwordManager]="passwordManager"
|
[passwordManager]="passwordManager"
|
||||||
[estimatedTax]="estimatedTax"
|
[estimatedTax]="estimatedTax$ | async"
|
||||||
></billing-cart-summary>
|
></billing-cart-summary>
|
||||||
@if (isFamiliesPlan) {
|
@if (isFamiliesPlan) {
|
||||||
<p bitTypography="helper" class="tw-italic tw-text-muted !tw-mb-0">
|
<p bitTypography="helper" class="tw-italic tw-text-muted !tw-mb-0">
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
AfterViewChecked,
|
AfterViewInit,
|
||||||
Component,
|
Component,
|
||||||
DestroyRef,
|
DestroyRef,
|
||||||
input,
|
input,
|
||||||
OnInit,
|
OnInit,
|
||||||
output,
|
output,
|
||||||
signal,
|
signal,
|
||||||
ViewChild,
|
viewChild,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
catchError,
|
catchError,
|
||||||
of,
|
of,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
|
map,
|
||||||
|
shareReplay,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
@@ -96,7 +98,8 @@ export type UpgradePaymentParams = {
|
|||||||
providers: [UpgradePaymentService],
|
providers: [UpgradePaymentService],
|
||||||
templateUrl: "./upgrade-payment.component.html",
|
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 selectedPlanId = input.required<PersonalSubscriptionPricingTierId>();
|
||||||
protected readonly account = input.required<Account>();
|
protected readonly account = input.required<Account>();
|
||||||
protected goBack = output<void>();
|
protected goBack = output<void>();
|
||||||
@@ -104,12 +107,8 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
|
|||||||
protected selectedPlan: PlanDetails | null = null;
|
protected selectedPlan: PlanDetails | null = null;
|
||||||
protected hasEnoughAccountCredit$!: Observable<boolean>;
|
protected hasEnoughAccountCredit$!: Observable<boolean>;
|
||||||
|
|
||||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
readonly paymentComponent = viewChild.required(EnterPaymentMethodComponent);
|
||||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
readonly cartSummaryComponent = viewChild.required(CartSummaryComponent);
|
||||||
@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;
|
|
||||||
|
|
||||||
protected formGroup = new FormGroup({
|
protected formGroup = new FormGroup({
|
||||||
organizationName: new FormControl<string>("", [Validators.required]),
|
organizationName: new FormControl<string>("", [Validators.required]),
|
||||||
@@ -118,12 +117,11 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
|
|||||||
});
|
});
|
||||||
|
|
||||||
protected readonly loading = signal(true);
|
protected readonly loading = signal(true);
|
||||||
private cartSummaryConfigured = false;
|
|
||||||
private pricingTiers$!: Observable<PersonalSubscriptionPricingTier[]>;
|
private pricingTiers$!: Observable<PersonalSubscriptionPricingTier[]>;
|
||||||
|
|
||||||
// Cart Summary data
|
// Cart Summary data
|
||||||
protected passwordManager!: LineItem;
|
protected passwordManager!: LineItem;
|
||||||
protected estimatedTax = 0;
|
protected estimatedTax$!: Observable<number>;
|
||||||
|
|
||||||
// Display data
|
// Display data
|
||||||
protected upgradeToMessage = "";
|
protected upgradeToMessage = "";
|
||||||
@@ -165,49 +163,42 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
|
|||||||
this.upgradeToMessage = this.i18nService.t(
|
this.upgradeToMessage = this.i18nService.t(
|
||||||
this.isFamiliesPlan ? "upgradeToFamilies" : "upgradeToPremium",
|
this.isFamiliesPlan ? "upgradeToFamilies" : "upgradeToPremium",
|
||||||
);
|
);
|
||||||
|
|
||||||
this.estimatedTax = 0;
|
|
||||||
} else {
|
} else {
|
||||||
this.complete.emit({ status: UpgradePaymentStatus.Closed, organizationId: null });
|
this.complete.emit({ status: UpgradePaymentStatus.Closed, organizationId: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.formGroup.controls.billingAddress.valueChanges
|
this.estimatedTax$ = this.formGroup.controls.billingAddress.valueChanges.pipe(
|
||||||
.pipe(
|
startWith(this.formGroup.controls.billingAddress.value),
|
||||||
debounceTime(1000),
|
debounceTime(1000),
|
||||||
// Only proceed when form has required values
|
// Only proceed when form has required values
|
||||||
switchMap(() => this.refreshSalesTax$()),
|
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.loading.set(false);
|
this.loading.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewChecked(): void {
|
ngAfterViewInit(): void {
|
||||||
// Configure cart summary only once when it becomes available
|
const cartSummaryComponent = this.cartSummaryComponent();
|
||||||
if (this.cartSummaryComponent && !this.cartSummaryConfigured) {
|
cartSummaryComponent.isExpanded.set(false);
|
||||||
this.cartSummaryComponent.isExpanded.set(false);
|
|
||||||
this.cartSummaryConfigured = true;
|
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 {
|
protected get isPremiumPlan(): boolean {
|
||||||
@@ -252,7 +243,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
|
|||||||
};
|
};
|
||||||
|
|
||||||
protected isFormValid(): boolean {
|
protected isFormValid(): boolean {
|
||||||
return this.formGroup.valid && this.paymentComponent?.validate();
|
return this.formGroup.valid && this.paymentComponent().validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processUpgrade(): Promise<UpgradePaymentResult> {
|
private async processUpgrade(): Promise<UpgradePaymentResult> {
|
||||||
@@ -335,17 +326,19 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
|
|||||||
return { type: NonTokenizablePaymentMethods.accountCredit };
|
return { type: NonTokenizablePaymentMethods.accountCredit };
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.paymentComponent?.tokenize();
|
return await this.paymentComponent().tokenize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(0);
|
return of(this.INITIAL_TAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress);
|
const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress);
|
||||||
|
if (!billingAddress.country || !billingAddress.postalCode) {
|
||||||
|
return of(this.INITIAL_TAX_VALUE);
|
||||||
|
}
|
||||||
return from(
|
return from(
|
||||||
this.upgradePaymentService.calculateEstimatedTax(this.selectedPlan, billingAddress),
|
this.upgradePaymentService.calculateEstimatedTax(this.selectedPlan, billingAddress),
|
||||||
).pipe(
|
).pipe(
|
||||||
@@ -355,7 +348,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked {
|
|||||||
variant: "error",
|
variant: "error",
|
||||||
message: this.i18nService.t("taxCalculationError"),
|
message: this.i18nService.t("taxCalculationError"),
|
||||||
});
|
});
|
||||||
return of(0); // Return default value on error
|
return of(this.INITIAL_TAX_VALUE); // Return default value on error
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CurrencyPipe } from "@angular/common";
|
import { CurrencyPipe } from "@angular/common";
|
||||||
import { Component, computed, input, signal } from "@angular/core";
|
import { Component, computed, input, signal } from "@angular/core";
|
||||||
|
import { toObservable } from "@angular/core/rxjs-interop";
|
||||||
|
|
||||||
import { TypographyModule, IconButtonModule } from "@bitwarden/components";
|
import { TypographyModule, IconButtonModule } from "@bitwarden/components";
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
@@ -71,6 +72,11 @@ export class CartSummaryComponent {
|
|||||||
*/
|
*/
|
||||||
readonly total = computed<number>(() => this.getTotalCost());
|
readonly total = computed<number>(() => this.getTotalCost());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable of computed total value
|
||||||
|
*/
|
||||||
|
readonly total$ = toObservable(this.total);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the expanded/collapsed state of the cart items
|
* Toggles the expanded/collapsed state of the cart items
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user