mirror of
https://github.com/bitwarden/browser
synced 2026-03-01 11:01:17 +00:00
[PM-29602] Build Upgrade Dialogs (#18539)
* BREAKING CHANGE: rename tax-client and add proration endpoint update * fix(billing)!: rename tax-client in components * feat(billing): Add upgrade endpoint * fix(billing): update preview client error * fix(billing): add billing address to clients * feat(billing): Update messages for changes * feat(biilling): Update unified upgrade dialog logic * feat(billing): add new premium org card * feat(billing): add premium org component * fix(billing): Update account billing client and remove redundant status * fix(billing): unified upgrade dialog add feature flag and tests * fix(billing): update unified upgrade logic * fix(billing): update tests and logic update update fix * fix(billing): add required messages message * fix(billing): update unified dialog logic and re-add comments * feat(billing): improves premium org upgrade dialog Adds a close button to the premium organization upgrade dialog. Updates the success toast message after upgrading to teams. Hides the formatted amount for credit discounts. Sets the change detection strategy to OnPush for improved performance. * fix(billing): prevents multiple upgrade dialogs from opening Adds a check to prevent multiple upgrade dialogs from opening simultaneously. Ensures correct redirection to the organization vault after upgrading to Teams or Enterprise. * Feat(tooltip): Add `showTooltipOnFocus` input to TooltipDirective * Fix(billing): Disable tooltip on focus for various billing buttons * Refactor(billing): Standardize subscription cadence display * Refactor(billing): Update InvoicePreview with prorated amount details * Refactor(billing): Enhance Premium Org Upgrade Payment logic * Feat(billing): Add SubscriptionCadence import to account billing client * refactor(i18n): Rename 'premiumMembershipDiscount' to 'premiumSubscriptionCredit' * fix(billing): Ensure encrypted org key is present during upgrade * refactor(billing): revert PremiumUpgradeDialog focus management * refactor(billing): Clean up subscription details and type definitions * feat(billing): Add dedicated Premium to Organization upgrade dialog * refactor(billing): Return organization ID from PremiumOrgUpgradeService * refactor(billing): Remove premium to org upgrade logic from UnifiedUpgradeDialog * feat(billing): Integrate PremiumOrgUpgradeDialog into account subscription * Refactor: Make `openUpgradeDialog` return `void` * Remove obsolete `planSelectionStepTitleOverride` tests * Feature: Add 'Back' status to UpgradePaymentStatus * Test: Mock `OrganizationService` in `PremiumOrgUpgradePaymentComponent` tests * Chore: Remove redundant comment in unified upgrade dialog HTML * refactor(billing): Remove obsolete unified upgrade change * refactor(billing): remove unused ApiService and DestroyRef * feat(billing): add pre-condition checks for premium org upgrade dialog * refactor(billing): clean up unused dialog data and HTML comment * refactor(billing): rename premium org upgrade dialog flag * feat(billing): close premium org upgrade dialog if feature is disabled * feat(payment): add hideHeader input to DisplayPaymentMethodComponent * refactor(billing): update premium org upgrade payment to display existing payment method * test(billing): update premium org upgrade payment component tests * docs(billing): refine JSDoc for PremiumOrgUpgradeDialogParams * Revert "Feat(tooltip): Add `showTooltipOnFocus` input to TooltipDirective" This reverts commit02f62bc0fd. * Revert "Fix(billing): Disable tooltip on focus for various billing buttons" This reverts commit91f7747df7. * fix(billing): Ensure early exit for closed premium org upgrade payment * refactor: rename PremiumOrgUpgradeComponent to PremiumOrgUpgradePlanSelectionComponent * feat(i18n): add payment method update error translation key * feat(billing): introduce DisplayPaymentMethodInlineComponent * feat(billing): integrate inline payment method in PremiumOrgUpgradePayment * feat(pricing): allow hiding pricing term in cart summary * refactor(billing): optimize invoice preview and update cart configuration * refactor(billing): migrate AccountSubscriptionComponent state to signals * chore(html): improve form field layout and accessibility * feat(pricing): add `hidePricingTerm` input and basic header logic * feat(pricing): apply `hidePricingTerm` to cart item breakdowns * docs(pricing): update cart summary documentation for `hideBreakdown` and `hidePricingTerm` * test(pricing): add tests for `hidePricingTerm` and refine term display selector * refactor(pricing): update cart summary test selectors for robustness * docs: reformat `hideBreakdown` description in `CartSummaryComponent` MDX * refactor: remoe additonal DisplayPaymentMethodInlineComponent in imports * Revert "feat(i18n): add payment method update error translation key" This reverts commitb4aeb74e1a. * feat(i18n): Add payment method update error message * refactor(pricing): move CartSummaryComponent hidePricingTerm to input * docs(pricing): update CartSummaryComponent `hidePricingTerm` usage in MDX * test(pricing): update CartSummaryComponent `hidePricingTerm` tests and stories * chore(pricing): add spacing in CartSummaryComponent spec assertion * refactor(billing): Use ngOnInit for dialog initialization logic * refactor(billing): Migrate hidePricingTerm from Cart type to direct input * Refactor: Update payment method action buttons to use `bitLink` * feat(billing): add hidePricingTerm input to MockCartSummaryComponent
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ToastService, IconComponent } from "@bitwarden/components";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { BitwardenSubscriber } from "../../types";
|
||||
import { getCardBrandIcon, MaskedPaymentMethod, TokenizablePaymentMethods } from "../types";
|
||||
|
||||
import { EnterPaymentMethodComponent } from "./enter-payment-method.component";
|
||||
|
||||
/**
|
||||
* Component for inline editing of payment methods.
|
||||
* Displays a form to update payment method details directly within the parent view.
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-display-payment-method-inline",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<bit-section>
|
||||
@if (!isChangingPayment()) {
|
||||
<h5 bitTypography="h5">{{ "paymentMethod" | i18n }}</h5>
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
@if (paymentMethod(); as pm) {
|
||||
@switch (pm.type) {
|
||||
@case ("bankAccount") {
|
||||
@if (pm.hostedVerificationUrl) {
|
||||
<p>
|
||||
{{ "verifyBankAccountWithStripe" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
[attr.href]="pm.hostedVerificationUrl"
|
||||
>{{ "verifyNow" | i18n }}</a
|
||||
>
|
||||
</p>
|
||||
}
|
||||
|
||||
<p>
|
||||
<bit-icon name="bwi-billing"></bit-icon>
|
||||
{{ pm.bankName }}, *{{ pm.last4 }}
|
||||
@if (pm.hostedVerificationUrl) {
|
||||
<span>- {{ "unverified" | i18n }}</span>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
@case ("card") {
|
||||
<p class="tw-flex tw-gap-2">
|
||||
@if (cardBrandIcon(); as icon) {
|
||||
<i class="bwi bwi-fw credit-card-icon {{ icon }}"></i>
|
||||
} @else {
|
||||
<bit-icon name="bwi-credit-card"></bit-icon>
|
||||
}
|
||||
{{ pm.brand | titlecase }}, *{{ pm.last4 }},
|
||||
{{ pm.expiration }}
|
||||
</p>
|
||||
}
|
||||
@case ("payPal") {
|
||||
<p>
|
||||
<bit-icon name="bwi-paypal" class="tw-text-primary-600"></bit-icon>
|
||||
{{ pm.email }}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<p bitTypography="body1">{{ "noPaymentMethod" | i18n }}</p>
|
||||
}
|
||||
@let key = paymentMethod() ? "changePaymentMethod" : "addPaymentMethod";
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
class="tw-cursor-pointer tw-mb-4"
|
||||
(click)="changePaymentMethod()"
|
||||
>
|
||||
{{ key | i18n }}</a
|
||||
>
|
||||
</div>
|
||||
} @else {
|
||||
<app-enter-payment-method
|
||||
#enterPaymentMethodComponent
|
||||
[includeBillingAddress]="true"
|
||||
[group]="formGroup"
|
||||
[showBankAccount]="true"
|
||||
[showAccountCredit]="false"
|
||||
>
|
||||
</app-enter-payment-method>
|
||||
<div class="tw-mt-4 tw-flex tw-gap-2">
|
||||
<button
|
||||
bitLink
|
||||
linkType="default"
|
||||
type="button"
|
||||
(click)="submit()"
|
||||
[disabled]="formGroup.invalid"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitLink linkType="subtle" type="button" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</bit-section>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [SharedModule, EnterPaymentMethodComponent, IconComponent],
|
||||
providers: [SubscriberBillingClient],
|
||||
})
|
||||
export class DisplayPaymentMethodInlineComponent {
|
||||
readonly subscriber = input.required<BitwardenSubscriber>();
|
||||
readonly paymentMethod = input.required<MaskedPaymentMethod | null>();
|
||||
readonly updated = output<MaskedPaymentMethod>();
|
||||
readonly changingStateChanged = output<boolean>();
|
||||
|
||||
protected formGroup = EnterPaymentMethodComponent.getFormGroup();
|
||||
|
||||
private readonly enterPaymentMethodComponent = viewChild<EnterPaymentMethodComponent>(
|
||||
EnterPaymentMethodComponent,
|
||||
);
|
||||
|
||||
protected readonly isChangingPayment = signal(false);
|
||||
protected readonly cardBrandIcon = computed(() => getCardBrandIcon(this.paymentMethod()));
|
||||
|
||||
private readonly billingClient = inject(SubscriberBillingClient);
|
||||
private readonly i18nService = inject(I18nService);
|
||||
private readonly toastService = inject(ToastService);
|
||||
private readonly logService = inject(LogService);
|
||||
|
||||
/**
|
||||
* Initiates the payment method change process by displaying the inline form.
|
||||
*/
|
||||
protected changePaymentMethod = async (): Promise<void> => {
|
||||
this.isChangingPayment.set(true);
|
||||
this.changingStateChanged.emit(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Submits the payment method update form.
|
||||
* Validates the form, tokenizes the payment method, and sends the update request.
|
||||
*/
|
||||
protected submit = async (): Promise<void> => {
|
||||
try {
|
||||
if (!this.formGroup.valid) {
|
||||
this.formGroup.markAllAsTouched();
|
||||
throw new Error("Form is invalid");
|
||||
}
|
||||
|
||||
const component = this.enterPaymentMethodComponent();
|
||||
if (!component) {
|
||||
throw new Error("Payment method component not found");
|
||||
}
|
||||
|
||||
const paymentMethod = await component.tokenize();
|
||||
if (!paymentMethod) {
|
||||
throw new Error("Failed to tokenize payment method");
|
||||
}
|
||||
|
||||
const billingAddress =
|
||||
this.formGroup.value.type !== TokenizablePaymentMethods.payPal
|
||||
? this.formGroup.controls.billingAddress.getRawValue()
|
||||
: null;
|
||||
|
||||
await this.handlePaymentMethodUpdate(paymentMethod, billingAddress);
|
||||
} catch (error) {
|
||||
this.logService.error("Error submitting payment method update:", error);
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("paymentMethodUpdateError"),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the payment method update API call and result processing.
|
||||
*/
|
||||
private async handlePaymentMethodUpdate(paymentMethod: any, billingAddress: any): Promise<void> {
|
||||
const result = await this.billingClient.updatePaymentMethod(
|
||||
this.subscriber(),
|
||||
paymentMethod,
|
||||
billingAddress,
|
||||
);
|
||||
|
||||
switch (result.type) {
|
||||
case "success": {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("paymentMethodUpdated"),
|
||||
});
|
||||
this.updated.emit(result.value);
|
||||
this.isChangingPayment.set(false);
|
||||
this.changingStateChanged.emit(false);
|
||||
this.formGroup.reset();
|
||||
break;
|
||||
}
|
||||
case "error": {
|
||||
this.logService.error("Error submitting payment method update:", result);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("paymentMethodUpdateError"),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the inline editing and resets the form.
|
||||
*/
|
||||
protected cancel = (): void => {
|
||||
this.formGroup.reset();
|
||||
this.changingStateChanged.emit(false);
|
||||
this.isChangingPayment.set(false);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user