1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-20 19:34:03 +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 commit 02f62bc0fd.

* Revert "Fix(billing): Disable tooltip on focus for various billing buttons"

This reverts commit 91f7747df7.

* 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 commit b4aeb74e1a.

* 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:
Stephon Brown
2026-02-09 15:09:37 -05:00
committed by GitHub
parent f22736bb64
commit 37eeffd03a
26 changed files with 3240 additions and 40 deletions

View File

@@ -1,6 +1,6 @@
@let cart = this.cart();
@let term = this.term();
@let hideTerm = this.hidePricingTerm();
<div class="tw-size-full">
<div class="tw-flex tw-items-center tw-pb-2">
<div class="tw-flex tw-items-center">
@@ -16,7 +16,9 @@
{{ "total" | i18n }}: {{ total() | currency: "USD" : "symbol" }} USD
</h2>
<span bitTypography="h3">&nbsp;</span>
<span bitTypography="body1" class="tw-text-main tw-font-normal">/ {{ term }}</span>
@if (!hideTerm) {
<span bitTypography="body1" class="tw-text-muted tw-ms-2"> / {{ term | i18n }} </span>
}
}
</div>
<button
@@ -58,8 +60,10 @@
@if (!passwordManagerSeats.hideBreakdown) {
x
{{ passwordManagerSeats.cost | currency: "USD" : "symbol" }}
/
{{ term }}
@if (!hideTerm) {
/
{{ term }}
}
}
</div>
</div>
@@ -86,8 +90,11 @@
)
}}
@if (!additionalStorage.hideBreakdown) {
x {{ additionalStorage.cost | currency: "USD" : "symbol" }} /
{{ term }}
x {{ additionalStorage.cost | currency: "USD" : "symbol" }}
@if (!hideTerm) {
/
{{ term }}
}
}
</div>
</div>
@@ -125,7 +132,10 @@
@if (!secretsManagerSeats.hideBreakdown) {
x
{{ secretsManagerSeats.cost | currency: "USD" : "symbol" }}
/ {{ term }}
@if (!hideTerm) {
/
{{ term }}
}
}
</div>
<div
@@ -152,8 +162,10 @@
@if (!additionalServiceAccounts.hideBreakdown) {
x
{{ additionalServiceAccounts.cost | currency: "USD" : "symbol" }}
/
{{ term }}
@if (!hideTerm) {
/
{{ term }}
}
}
</div>
<div
@@ -219,7 +231,10 @@
<div id="total-section" class="tw-flex tw-justify-between tw-items-center tw-pt-2">
<h3 bitTypography="h5" class="tw-text-muted tw-font-semibold">{{ "total" | i18n }}</h3>
<div bitTypography="body1" class="tw-text-muted" data-testid="final-total">
{{ total() | currency: "USD" : "symbol" }} / {{ term | i18n }}
{{ total() | currency: "USD" : "symbol" }}
@if (!hidePricingTerm()) {
/ {{ term | i18n }}
}
</div>
</div>
</div>

View File

