From 38465c059c297f8d52dffdf73d6990cfd4b10ce6 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Tue, 3 Feb 2026 12:47:58 -0500 Subject: [PATCH] [PM-29602] Update Cart Summary for Upgrade Flow (#18605) * feat(billing): update cart-summary logic Add functionality to hide breakdown and allow translation params * tests(cart-summary): update tests and stories * feat(pricing): Add quantity support to discount labels * feat(pricing): discount quantity story * Revert "feat(pricing): discount quantity story" This reverts commit 2c00891f1fbb654954d58483d4dfdb720b5d9348. * Revert "feat(pricing): Add quantity support to discount labels" This reverts commit 8350fdd90f0de7f0d7675cd1be5a22cba34ed3fe. * fix(cart-summary): Adjust discount text styling * feat(pricing): adds support for hidden discount amounts Allows hiding the formatted amount for discounts in the cart summary. This is useful for scenarios where the discount amount is displayed elsewhere or is not relevant to the user. Updates the storybook to include a story demonstrating this feature. * feat(pricing): conditionally format currency amounts to show or hide decimals * Revert "feat(pricing): adds support for hidden discount amounts" This reverts commit 076724276c05a4463f05aa50fc119f5058dc2324. * Revert "fix(cart-summary): Adjust discount text styling" This reverts commit d02c12fc2a11b3e050bf59ba85525d8f066bd446. * Revert "discount translation" * feat(pricing): add credit type to cart summary * feat(pricing-card): Add i18n and icon component infrastructure * feat(pricing-card): Apply i18n pipe to pricing card template * refactor(pricing-card): Replace `` tags with `` in template * test(pricing-card): Update tests for i18n and icon component changes * docs(pricing-card): Enhance Storybook and documentation for new features * feat(pricing-card): Adds "per user" translation key * refactor(pricing-card): use property binding for bit-icon name * docs(pricing-card): expand price cadence options in MDX * fix(icon): update exports for icon types * feat(billing): Use strongly typed BitwardenIcon for pricing card buttons * refactor(pricing): Remove unused I18nService from PricingCardComponent * fix(pricing): Improve pricing card button icon template null-safety * fix(pricing-card): format update Clarifies the description of the `price` property within the PricingCard component documentation. No functional code changes are included. * refactor: Update discount label typography in cart summary * refactor(stories): Rename account credit translation key to premium subscription credit * feat(pricing-card): update spacing for card without button --- apps/web/src/locales/en/messages.json | 3 + .../subscription-pricing-card-details.ts | 8 +- libs/components/src/index.ts | 1 + .../cart-summary/cart-summary.component.html | 122 +++++++--- .../cart-summary/cart-summary.component.mdx | 78 ++++++- .../cart-summary.component.spec.ts | 212 ++++++++++++++++++ .../cart-summary.component.stories.ts | 91 ++++++++ .../cart-summary/cart-summary.component.ts | 23 +- .../pricing-card/pricing-card.component.html | 32 +-- .../pricing-card/pricing-card.component.mdx | 54 ++++- .../pricing-card.component.spec.ts | 26 ++- .../pricing-card.component.stories.ts | 43 +++- .../pricing-card/pricing-card.component.ts | 18 +- libs/pricing/src/types/cart.ts | 5 + libs/pricing/src/types/credit.ts | 5 + 15 files changed, 662 insertions(+), 59 deletions(-) create mode 100644 libs/pricing/src/types/credit.ts diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index a894b328d56..04566a666d4 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -12782,5 +12782,8 @@ }, "invalidSendPassword": { "message": "Invalid Send password" + }, + "perUser": { + "message": "per user" } } 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 9000b10a729..5f37f91c4f0 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,14 @@ import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier"; -import { ButtonType } from "@bitwarden/components"; +import { BitwardenIcon, ButtonType } from "@bitwarden/components"; export type SubscriptionPricingCardDetails = { title: string; tagline: string; price?: { amount: number; cadence: SubscriptionCadence }; - button: { text: string; type: ButtonType; icon?: { type: string; position: "before" | "after" } }; + button: { + text: string; + type: ButtonType; + icon?: { type: BitwardenIcon; position: "before" | "after" }; + }; features: string[]; }; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 7395b87b2ab..d92e0770e49 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,4 +1,5 @@ export { ButtonType, ButtonLikeAbstraction } from "./shared/button-like.abstraction"; +export { BitwardenIcon } from "./shared/icon"; export * from "./a11y"; export * from "./anon-layout"; export * from "./async-actions"; 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 e916de3995d..d3a0ad25e6c 100644 --- a/libs/pricing/src/components/cart-summary/cart-summary.component.html +++ b/libs/pricing/src/components/cart-summary/cart-summary.component.html @@ -16,7 +16,7 @@ {{ "total" | i18n }}: {{ total() | currency: "USD" : "symbol" }} USD   - / {{ term }} + / {{ term }} } - } - + + }
@@ -67,10 +69,12 @@
    @for (feature of featureList; track feature) {
  • - + > + {{ feature }} diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.mdx b/libs/pricing/src/components/pricing-card/pricing-card.component.mdx index 905b8e6981f..1cbac94d8ee 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.mdx +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.mdx @@ -39,7 +39,7 @@ import { PricingCardComponent } from "@bitwarden/pricing"; | Input | Type | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tagline` | `string` | **Required.** Descriptive text below title (max 2 lines) | -| `price` | `{ amount: number; cadence: "monthly" \| "annually"; showPerUser?: boolean }` | **Optional.** Price information. If omitted, no price is shown | +| `price` | `{ amount: number; cadence: "month" \| "monthly" \| "year" \| "annually"; showPerUser?: boolean }` | **Optional.** Price information. If omitted, no price is shown | | `button` | `{ type: ButtonType; text: string; disabled?: boolean; icon?: { type: string; position: "before" \| "after" } }` | **Optional.** Button configuration with optional icon. If omitted, no button is shown. Icon uses `bwi-*` classes, position defaults to "after" | | `features` | `string[]` | **Optional.** List of features with checkmarks | | `activeBadge` | `{ text: string; variant?: BadgeVariant }` | **Optional.** Active plan badge using proper Badge component, positioned on the same line as title, aligned to the right. If omitted, no badge is shown | @@ -182,6 +182,58 @@ For coming soon or unavailable plans: ``` +### With Button Icons + +Add icons to buttons for enhanced visual communication: + + + +```html + + + + + + + +``` + +### Active Plan Badge + +Show which plan is currently active: + + + +```html + + +``` + ### Pricing Grid Layout Multiple cards displayed together: diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts b/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts index 669b54c5b57..fc8a9541952 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts @@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { BadgeVariant, ButtonType, SvgModule, TypographyModule } from "@bitwarden/components"; import { PricingCardComponent } from "@bitwarden/pricing"; @@ -69,6 +70,29 @@ describe("PricingCardComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [PricingCardComponent, TestHostComponent, SvgModule, TypographyModule, CommonModule], + providers: [ + { + provide: I18nService, + useValue: { + t: (key: string) => { + switch (key) { + case "month": + return "month"; + case "monthly": + return "monthly"; + case "year": + return "year"; + case "annually": + return "annually"; + case "perUser": + return "per user"; + default: + return key; + } + }, + }, + }, + ], }).compileComponents(); // For signal inputs, we need to set required inputs through the host component @@ -151,7 +175,7 @@ describe("PricingCardComponent", () => { it("should display bwi-check icons for features", () => { hostFixture.detectChanges(); const compiled = hostFixture.nativeElement; - const icons = compiled.querySelectorAll("i.bwi-check"); + const icons = compiled.querySelectorAll("bit-icon[name='bwi-check']"); expect(icons.length).toBe(3); // One for each feature }); diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.stories.ts b/libs/pricing/src/components/pricing-card/pricing-card.component.stories.ts index 832345de357..63946cbf19a 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.stories.ts +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.stories.ts @@ -1,15 +1,42 @@ -import { Meta, StoryObj } from "@storybook/angular"; +import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; -import { TypographyModule } from "@bitwarden/components"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SvgModule, TypographyModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { PricingCardComponent } from "./pricing-card.component"; export default { title: "Billing/Pricing Card", component: PricingCardComponent, - moduleMetadata: { - imports: [TypographyModule], - }, + decorators: [ + moduleMetadata({ + imports: [PricingCardComponent, SvgModule, TypographyModule, I18nPipe], + providers: [ + { + provide: I18nService, + useValue: { + t: (key: string) => { + switch (key) { + case "month": + return "month"; + case "monthly": + return "monthly"; + case "year": + return "year"; + case "annually": + return "annually"; + case "perUser": + return "per user"; + default: + return key; + } + }, + }, + }, + ], + }), + ], args: { tagline: "Everything you need for secure password management across all your devices", }, @@ -83,7 +110,7 @@ export const WithoutFeatures: Story = { }), args: { tagline: "Advanced security and management for your organization", - price: { amount: 3, cadence: "monthly" }, + price: { amount: 3, cadence: "month" }, button: { text: "Contact Sales", type: "primary" }, }, }; @@ -150,7 +177,7 @@ export const LongTagline: Story = { args: { tagline: "Comprehensive password management solution for teams and organizations that need advanced security features, detailed reporting, and enterprise-grade administration tools that scale with your business", - price: { amount: 5, cadence: "monthly", showPerUser: true }, + price: { amount: 5, cadence: "month", showPerUser: true }, button: { text: "Start Business Trial", type: "primary" }, features: [ "Everything in Premium", @@ -274,7 +301,7 @@ export const WithoutButton: Story = { }), args: { tagline: "This plan will be available soon with exciting new features", - price: { amount: 15, cadence: "monthly" }, + price: { amount: 15, cadence: "month" }, features: ["Advanced security features", "Enhanced collaboration tools", "Premium support"], }, }; diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.ts b/libs/pricing/src/components/pricing-card/pricing-card.component.ts index 4b9241fc9dd..23eda0fa99b 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.ts +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.ts @@ -4,12 +4,15 @@ import { ChangeDetectionStrategy, Component, input, output } from "@angular/core import { BadgeModule, BadgeVariant, + BitwardenIcon, ButtonModule, ButtonType, CardComponent, + IconModule, SvgModule, TypographyModule, } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; /** * A reusable UI-only component that displays pricing information in a card format. @@ -20,20 +23,29 @@ import { selector: "billing-pricing-card", templateUrl: "./pricing-card.component.html", changeDetection: ChangeDetectionStrategy.OnPush, - imports: [BadgeModule, ButtonModule, SvgModule, TypographyModule, CurrencyPipe, CardComponent], + imports: [ + BadgeModule, + ButtonModule, + SvgModule, + IconModule, + TypographyModule, + CurrencyPipe, + CardComponent, + I18nPipe, + ], }) export class PricingCardComponent { readonly tagline = input.required(); readonly price = input<{ amount: number; - cadence: "monthly" | "annually"; + cadence: "month" | "monthly" | "year" | "annually"; showPerUser?: boolean; }>(); readonly button = input<{ type: ButtonType; text: string; disabled?: boolean; - icon?: { type: string; position: "before" | "after" }; + icon?: { type: BitwardenIcon; position: "before" | "after" }; }>(); readonly features = input(); readonly activeBadge = input<{ text: string; variant?: BadgeVariant }>(); diff --git a/libs/pricing/src/types/cart.ts b/libs/pricing/src/types/cart.ts index ed5108edee8..aeec6b269af 100644 --- a/libs/pricing/src/types/cart.ts +++ b/libs/pricing/src/types/cart.ts @@ -1,10 +1,14 @@ import { Discount } from "@bitwarden/pricing"; +import { Credit } from "./credit"; + export type CartItem = { translationKey: string; + translationParams?: Array; quantity: number; cost: number; discount?: Discount; + hideBreakdown?: boolean; }; export type Cart = { @@ -18,5 +22,6 @@ export type Cart = { }; cadence: "annually" | "monthly"; discount?: Discount; + credit?: Credit; estimatedTax: number; }; diff --git a/libs/pricing/src/types/credit.ts b/libs/pricing/src/types/credit.ts new file mode 100644 index 00000000000..bb7e42bcb62 --- /dev/null +++ b/libs/pricing/src/types/credit.ts @@ -0,0 +1,5 @@ +export type Credit = { + translationKey: string; + translationParams?: Array; + value: number; +};