1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-24982] Create Cart Summary Component in Bitwarden Pricing (#16344)

* feature(billing): add cart-summary component

* tests(billing): add tests for component

* feature(billing): add stories and documentation for storybook

* feature(billing): export component

* fix: add localization and remove null coalescing for PM

* fix: import localization pipe and update story for I18n Service

remove service

* fix(billing): add IconButtonModule and use lineitem name

* fix(billing): Update story props and add Family and Premium examples

* fix(billing): Add examples and table of contents do to docs

* fix(billing): update aria properties

* fix(billing): add figma link and description

* fix(billing): update docs

* fix(billing): remove optional chaining since property is already checked

* fix(billing): Update fonts and button padding

* fix(billing): Update bitIconButton size to small
This commit is contained in:
Stephon Brown
2025-09-10 15:22:16 -04:00
committed by GitHub
parent af790c0d84
commit 4ef9ab2c9a
6 changed files with 927 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
@let passwordManager = this.passwordManager();
@let additionalStorage = this.additionalStorage();
@let secretsManager = this.secretsManager();
@let additionalServiceAccounts = this.secretsManager()?.additionalServiceAccounts;
<div class="tw-size-full">
<div class="tw-flex tw-items-center tw-pb-4">
<div class="tw-flex tw-items-center">
<h2 bitTypography="h4" id="purchase-summary-heading" class="!tw-m-0">
{{ "Total" | i18n }}: {{ total() | currency: "USD" : "symbol" }} USD
</h2>
<span bitTypography="h3">&nbsp;</span>
<span bitTypography="body1" class="tw-text-main">/ {{ passwordManager.cadence | i18n }}</span>
</div>
<button
type="button"
size="small"
(click)="toggleExpanded()"
[bitIconButton]="isExpanded() ? 'bwi-angle-up' : 'bwi-angle-down'"
[label]="isExpanded() ? 'Collapse purchase details' : 'Expand purchase details'"
[attr.aria-expanded]="isExpanded()"
[attr.aria-controls]="'purchase-summary-details'"
></button>
</div>
@if (isExpanded()) {
<div id="purchase-summary-details" class="tw-pb-2">
<!-- Password Manager Section -->
<div class="tw-border-b tw-border-secondary-100 tw-pb-2">
<div class="tw-flex tw-justify-between tw-mb-1">
<h3 bitTypography="h5" class="tw-text-muted">{{ "Password Manager" | i18n }}</h3>
</div>
<!-- Password Manager Members -->
<div class="tw-flex tw-justify-between">
<div class="tw-flex-1">
<div bitTypography="body1" class="tw-text-muted">
{{ passwordManager.quantity }} {{ passwordManager.name | i18n }} x
{{ passwordManager.cost | currency: "USD" : "symbol" }}
/
{{ passwordManager.cadence | i18n }}
</div>
</div>
<div bitTypography="body1" class="tw-text-muted">
{{ passwordManagerTotal() | currency: "USD" : "symbol" }}
</div>
</div>
<!-- Additional Storage -->
@if (additionalStorage) {
<div class="tw-flex tw-justify-between">
<div class="tw-flex-1">
<div bitTypography="body1" class="tw-text-muted">
{{ additionalStorage.quantity }} {{ additionalStorage.name | i18n }} x
{{ additionalStorage.cost | currency: "USD" : "symbol" }} /
{{ additionalStorage.cadence | i18n }}
</div>
</div>
<div bitTypography="body1" class="tw-text-muted">
{{ additionalStorageTotal() | currency: "USD" : "symbol" }}
</div>
</div>
}
</div>
<!-- Secrets Manager Section -->
@if (secretsManager) {
<div class="tw-border-b tw-border-secondary-100 tw-py-2">
<div class="tw-flex tw-justify-between">
<h3 bitTypography="h5" class="tw-text-muted">{{ "Secrets Manager" | i18n }}</h3>
</div>
<!-- Secrets Manager Members -->
<div class="tw-flex tw-justify-between">
<div bitTypography="body1" class="tw-text-muted">
{{ secretsManager.seats.quantity }} {{ secretsManager.seats.name | i18n }} x
{{ secretsManager.seats.cost | currency: "USD" : "symbol" }}
/ {{ secretsManager.seats.cadence | i18n }}
</div>
<div bitTypography="body1" class="tw-text-muted">
{{ secretsManagerSeatsTotal() | currency: "USD" : "symbol" }}
</div>
</div>
<!-- Additional Service Accounts -->
@if (additionalServiceAccounts) {
<div class="tw-flex tw-justify-between">
<div bitTypography="body1" class="tw-text-muted">
{{ additionalServiceAccounts.quantity }}
{{ additionalServiceAccounts.name | i18n }} x
{{ additionalServiceAccounts.cost | currency: "USD" : "symbol" }}
/
{{ additionalServiceAccounts.cadence | i18n }}
</div>
<div bitTypography="body1" class="tw-text-muted">
{{ additionalServiceAccountsTotal() | currency: "USD" : "symbol" }}
</div>
</div>
}
</div>
}
<!-- Estimated Tax -->
<div class="tw-flex tw-justify-between tw-border-b tw-border-secondary-100 tw-pt-2 tw-pb-0.5">
<h3 bitTypography="h5" class="tw-text-muted">{{ "Estimated tax" | i18n }}</h3>
<div bitTypography="body1" class="tw-text-muted">
{{ estimatedTax() | currency: "USD" : "symbol" }}
</div>
</div>
<!-- Total -->
<div class="tw-flex tw-justify-between tw-items-center tw-pt-2">
<h3 bitTypography="h5" class="tw-text-muted">{{ "Total" | i18n }}</h3>
<div bitTypography="body1" class="tw-text-muted">
{{ total() | currency: "USD" : "symbol" }} / {{ passwordManager.cadence | i18n }}
</div>
</div>
</div>
}
</div>

View File

@@ -0,0 +1,351 @@
import { Meta, Story, Canvas } from "@storybook/addon-docs";
import * as CartSummaryStories from "./cart-summary.component.stories";
<Meta of={CartSummaryStories} />
# Cart Summary
A reusable UI component for displaying order summary information with consistent styling and
behavior across Bitwarden applications.
<Canvas of={CartSummaryStories.Default} />
## Table of Contents
- [Usage](#usage)
- [API](#api)
- [Inputs](#inputs)
- [Events](#events)
- [Data Structure](#data-structure)
- [Flexibility](#flexibility)
- [Design](#design)
- [Examples](#examples)
- [Yearly Cadence](#yearly-cadence)
- [With Additional Storage](#with-additional-storage)
- [With Secrets Manager](#with-secrets-manager)
- [With Secrets Manager and Additional Service Accounts](#with-secrets-manager-and-additional-service-accounts)
- [All Products](#all-products)
- [Premium Plan](#premium-plan)
- [Family Plan](#family-plan)
- [Features](#features)
- [Do's and Don'ts](#dos-and-donts)
- [Accessibility](#accessibility)
## Usage
The cart summary component is designed to be used in checkout and subscription interfaces to display
order details, prices, and totals.
```ts
import { CartSummaryComponent, LineItem } from "@bitwarden/pricing";
```
```html
<billing-cart-summary
[passwordManager]="passwordManagerItem"
[additionalStorage]="additionalStorageItem"
[secretsManager]="secretsManagerItems"
[estimatedTax]="taxAmount"
>
</billing-cart-summary>
```
## API
### Inputs
| Input | Type | Description |
| ------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------- |
| `passwordManager` | `LineItem` | **Required.** The Password Manager product line item |
| `additionalStorage` | `LineItem \| undefined` | **Optional.** Additional storage line item, if applicable |
| `secretsManager` | `{ seats: LineItem; additionalServiceAccounts?: LineItem } \| undefined` | **Optional.** Secrets Manager related line items, if applicable |
| `estimatedTax` | `number` | **Required.** Estimated tax amount |
### Events
The cart summary component does not emit any events, but it does have internal behavior for
collapsing and expanding the view.
## Data Structure
The component uses the following LineItem data structure:
```typescript
export type LineItem = {
quantity: number; // Number of items
name: string; // Display name of the item
cost: number; // Cost of each item
cadence: "month" | "year"; // Billing period
};
```
## Flexibility
The cart summary component provides flexibility through its structured input properties:
```html
<!-- Basic usage with only Password Manager -->
<billing-cart-summary
[passwordManager]="{
quantity: 5,
name: 'Members',
cost: 50.00,
cadence: 'month'
}"
[estimatedTax]="9.60"
>
</billing-cart-summary>
<!-- With Additional Storage -->
<billing-cart-summary
[passwordManager]="{
quantity: 5,
name: 'Members',
cost: 50.00,
cadence: 'month'
}"
[additionalStorage]="{
quantity: 2,
name: 'Additional storage GB',
cost: 10.00,
cadence: 'month'
}"
[estimatedTax]="12.00"
>
</billing-cart-summary>
```
## Design
The component follows the Bitwarden design system with:
- **Collapsible Interface**: Toggles between compact and detailed views
- **Clean Styling**: Uses Tailwind utility classes for consistent appearance
- **Modern Angular**: Uses `@if` control flow and `@let` with signal inputs
- **Signal inputs**: Type-safe inputs using Angular's signal-based input API
- **Typography**: Consistent text styling using the typography module
- **Layout**: Flexbox layout with clear section boundaries
- **Interactivity**: Collapsible summary with intuitive toggle behavior
- **Accessibility**: Semantic structure with proper button and icon usage using IconButtonModule
## Examples
### Yearly Cadence
Show cart with yearly subscription:
<Canvas of={CartSummaryStories.PasswordManagerYearlyCadence} />
```html
<billing-cart-summary
[passwordManager]="{
quantity: 5,
name: 'Members',
cost: 500.00,
cadence: 'year'
}"
[estimatedTax]="120.00"
>
</billing-cart-summary>
```
### With Additional Storage
Show cart with password manager and additional storage:
<Canvas of={CartSummaryStories.WithAdditionalStorage} />
```html
<billing-cart-summary
[passwordManager]="{
quantity: 5,
name: 'Members',
cost: 50.00,
cadence: 'month'
}"
[additionalStorage]="{
quantity: 2,
name: 'Additional storage GB',
cost: 10.00,
cadence: 'month'
}"
[estimatedTax]="12.00"
>
</billing-cart-summary>
```
### With Secrets Manager
Show cart with password manager and secrets manager seats only:
<Canvas of={CartSummaryStories.SecretsManagerSeatsOnly} />
```html
<billing-cart-summary
[passwordManager]="{
quantity: 5,
name: 'Members',
cost: 50.00,
cadence: 'month'
}"
[secretsManager]="{
seats: {
quantity: 3,
name: 'Members',
cost: 30.00,
cadence: 'month'
}
}"
[estimatedTax]="16.00"
>
</billing-cart-summary>
```
### With Secrets Manager and Additional Service Accounts
Show cart with password manager, secrets manager seats, and additional service accounts:
<Canvas of={CartSummaryStories.SecretsManagerSeatsAndServiceAccounts} />
```html
<billing-cart-summary
[passwordManager]="{
quantity: 5,
name: 'Members',
cost: 50.00,
cadence: 'month'
}"
[secretsManager]="{
seats: {
quantity: 3,
name: 'Members',
cost: 30.00,
cadence: 'month'
},
additionalServiceAccounts: {
quantity: 2,
name: 'Additional machine accounts',
cost: 6.00,
cadence: 'month'
}
}"
[estimatedTax]="16.00"
>
</billing-cart-summary>
```
### All Products
Show a cart with all available products:
<Canvas of={CartSummaryStories.AllProducts} />
```html
<billing-cart-summary
[passwordManager]="{
quantity: 5,
name: 'Members',
cost: 50.00,
cadence: 'month'
}"
[additionalStorage]="{
quantity: 2,
name: 'Additional storage GB',
cost: 10.00,
cadence: 'month'
}"
[secretsManager]="{
seats: {
quantity: 3,
name: 'Members',
cost: 30.00,
cadence: 'month'
},
additionalServiceAccounts: {
quantity: 2,
name: 'Additional machine accounts',
cost: 6.00,
cadence: 'month'
}
}"
[estimatedTax]="19.20"
>
</billing-cart-summary>
```
### Premium Plan
Show cart with premium plan:
<Canvas of={CartSummaryStories.PremiumPlan} />
```html
<billing-cart-summary
[passwordManager]="{
quantity: 1,
name: 'Premium membership',
cost: 10.00,
cadence: 'month'
}"
[estimatedTax]="2.71"
>
</billing-cart-summary>
```
### Families Plan
Show cart with families plan:
<Canvas of={CartSummaryStories.FamiliesPlan} />
```html
<billing-cart-summary
[passwordManager]="{
quantity: 1,
name: 'Families membership',
cost: 40.00,
cadence: 'month'
}"
[estimatedTax]="4.67"
>
</billing-cart-summary>
```
## Features
- **Collapsible Interface**: Users can toggle between a summary view showing only the total and a
detailed view showing all line items
- **Line Item Grouping**: Organizes items by product category (Password Manager, Secrets Manager)
- **Dynamic Calculations**: Automatically calculates and displays subtotals and totals using Angular
signals and computed values
- **Flexible Structure**: Accommodates different combinations of products and add-ons
- **Consistent Formatting**: Maintains uniform display of prices, quantities, and cadence
- **Modern Angular Patterns**: Uses `@let` to efficiently store and reuse signal values in the
template
## Do's and Don'ts
### ✅ Do
- Use consistent naming and formatting for line items
- Include clear quantity and unit pricing information
- Ensure tax estimates are accurate and clearly labeled
- Maintain consistent cadence formats across related items
- Use the same cadence for all items within a single cart
### ❌ Don't
- Mix monthly and yearly cadences within the same cart
- Omit required inputs (passwordManager, estimatedTax)
- Modify the component's internal calculations
- Use inconsistent formatting for monetary values
- Override the default styles and layout
## Accessibility
The component includes:
- Semantic HTML structure with proper headings
- Button element for the collapsible toggle
- Keyboard navigation support
- Clear visual indication of expanded state
- Descriptive labels and text
- Sufficient color contrast for text elements

View File

@@ -0,0 +1,209 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { CartSummaryComponent, LineItem } from "./cart-summary.component";
describe("CartSummaryComponent", () => {
let component: CartSummaryComponent;
let fixture: ComponentFixture<CartSummaryComponent>;
const mockPasswordManager: LineItem = {
quantity: 5,
name: "Password Manager",
cost: 50,
cadence: "month",
};
const mockAdditionalStorage: LineItem = {
quantity: 2,
name: "Additional Storage",
cost: 10,
cadence: "month",
};
const mockSecretsManager = {
seats: {
quantity: 3,
name: "Secrets Manager Seats",
cost: 30,
cadence: "month" as "month" | "year",
},
additionalServiceAccounts: {
quantity: 2,
name: "Additional Service Accounts",
cost: 6,
cadence: "month" as "month" | "year",
},
};
const mockEstimatedTax = 9.6;
function setupComponent(
options: {
passwordManager?: LineItem;
additionalStorage?: LineItem | null;
secretsManager?: { seats: LineItem; additionalServiceAccounts?: LineItem } | null;
estimatedTax?: number;
} = {},
) {
const pm = options.passwordManager ?? mockPasswordManager;
const storage =
options.additionalStorage !== null
? (options.additionalStorage ?? mockAdditionalStorage)
: undefined;
const sm =
options.secretsManager !== null ? (options.secretsManager ?? mockSecretsManager) : undefined;
const tax = options.estimatedTax ?? mockEstimatedTax;
// Set input values
fixture.componentRef.setInput("passwordManager", pm);
if (storage !== undefined) {
fixture.componentRef.setInput("additionalStorage", storage);
}
if (sm !== undefined) {
fixture.componentRef.setInput("secretsManager", sm);
}
fixture.componentRef.setInput("estimatedTax", tax);
fixture.detectChanges();
}
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CartSummaryComponent],
}).compileComponents();
fixture = TestBed.createComponent(CartSummaryComponent);
component = fixture.componentInstance;
// Default setup with all inputs
setupComponent();
});
it("should create", () => {
// Assert
expect(component).toBeTruthy();
});
describe("UI Toggle Functionality", () => {
it("should toggle expanded state when the button is clicked", () => {
// Arrange
expect(component.isExpanded()).toBe(true);
const toggleButton = fixture.debugElement.query(By.css("button[type='button']"));
expect(toggleButton).toBeTruthy();
// Act - First click (collapse)
toggleButton.triggerEventHandler("click", null);
fixture.detectChanges();
// Assert - Component is collapsed
expect(component.isExpanded()).toBe(false);
const icon = fixture.debugElement.query(By.css("i.bwi"));
expect(icon.nativeElement.classList.contains("bwi-angle-down")).toBe(true);
// Act - Second click (expand)
toggleButton.triggerEventHandler("click", null);
fixture.detectChanges();
// Assert - Component is expanded again
expect(component.isExpanded()).toBe(true);
expect(icon.nativeElement.classList.contains("bwi-angle-up")).toBe(true);
});
it("should hide details when collapsed", () => {
// Arrange
component.isExpanded.set(false);
fixture.detectChanges();
// Act / Assert
const detailsSection = fixture.debugElement.query(By.css(".tw-mb-4.tw-pb-4.tw-text-muted"));
expect(detailsSection).toBeFalsy();
});
it("should show details when expanded", () => {
// Arrange
component.isExpanded.set(true);
fixture.detectChanges();
// Act / Assert
const detailsSection = fixture.debugElement.query(By.css(".tw-mb-4.tw-pb-4.tw-text-muted"));
expect(detailsSection).toBeTruthy();
});
});
describe("Content Rendering", () => {
it("should display correct password manager information", () => {
// Arrange
const pmSection = fixture.debugElement.query(By.css(".tw-mb-3.tw-border-b"));
const pmHeading = pmSection.query(By.css(".tw-font-semibold"));
const pmLineItem = pmSection.query(By.css(".tw-flex-1 .tw-text-sm"));
const pmTotal = pmSection.query(By.css(".tw-text-sm:not(.tw-flex-1 *)"));
// Act/ Assert
expect(pmSection).toBeTruthy();
expect(pmHeading.nativeElement.textContent.trim()).toBe("Password Manager");
expect(pmLineItem.nativeElement.textContent).toContain("5 Members");
expect(pmLineItem.nativeElement.textContent).toContain("$50.00");
expect(pmLineItem.nativeElement.textContent).toContain("month");
expect(pmTotal.nativeElement.textContent).toContain("$250.00"); // 5 * $50
});
it("should display correct additional storage information", () => {
// Arrange
const storageItem = fixture.debugElement.query(
By.css(".tw-mb-3.tw-border-b .tw-flex-justify-between:nth-of-type(3)"),
);
const storageText = fixture.debugElement.query(By.css(".tw-mb-3.tw-border-b")).nativeElement
.textContent;
// Act/Assert
expect(storageItem).toBeTruthy();
expect(storageText).toContain("2 Additional GB");
expect(storageText).toContain("$10.00");
expect(storageText).toContain("$20.00");
});
it("should display correct secrets manager information", () => {
// Arrange
const smSection = fixture.debugElement.queryAll(By.css(".tw-mb-3.tw-border-b"))[1];
const smHeading = smSection.query(By.css(".tw-font-semibold"));
const sectionText = smSection.nativeElement.textContent;
// Act/ Assert
expect(smSection).toBeTruthy();
expect(smHeading.nativeElement.textContent.trim()).toBe("Secrets Manager");
// Check seats line item
expect(sectionText).toContain("3 Members");
expect(sectionText).toContain("$30.00");
expect(sectionText).toContain("$90.00"); // 3 * $30
// Check additional service accounts
expect(sectionText).toContain("2 Additional machine accounts");
expect(sectionText).toContain("$6.00");
expect(sectionText).toContain("$12.00"); // 2 * $6
});
it("should display correct tax and total", () => {
// Arrange
const taxSection = fixture.debugElement.query(
By.css(".tw-flex.tw-justify-between.tw-mb-3.tw-border-b:last-of-type"),
);
const expectedTotal = "$381.60"; // 250 + 20 + 90 + 12 + 9.6
const topTotal = fixture.debugElement.query(By.css("h2"));
const bottomTotal = fixture.debugElement.query(
By.css(
".tw-flex.tw-justify-between.tw-items-center:last-child .tw-font-semibold:last-child",
),
);
// Act / Assert
expect(taxSection.nativeElement.textContent).toContain("Estimated Tax");
expect(taxSection.nativeElement.textContent).toContain("$9.60");
expect(topTotal.nativeElement.textContent).toContain(expectedTotal);
expect(bottomTotal.nativeElement.textContent).toContain(expectedTotal);
});
});
});

View File

@@ -0,0 +1,154 @@
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { IconButtonModule, TypographyModule } from "@bitwarden/components";
import { CartSummaryComponent } from "./cart-summary.component";
export default {
title: "Billing/Cart Summary",
component: CartSummaryComponent,
description: "A summary of the items in the cart, including pricing details.",
decorators: [
moduleMetadata({
imports: [TypographyModule, IconButtonModule],
// Return the same value for all keys for simplicity
providers: [
{
provide: I18nService,
useValue: { t: (key: string) => key },
},
],
}),
],
args: {
passwordManager: {
quantity: 5,
name: "Members",
cost: 50.0,
cadence: "month",
},
estimatedTax: 9.6,
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/design/nuFrzHsgEoEk2Sm8fWOGuS/Premium-Upgrade-flows--pricing-increase-?node-id=877-23653&t=OpDXkupIsvfbh4jT-4",
},
},
} as Meta<CartSummaryComponent>;
type Story = StoryObj<CartSummaryComponent>;
export const Default: Story = {};
export const WithAdditionalStorage: Story = {
args: {
...Default.args,
additionalStorage: {
quantity: 2,
name: "Additional storage GB",
cost: 10.0,
cadence: "month",
},
estimatedTax: 12.0,
},
};
export const PasswordManagerYearlyCadence: Story = {
args: {
passwordManager: {
quantity: 5,
name: "Members",
cost: 500.0,
cadence: "year",
},
estimatedTax: 120.0,
},
};
export const SecretsManagerSeatsOnly: Story = {
args: {
...Default.args,
secretsManager: {
seats: {
quantity: 3,
name: "Members",
cost: 30.0,
cadence: "month",
},
},
estimatedTax: 16.0,
},
};
export const SecretsManagerSeatsAndServiceAccounts: Story = {
args: {
...Default.args,
secretsManager: {
seats: {
quantity: 3,
name: "Members",
cost: 30.0,
cadence: "month",
},
additionalServiceAccounts: {
quantity: 2,
name: "Additional machine accounts",
cost: 6.0,
cadence: "month",
},
},
estimatedTax: 16.0,
},
};
export const AllProducts: Story = {
args: {
...Default.args,
additionalStorage: {
quantity: 2,
name: "Additional storage GB",
cost: 10.0,
cadence: "month",
},
secretsManager: {
seats: {
quantity: 3,
name: "Members",
cost: 30.0,
cadence: "month",
},
additionalServiceAccounts: {
quantity: 2,
name: "Additional machine accounts",
cost: 6.0,
cadence: "month",
},
},
estimatedTax: 19.2,
},
};
export const FamiliesPlan: Story = {
args: {
passwordManager: {
quantity: 1,
name: "Families membership",
cost: 40.0,
cadence: "year",
},
estimatedTax: 4.67,
},
};
export const PremiumPlan: Story = {
args: {
passwordManager: {
quantity: 1,
name: "Premium membership",
cost: 10.0,
cadence: "year",
},
estimatedTax: 2.71,
},
};

View File

@@ -0,0 +1,92 @@
import { CurrencyPipe } from "@angular/common";
import { Component, computed, input, signal } from "@angular/core";
import { TypographyModule, IconButtonModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
export type LineItem = {
quantity: number;
name: string;
cost: number;
cadence: "month" | "year";
};
/**
* A reusable UI-only component that displays a cart summary with line items.
* This component has no external dependencies and performs minimal logic -
* it only displays data and allows expanding/collapsing of line items.
*/
@Component({
selector: "billing-cart-summary",
templateUrl: "./cart-summary.component.html",
imports: [TypographyModule, IconButtonModule, CurrencyPipe, I18nPipe],
})
export class CartSummaryComponent {
// Required inputs
passwordManager = input.required<LineItem>();
additionalStorage = input<LineItem>();
secretsManager = input<{ seats: LineItem; additionalServiceAccounts?: LineItem }>();
estimatedTax = input.required<number>();
// UI state
isExpanded = signal(true);
/**
* Calculates total for password manager line item
*/
readonly passwordManagerTotal = computed<number>(() => {
return this.passwordManager().quantity * this.passwordManager().cost;
});
/**
* Calculates total for additional storage line item if present
*/
readonly additionalStorageTotal = computed<number>(() => {
const storage = this.additionalStorage();
return storage ? storage.quantity * storage.cost : 0;
});
/**
* Calculates total for secrets manager seats if present
*/
readonly secretsManagerSeatsTotal = computed<number>(() => {
const sm = this.secretsManager();
return sm?.seats ? sm.seats.quantity * sm.seats.cost : 0;
});
/**
* Calculates total for secrets manager service accounts if present
*/
readonly additionalServiceAccountsTotal = computed<number>(() => {
const sm = this.secretsManager();
return sm?.additionalServiceAccounts
? sm.additionalServiceAccounts.quantity * sm.additionalServiceAccounts.cost
: 0;
});
/**
* Calculates the total of all line items
*/
readonly total = computed<number>(() => this.getTotalCost());
/**
* Toggles the expanded/collapsed state of the cart items
*/
toggleExpanded(): void {
this.isExpanded.update((value: boolean) => !value);
}
/**
* Gets the total cost of all line items in the cart
* @returns The total cost as a number
*/
private getTotalCost(): number {
return (
this.passwordManagerTotal() +
this.additionalStorageTotal() +
this.secretsManagerSeatsTotal() +
this.additionalServiceAccountsTotal() +
this.estimatedTax()
);
}
}

View File

@@ -1,2 +1,3 @@
// Components
export * from "./components/pricing-card/pricing-card.component";
export * from "./components/cart-summary/cart-summary.component";