@@ -29,6 +29,8 @@ behavior across Bitwarden applications.
- [With Percent Discount](#with-percent-discount)
- [With Amount Discount](#with-amount-discount)
- [With Discount and Credit](#with-discount-and-credit)
- [Hidden Cost Breakdown](#hidden-cost-breakdown)
- [Hidden Pricing Term](#hidden-pricing-term)
- [Custom Header Template](#custom-header-template)
- [Premium Plan](#premium-plan)
- [Families Plan](#families-plan)
@@ -53,10 +55,16 @@ import { CartSummaryComponent, Cart } from "@bitwarden/pricing";
### Inputs
| Input | Type | Description |
| -------- | ------------------------ | ------------------------------------------------------------------------------- |
| `cart` | `Cart` | **Required.** The cart data containing all products, discount, tax, and cadence |
| `header` | `TemplateRef<{ total }>` | **Optional.** Custom header template to replace the default header |
| Input | Type | Description |
| ----------------- | ------------------------ | ------------------------------------------------------------------------------------------- |
| `cart` | `Cart` | **Required.** The cart data containing all products, discount, tax, and cadence |
| `header` | `TemplateRef<{ total }>` | **Optional.** Custom header template to replace the default header |
| `hidePricingTerm` | `boolean` | **Optional.** When true, hides the billing term (e.g., "/ month", "/ year") from the header |
**Note:** Individual `CartItem` objects in the cart can include:
- `hideBreakdown` (boolean): Hides the cost breakdown (quantity × unit price) for that specific line
item
### Events
@@ -73,6 +81,7 @@ export type CartItem = {
quantity: number; // Number of items
cost: number; // Cost per item
discount?: Discount; // Optional item-level discount
hideBreakdown?: boolean; // Optional: hide cost breakdown (quantity × unit price)
};
export type Cart = {
@@ -468,6 +477,74 @@ Show cart with both discount and credit applied:
</billing-cart-summary>
```
### Hidden Cost Breakdown
Show cart with hidden cost breakdowns (hides quantity × unit price for line items):
<Canvas of={CartSummaryStories.WithHiddenBreakdown} />
```html
<billing-cart-summary
[cart]="{
passwordManager: {
seats: {
quantity: 5,
translationKey: 'members',
cost: 50.00,
hideBreakdown: true
},
additionalStorage: {
quantity: 2,
translationKey: 'additionalStorageGB',
cost: 10.00,
hideBreakdown: true
}
},
secretsManager: {
seats: {
quantity: 3,
translationKey: 'members',
cost: 30.00,
hideBreakdown: true
},
additionalServiceAccounts: {
quantity: 2,
translationKey: 'additionalServiceAccountsV2',
cost: 6.00,
hideBreakdown: true
}
},
cadence: 'monthly',
estimatedTax: 19.2
}"
>
</billing-cart-summary>
```
### Hidden Pricing Term
Show cart with hidden pricing term (hides "/ month" or "/ year" from header):
<Canvas of={CartSummaryStories.HiddenPricingTerm} />
```html
<billing-cart-summary
[cart]="{
passwordManager: {
seats: {
quantity: 5,
translationKey: 'members',
cost: 50.00
}
},
cadence: 'monthly',
estimatedTax: 9.6
}"
[hidePricingTerm]="true"
>
</billing-cart-summary>
```
### Custom Header Template
Show cart with custom header template:
@@ -546,6 +623,10 @@ Show cart with families plan:
keys
- **Custom Header Templates**: Optional header input allows for custom header designs while
maintaining cart functionality
- **Hidden Cost Breakdown**: Individual cart items can hide their cost breakdown (quantity × unit
price) using the `hideBreakdown` property
- **Hidden Pricing Term**: Component can hide the billing term ("/ month" or "/ year") from the
header using the `hidePricingTerm` input
- **Flexible Structure**: Accommodates different combinations of products, add-ons, and discounts
- **Consistent Formatting**: Maintains uniform display of prices, quantities, and cadence
- **Modern Angular Patterns**: Uses `@let` to efficiently store and reuse signal values, OnPush
@@ -561,6 +642,9 @@ Show cart with families plan:
- Use valid translation keys for CartItem translationKey (for i18n lookup)
- Provide complete Cart object with all required fields
- Use "annually" or "monthly" for cadence (not "year" or "month")
- Use `hideBreakdown` on individual cart items when you want to hide cost breakdowns
- Use the `hidePricingTerm` component input when the billing term shouldn't be displayed in the
header
### ❌ Don't

View File

@@ -192,7 +192,7 @@ describe("CartSummaryComponent", () => {
it("should display correct secrets manager information", () => {
// Arrange
const smSection = fixture.debugElement.query(By.css('[id="secrets-manager"]'));
const smHeading = smSection.query(By.css("h3"));
const smHeading = smSection?.query(By.css('div[bitTypography="h5"]'));
const sectionText = fixture.debugElement.query(By.css('[id="secrets-manager-members"]'))
.nativeElement.textContent;
const additionalSA = fixture.debugElement.query(By.css('[id="additional-service-accounts"]'))
@@ -200,7 +200,8 @@ describe("CartSummaryComponent", () => {
// Act/ Assert
expect(smSection).toBeTruthy();
expect(smHeading.nativeElement.textContent.trim()).toBe("Secrets Manager");
expect(smHeading).toBeTruthy();
expect(smHeading!.nativeElement.textContent.trim()).toBe("Secrets Manager");
// Check seats line item
expect(sectionText).toContain("3 Secrets Manager seats");
@@ -245,7 +246,7 @@ describe("CartSummaryComponent", () => {
it("should display term (month/year) in default header", () => {
// Arrange / Act
const allSpans = fixture.debugElement.queryAll(By.css("span.tw-text-main"));
const allSpans = fixture.debugElement.queryAll(By.css("span.tw-text-muted"));
// Find the span that contains the term
const termElement = allSpans.find((span) => span.nativeElement.textContent.includes("/"));
@@ -253,6 +254,42 @@ describe("CartSummaryComponent", () => {
expect(termElement).toBeTruthy();
expect(termElement!.nativeElement.textContent.trim()).toBe("/ month");
});
it("should hide term when hidePricingTerm is true", () => {
// Arrange
const cartWithHiddenTerm: Cart = {
...mockCart,
};
fixture.componentRef.setInput("cart", cartWithHiddenTerm);
fixture.componentRef.setInput("hidePricingTerm", true);
fixture.detectChanges();
// Act
const allSpans = fixture.debugElement.queryAll(By.css("span.tw-text-muted"));
const termElement = allSpans.find((span) => span.nativeElement.textContent.includes("/"));
// Assert
expect(component.hidePricingTerm()).toBe(true);
expect(termElement).toBeFalsy();
});
it("should show term when hidePricingTerm is false", () => {
// Arrange
const cartWithVisibleTerm: Cart = {
...mockCart,
};
fixture.componentRef.setInput("cart", cartWithVisibleTerm);
fixture.detectChanges();
// Act
const allSpans = fixture.debugElement.queryAll(By.css("span.tw-text-muted"));
const termElement = allSpans.find((span) => span.nativeElement.textContent.includes("/"));
// Assert
expect(component.hidePricingTerm()).toBe(false);
expect(termElement).toBeTruthy();
expect(termElement!.nativeElement.textContent).toContain("/ month");
});
});
describe("hideBreakdown Property", () => {
@@ -287,7 +324,7 @@ describe("CartSummaryComponent", () => {
);
// Assert
expect(pmLineItem.nativeElement.textContent).toContain("5 Members x $50.00 / month");
expect(pmLineItem.nativeElement.textContent).toContain("5 Members x $50.00 / month");
});
it("should hide cost breakdown for additional storage when hideBreakdown is true", () => {
@@ -401,7 +438,7 @@ describe("CartSummaryComponent", () => {
const discountSection = fixture.debugElement.query(
By.css('[data-testid="discount-section"]'),
);
const discountLabel = discountSection.query(By.css("h3"));
const discountLabel = discountSection.query(By.css("div.tw-text-success-600"));
const discountAmount = discountSection.query(By.css('[data-testid="discount-amount"]'));
// Act / Assert
@@ -426,7 +463,7 @@ describe("CartSummaryComponent", () => {
const discountSection = fixture.debugElement.query(
By.css('[data-testid="discount-section"]'),
);
const discountLabel = discountSection.query(By.css("h3"));
const discountLabel = discountSection.query(By.css("div.tw-text-success-600"));
const discountAmount = discountSection.query(By.css('[data-testid="discount-amount"]'));
// Act / Assert
@@ -481,7 +518,7 @@ describe("CartSummaryComponent", () => {
fixture.detectChanges();
const creditSection = fixture.debugElement.query(By.css('[data-testid="credit-section"]'));
const creditLabel = creditSection.query(By.css("h3"));
const creditLabel = creditSection.query(By.css('div[bitTypography="body1"]'));
const creditAmount = creditSection.query(By.css('[data-testid="credit-amount"]'));
// Act / Assert

View File

@@ -432,3 +432,21 @@ export const WithDiscountAndCredit: Story = {
} satisfies Cart,
},
};
export const HiddenPricingTerm: Story = {
name: "Hidden Pricing Term",
args: {
cart: {
passwordManager: {
seats: {
quantity: 5,
translationKey: "members",
cost: 50.0,
},
},
cadence: "monthly",
estimatedTax: 9.6,
} satisfies Cart,
hidePricingTerm: true,
},
};

View File

@@ -37,6 +37,9 @@ export class CartSummaryComponent {
// Optional inputs
readonly header = input<TemplateRef<{ total: number }>>();
// Hide pricing term (e.g., "/ month" or "/ year") if true
readonly hidePricingTerm = input<boolean>(false);
// UI state
readonly isExpanded = signal(true);