diff --git a/apps/web/src/app/billing/payment/components/display-payment-method-inline.component.ts b/apps/web/src/app/billing/payment/components/display-payment-method-inline.component.ts
new file mode 100644
index 00000000000..aa6d15401f4
--- /dev/null
+++ b/apps/web/src/app/billing/payment/components/display-payment-method-inline.component.ts
@@ -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: `
+
+ @if (!isChangingPayment()) {
+ {{ "paymentMethod" | i18n }}
+
+ @if (paymentMethod(); as pm) {
+ @switch (pm.type) {
+ @case ("bankAccount") {
+ @if (pm.hostedVerificationUrl) {
+
+ {{ "verifyBankAccountWithStripe" | i18n }}
+ {{ "verifyNow" | i18n }}
+
+ }
+
+
+
+ {{ pm.bankName }}, *{{ pm.last4 }}
+ @if (pm.hostedVerificationUrl) {
+ - {{ "unverified" | i18n }}
+ }
+
+ }
+ @case ("card") {
+
+ @if (cardBrandIcon(); as icon) {
+
+ } @else {
+
+ }
+ {{ pm.brand | titlecase }}, *{{ pm.last4 }},
+ {{ pm.expiration }}
+
+ }
+ @case ("payPal") {
+
+
+ {{ pm.email }}
+
+ }
+ }
+ } @else {
+
{{ "noPaymentMethod" | i18n }}
+ }
+ @let key = paymentMethod() ? "changePaymentMethod" : "addPaymentMethod";
+
+ {{ key | i18n }}
+
+ } @else {
+
+
+
+
+
+
+ }
+
+ `,
+ standalone: true,
+ imports: [SharedModule, EnterPaymentMethodComponent, IconComponent],
+ providers: [SubscriberBillingClient],
+})
+export class DisplayPaymentMethodInlineComponent {
+ readonly subscriber = input.required
();
+ readonly paymentMethod = input.required();
+ readonly updated = output();
+ readonly changingStateChanged = output();
+
+ protected formGroup = EnterPaymentMethodComponent.getFormGroup();
+
+ private readonly enterPaymentMethodComponent = viewChild(
+ 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 => {
+ 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 => {
+ 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 {
+ 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);
+ };
+}
diff --git a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts
index c5ffa4268ed..f8e244b3b7a 100644
--- a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts
+++ b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts
@@ -1,4 +1,4 @@
-import { Component, EventEmitter, Input, Output } from "@angular/core";
+import { Component, EventEmitter, input, Input, Output } from "@angular/core";
import { lastValueFrom } from "rxjs";
import { DialogService } from "@bitwarden/components";
@@ -15,7 +15,9 @@ import { ChangePaymentMethodDialogComponent } from "./change-payment-method-dial
selector: "app-display-payment-method",
template: `
- {{ "paymentMethod" | i18n }}
+ @if (!hideHeader()) {
+ {{ "paymentMethod" | i18n }}
+ }
@if (paymentMethod) {
@switch (paymentMethod.type) {
@case ("bankAccount") {
@@ -81,6 +83,7 @@ export class DisplayPaymentMethodComponent {
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() updated = new EventEmitter();
+ protected readonly hideHeader = input(false);
constructor(private dialogService: DialogService) {}
diff --git a/apps/web/src/app/billing/payment/components/index.ts b/apps/web/src/app/billing/payment/components/index.ts
index 5e10fa4763b..d570d3265e2 100644
--- a/apps/web/src/app/billing/payment/components/index.ts
+++ b/apps/web/src/app/billing/payment/components/index.ts
@@ -2,6 +2,7 @@ export * from "./add-account-credit-dialog.component";
export * from "./change-payment-method-dialog.component";
export * from "./display-account-credit.component";
export * from "./display-billing-address.component";
+export * from "./display-payment-method-inline.component";
export * from "./display-payment-method.component";
export * from "./edit-billing-address-dialog.component";
export * from "./enter-billing-address.component";
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 042eec107d4..fc2f463d9e6 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -12794,5 +12794,54 @@
},
"perUser": {
"message": "per user"
+ },
+ "upgradeToTeams": {
+ "message": "Upgrade to Teams"
+ },
+ "upgradeToEnterprise": {
+ "message": "Upgrade to Enterprise"
+ },
+ "upgradeShareEvenMore": {
+ "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise"
+ },
+ "organizationUpgradeTaxInformationMessage": {
+ "message": "Prices exclude tax and are billed annually."
+ },
+ "invoicePreviewErrorMessage": {
+ "message": "Encountered an error while generating the invoice preview."
+ },
+ "planProratedMembershipInMonths": {
+ "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)",
+ "placeholders": {
+ "plan": {
+ "content": "$1",
+ "example": "Families"
+ },
+ "numofmonths": {
+ "content": "$2",
+ "example": "6 Months"
+ }
+ }
+ },
+ "premiumSubscriptionCredit": {
+ "message": "Premium subscription credit"
+ },
+ "enterpriseMembership": {
+ "message": "Enterprise membership"
+ },
+ "teamsMembership": {
+ "message": "Teams membership"
+ },
+ "plansUpdated": {
+ "message": "You've upgraded to $PLAN$!",
+ "placeholders": {
+ "plan": {
+ "content": "$1",
+ "example": "Families"
+ }
+ }
+ },
+ "paymentMethodUpdateError": {
+ "message": "There was an error updating your payment method."
}
}
diff --git a/libs/angular/src/billing/types/subscription-pricing-card-details.ts b/libs/angular/src/billing/types/subscription-pricing-card-details.ts
index 5f37f91c4f0..8430f6d35b5 100644
--- a/libs/angular/src/billing/types/subscription-pricing-card-details.ts
+++ b/libs/angular/src/billing/types/subscription-pricing-card-details.ts
@@ -1,10 +1,13 @@
-import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier";
import { BitwardenIcon, ButtonType } from "@bitwarden/components";
export type SubscriptionPricingCardDetails = {
title: string;
tagline: string;
- price?: { amount: number; cadence: SubscriptionCadence };
+ price?: {
+ amount: number;
+ cadence: "month" | "monthly" | "year" | "annually";
+ showPerUser?: boolean;
+ };
button: {
text: string;
type: ButtonType;
diff --git a/libs/pricing/src/components/cart-summary/cart-summary.component.html b/libs/pricing/src/components/cart-summary/cart-summary.component.html
index d3a0ad25e6c..4e3e75baed9 100644
--- a/libs/pricing/src/components/cart-summary/cart-summary.component.html
+++ b/libs/pricing/src/components/cart-summary/cart-summary.component.html
@@ -1,6 +1,6 @@
@let cart = this.cart();
@let term = this.term();
-
+@let hideTerm = this.hidePricingTerm();
@@ -16,7 +16,9 @@
{{ "total" | i18n }}: {{ total() | currency: "USD" : "symbol" }} USD
- / {{ term }}
+ @if (!hideTerm) {
+ / {{ term | i18n }}
+ }
}
@@ -86,8 +90,11 @@
)
}}
@if (!additionalStorage.hideBreakdown) {
- x {{ additionalStorage.cost | currency: "USD" : "symbol" }} /
- {{ term }}
+ x {{ additionalStorage.cost | currency: "USD" : "symbol" }}
+ @if (!hideTerm) {
+ /
+ {{ term }}
+ }
}
@@ -125,7 +132,10 @@
@if (!secretsManagerSeats.hideBreakdown) {
x
{{ secretsManagerSeats.cost | currency: "USD" : "symbol" }}
- / {{ term }}
+ @if (!hideTerm) {
+ /
+ {{ term }}
+ }
}