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 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:
@@ -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"> </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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user