From 8c7faf49d5a5798145739b672d814629aa0a4350 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 4 Sep 2025 20:14:04 +0530 Subject: [PATCH] Billing/pm 23385 premium modal in web after registration (#16182) * create the pricing library * Create pricing-card.component * Refactor the code * feat: Add pricing card component library * Fix the test failing error * Address billing pr comments * feat: Add Storybook documentation and stories for pricing-card component * Fix some ui feedback * Changes from the display and sizes * feat(billing): refactor pricing card with flexible title slots and active badge * Enhance pricing card with flexible design and button icons * refactor: organize pricing card files into dedicated folder * Complete pricing card enhancements with Chromatic feedback fixes * refactor base on pr coments * Fix the button alignment * Update all the card to have the same height * Fix the slot issue on the title * Fix the Lint format issue * Add the header in the stories book --- .github/CODEOWNERS | 1 + .storybook/main.ts | 2 + libs/pricing/README.md | 5 + libs/pricing/jest.config.js | 16 + libs/pricing/package.json | 21 + libs/pricing/project.json | 33 ++ .../pricing-card/pricing-card.component.html | 85 ++++ .../pricing-card/pricing-card.component.mdx | 228 +++++++++++ .../pricing-card.component.spec.ts | 194 ++++++++++ .../pricing-card.component.stories.ts | 365 ++++++++++++++++++ .../pricing-card/pricing-card.component.ts | 42 ++ libs/pricing/src/index.ts | 2 + libs/pricing/src/pricing.spec.ts | 8 + libs/pricing/test.setup.ts | 28 ++ libs/pricing/tsconfig.json | 13 + libs/pricing/tsconfig.lib.json | 10 + libs/pricing/tsconfig.spec.json | 10 + package-lock.json | 8 + tailwind.config.js | 1 + tsconfig.base.json | 1 + 20 files changed, 1073 insertions(+) create mode 100644 libs/pricing/README.md create mode 100644 libs/pricing/jest.config.js create mode 100644 libs/pricing/package.json create mode 100644 libs/pricing/project.json create mode 100644 libs/pricing/src/components/pricing-card/pricing-card.component.html create mode 100644 libs/pricing/src/components/pricing-card/pricing-card.component.mdx create mode 100644 libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts create mode 100644 libs/pricing/src/components/pricing-card/pricing-card.component.stories.ts create mode 100644 libs/pricing/src/components/pricing-card/pricing-card.component.ts create mode 100644 libs/pricing/src/index.ts create mode 100644 libs/pricing/src/pricing.spec.ts create mode 100644 libs/pricing/test.setup.ts create mode 100644 libs/pricing/tsconfig.json create mode 100644 libs/pricing/tsconfig.lib.json create mode 100644 libs/pricing/tsconfig.spec.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c190a77068..154dcb0f72 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -216,3 +216,4 @@ apps/web/src/locales/en/messages.json **/tsconfig.json @bitwarden/team-platform-dev **/jest.config.js @bitwarden/team-platform-dev **/project.jsons @bitwarden/team-platform-dev +libs/pricing @bitwarden/team-billing-dev diff --git a/.storybook/main.ts b/.storybook/main.ts index 879e87fe37..d3811bb178 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -10,6 +10,8 @@ const config: StorybookConfig = { "../libs/auth/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/dirt/card/src/**/*.mdx", "../libs/dirt/card/src/**/*.stories.@(js|jsx|ts|tsx)", + "../libs/pricing/src/**/*.mdx", + "../libs/pricing/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/tools/send/send-ui/src/**/*.mdx", "../libs/tools/send/send-ui/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/vault/src/**/*.mdx", diff --git a/libs/pricing/README.md b/libs/pricing/README.md new file mode 100644 index 0000000000..600dd64f71 --- /dev/null +++ b/libs/pricing/README.md @@ -0,0 +1,5 @@ +# pricing + +Owned by: billing + +Components and services that facilitate the retrieval and display of Bitwarden's pricing. diff --git a/libs/pricing/jest.config.js b/libs/pricing/jest.config.js new file mode 100644 index 0000000000..2aa2bfa828 --- /dev/null +++ b/libs/pricing/jest.config.js @@ -0,0 +1,16 @@ +const { pathsToModuleNameMapper } = require("ts-jest"); + +const { compilerOptions } = require("../../tsconfig.base"); + +const sharedConfig = require("../../libs/shared/jest.config.angular"); + +/** @type {import('jest').Config} */ +module.exports = { + ...sharedConfig, + displayName: "libs/pricing tests", + setupFilesAfterEnv: ["/test.setup.ts"], + coverageDirectory: "../../coverage/libs/pricing", + moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { + prefix: "/../../", + }), +}; diff --git a/libs/pricing/package.json b/libs/pricing/package.json new file mode 100644 index 0000000000..9d5ec85c1b --- /dev/null +++ b/libs/pricing/package.json @@ -0,0 +1,21 @@ +{ + "name": "@bitwarden/pricing", + "version": "0.0.0", + "description": "Components and services that facilitate the retrieval and display of Bitwarden's pricing.", + "keywords": [ + "bitwarden" + ], + "author": "Bitwarden Inc.", + "homepage": "https://bitwarden.com", + "repository": { + "type": "git", + "url": "https://github.com/bitwarden/clients" + }, + "license": "GPL-3.0", + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && tsc", + "build:watch": "npm run clean && tsc -watch" + }, + "private": true +} diff --git a/libs/pricing/project.json b/libs/pricing/project.json new file mode 100644 index 0000000000..7e6e154bce --- /dev/null +++ b/libs/pricing/project.json @@ -0,0 +1,33 @@ +{ + "name": "pricing", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/pricing/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/pricing", + "main": "libs/pricing/src/index.ts", + "tsConfig": "libs/pricing/tsconfig.lib.json", + "assets": ["libs/pricing/*.md"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/pricing/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/pricing/jest.config.js" + } + } + } +} diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.html b/libs/pricing/src/components/pricing-card/pricing-card.component.html new file mode 100644 index 0000000000..d0ac4fc519 --- /dev/null +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.html @@ -0,0 +1,85 @@ +
+ +
+ + + + @if (activeBadge(); as activeBadgeValue) { + + {{ activeBadgeValue.text }} + + } +
+ + +
+

+ {{ tagline() }} +

+
+ + + @if (price(); as priceValue) { +
+
+ ${{ priceValue.amount }} + + / {{ priceValue.cadence }} + @if (priceValue.showPerUser) { + per user + } + +
+
+ } + + +
+ @if (button(); as buttonConfig) { + + } +
+ + +
+ @if (features(); as featureList) { + @if (featureList.length > 0) { +
    + @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 new file mode 100644 index 0000000000..355ca71eb8 --- /dev/null +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.mdx @@ -0,0 +1,228 @@ +import { Meta, Story, Canvas } from "@storybook/addon-docs"; +import * as PricingCardStories from "./pricing-card.component.stories"; + + + +# Pricing Card + +A reusable UI component for displaying pricing plans with consistent styling and behavior across +Bitwarden applications. + + + +## Usage + +The pricing card component is designed to be used in billing and subscription interfaces to display +different pricing tiers and plans. + +```ts +import { PricingCardComponent } from "@bitwarden/pricing"; +``` + +```html + +

Premium Plan

+
+``` + +## API + +### Inputs + +| 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 | +| `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 | + +### Content Slots + +| Slot | Description | +| -------------- | ----------------------------------------------------------------------------------------------------------------- | +| `slot="title"` | **Required.** HTML element with `slot="title"` attribute for the title with appropriate heading level and styling | + +### Events + +| Event | Type | Description | +| ------------- | ------ | ----------------------------------------- | +| `buttonClick` | `void` | Emitted when the action button is clicked | + +## Flexibility + +The title slot allows complete control over the heading element and styling: + +```html + +

Main Plan

+

Sub Plan

+ + +

Featured Plan

+``` + +| Output | Type | Description | +| ------------- | ------ | --------------------------------------- | +| `buttonClick` | `void` | Emitted when the plan button is clicked | + +## Design + +The component follows the Bitwarden design system with: + +- **Fixed width**: 449px for consistent layout +- **Border & Elevation**: secondary-100 border with shadow-sm elevation +- **Border radius**: 24px (tw-rounded-3xl) for modern appearance +- **Spacing**: 32px padding (tw-p-8) around content +- **Modern Angular**: Uses `@if`, `@for`, and `@switch` control flow with signal inputs +- **Signal inputs**: Type-safe inputs using Angular's signal-based input API +- **Official buttons**: Uses `bitButton` directive from Component Library +- **Typography**: Uses `bitTypography` helper and custom 30px price styling +- **Icons**: Uses `bwi-check` icon with primary-600 styling from the legacy icon library +- **Layout**: Flexbox column layout with `tw-h-full` for equal height alignment in grid layouts +- **Accessibility**: Configurable heading levels and semantic structure + +## Examples + +### Basic Plan (No Price) + +For free or contact-based plans, omit the `price` input: + + + +```html + + +``` + +### Business Plan with Per User Pricing + +Show business plans with "per user" text: + + + +```html + + +``` + +### Annual Pricing + +Show annual pricing with different cadence: + + + +```html + + +``` + +### Configurable Heading Levels + +For accessibility, you can configure the heading level: + + + +```html + + + + + + + +``` + +### Disabled State + +For coming soon or unavailable plans: + + + +```html + + +``` + +### Pricing Grid Layout + +Multiple cards displayed together: + + + +## Button Types + +The component supports all standard button types from the Component Library: + +- `primary` - Main call-to-action (blue background, white text) +- `secondary` - Secondary action (transparent background, blue text) +- `danger` - Destructive actions (red theme) +- `unstyled` - Text-only button + +## Do's and Don'ts + +### ✅ Do + +- Use consistent button text like "Choose [Plan]" or "Get Started" +- Keep taglines concise and focused on key benefits +- Use annual pricing to show value (e.g., "2 months free") +- Group related plans together with consistent styling + +### ❌ Don't + +- Make taglines longer than 2 lines (they will be truncated) +- Use custom button styling - rely on the built-in types +- Mix different pricing cadences in the same comparison +- Override the 449px width - it's designed for optimal layout + +## Accessibility + +The component includes: + +- Proper heading hierarchy (`h3` for titles) +- Semantic button elements with `type="button"` +- Screen reader friendly structure +- Focus management and keyboard navigation +- High contrast color combinations 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 new file mode 100644 index 0000000000..ed2c28d8cb --- /dev/null +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts @@ -0,0 +1,194 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { ButtonType, IconModule, TypographyModule } from "@bitwarden/components"; + +import { PricingCardComponent } from "./pricing-card.component"; + +@Component({ + template: ` + + +

{{ titleText }}

+ +

{{ titleText }}

+ +

{{ titleText }}

+ +

{{ titleText }}

+ +
{{ titleText }}
+ +
{{ titleText }}
+
+
+ `, + imports: [PricingCardComponent, CommonModule, TypographyModule], +}) +class TestHostComponent { + titleText = "Test Plan"; + tagline = "A great plan for testing"; + price: { amount: number; cadence: "monthly" | "annually"; showPerUser?: boolean } = { + amount: 10, + cadence: "monthly", + }; + button: { type: ButtonType; text: string; disabled?: boolean } = { + text: "Select Plan", + type: "primary", + }; + features = ["Feature 1", "Feature 2", "Feature 3"]; + titleLevel: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" = "h3"; + activeBadge: { text: string; variant?: string } | undefined = undefined; + + onButtonClick() { + // Test method + } +} + +describe("PricingCardComponent", () => { + let component: PricingCardComponent; + let fixture: ComponentFixture; + let hostComponent: TestHostComponent; + let hostFixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + PricingCardComponent, + TestHostComponent, + IconModule, + TypographyModule, + CommonModule, + ], + }).compileComponents(); + + // For signal inputs, we need to set required inputs through the host component + hostFixture = TestBed.createComponent(TestHostComponent); + hostComponent = hostFixture.componentInstance; + + fixture = TestBed.createComponent(PricingCardComponent); + component = fixture.componentInstance; + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should display title and tagline", () => { + hostFixture.detectChanges(); + const compiled = hostFixture.nativeElement; + + // Test that the component renders and shows the tagline (which is an input, not projected content) + expect(compiled.querySelector("p").textContent).toContain("A great plan for testing"); + // Note: Title testing is skipped due to content projection limitations in Angular testing + }); + + it("should display price when provided", () => { + hostFixture.detectChanges(); + const compiled = hostFixture.nativeElement; + + expect(compiled.textContent).toContain("$10"); + expect(compiled.textContent).toContain("/ monthly"); + }); + + it("should display features when provided", () => { + hostFixture.detectChanges(); + const compiled = hostFixture.nativeElement; + + expect(compiled.textContent).toContain("Feature 1"); + expect(compiled.textContent).toContain("Feature 2"); + expect(compiled.textContent).toContain("Feature 3"); + }); + + it("should emit buttonClick when button is clicked", () => { + jest.spyOn(hostComponent, "onButtonClick"); + hostFixture.detectChanges(); + + const button = hostFixture.nativeElement.querySelector("button"); + button.click(); + + expect(hostComponent.onButtonClick).toHaveBeenCalled(); + }); + + it("should work without optional inputs", () => { + hostComponent.price = undefined as any; + hostComponent.features = undefined as any; + hostComponent.button = undefined as any; + + hostFixture.detectChanges(); + + // Note: Title content projection testing skipped due to Angular testing limitations + expect(hostFixture.nativeElement.querySelector("button")).toBeFalsy(); + }); + + it("should display per user text when showPerUser is true", () => { + hostComponent.price = { amount: 5, cadence: "monthly", showPerUser: true }; + hostFixture.detectChanges(); + const compiled = hostFixture.nativeElement; + + expect(compiled.textContent).toContain("$5"); + expect(compiled.textContent).toContain("per user"); + }); + + it("should use configurable heading level", () => { + hostComponent.titleLevel = "h2"; + hostFixture.detectChanges(); + + // Note: Content projection testing for configurable headings is covered in Storybook + // Angular unit tests have limitations with content projection testing + expect(component).toBeTruthy(); // Basic smoke test + }); + + it("should display bwi-check icons for features", () => { + hostFixture.detectChanges(); + const compiled = hostFixture.nativeElement; + const icons = compiled.querySelectorAll("i.bwi-check"); + + expect(icons.length).toBe(3); // One for each feature + }); + + it("should not display button when button input is not provided", () => { + hostComponent.button = undefined as any; + hostFixture.detectChanges(); + const compiled = hostFixture.nativeElement; + + expect(compiled.querySelector("button")).toBeFalsy(); + }); + + it("should display active badge when activeBadge is provided", () => { + hostComponent.activeBadge = { text: "Current Plan" }; + hostFixture.detectChanges(); + const compiled = hostFixture.nativeElement; + + const badge = compiled.querySelector("span[bitBadge]"); + expect(badge).toBeTruthy(); + expect(badge.textContent.trim()).toBe("Current Plan"); + }); + + it("should not display active badge when activeBadge is not provided", () => { + hostComponent.activeBadge = undefined; + hostFixture.detectChanges(); + const compiled = hostFixture.nativeElement; + + expect(compiled.querySelector("span[bitBadge]")).toBeFalsy(); + }); + + it("should have proper layout structure with flexbox", () => { + hostFixture.detectChanges(); + const compiled = hostFixture.nativeElement; + const cardContainer = compiled.querySelector("div"); + + expect(cardContainer.classList).toContain("tw-flex"); + expect(cardContainer.classList).toContain("tw-flex-col"); + expect(cardContainer.classList).toContain("tw-size-full"); + expect(cardContainer.classList).not.toContain("tw-block"); // Should not have conflicting display property + }); +}); 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 new file mode 100644 index 0000000000..832345de35 --- /dev/null +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.stories.ts @@ -0,0 +1,365 @@ +import { Meta, StoryObj } from "@storybook/angular"; + +import { TypographyModule } from "@bitwarden/components"; + +import { PricingCardComponent } from "./pricing-card.component"; + +export default { + title: "Billing/Pricing Card", + component: PricingCardComponent, + moduleMetadata: { + imports: [TypographyModule], + }, + args: { + tagline: "Everything you need for secure password management across all your devices", + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/nuFrzHsgEoEk2Sm8fWOGuS/Premium-Upgrade-flows--pricing-increase-?node-id=858-44276&t=KjcXRRvf8PXJI51j-0", + }, + }, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: ` + +

Premium Plan

+
+ `, + }), + args: { + tagline: "Everything you need for secure password management across all your devices", + price: { amount: 10, cadence: "monthly" }, + button: { text: "Choose Premium", type: "primary" }, + features: [ + "Unlimited passwords and passkeys", + "Secure password sharing", + "Integrated 2FA authenticator", + "Advanced 2FA options", + "Priority customer support", + ], + }, +}; + +export const WithoutPrice: Story = { + render: (args) => ({ + props: args, + template: ` + +

Free Plan

+
+ `, + }), + args: { + tagline: "Get started with essential password management features", + button: { text: "Get Started", type: "secondary" }, + features: ["Store unlimited passwords", "Access from any device", "Secure password generator"], + }, +}; + +export const WithoutFeatures: Story = { + render: (args) => ({ + props: args, + template: ` + +

Enterprise Plan

+
+ `, + }), + args: { + tagline: "Advanced security and management for your organization", + price: { amount: 3, cadence: "monthly" }, + button: { text: "Contact Sales", type: "primary" }, + }, +}; + +export const Annual: Story = { + render: (args) => ({ + props: args, + template: ` + +

Premium Plan

+
+ `, + }), + args: { + tagline: "Save more with annual billing", + price: { amount: 120, cadence: "annually" }, + button: { text: "Choose Annual", type: "primary" }, + features: [ + "All Premium features", + "2 months free with annual billing", + "Priority customer support", + ], + }, +}; + +export const Disabled: Story = { + render: (args) => ({ + props: args, + template: ` + +

Coming Soon

+
+ `, + }), + args: { + tagline: "This plan will be available soon with exciting new features", + price: { amount: 15, cadence: "monthly" }, + button: { text: "Coming Soon", type: "secondary", disabled: true }, + features: ["Advanced security features", "Enhanced collaboration tools", "Premium support"], + }, +}; + +export const LongTagline: Story = { + render: (args) => ({ + props: args, + template: ` + +

Business Plan

+
+ `, + }), + 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 }, + button: { text: "Start Business Trial", type: "primary" }, + features: [ + "Everything in Premium", + "Admin dashboard", + "Team reporting", + "Advanced permissions", + "SSO integration", + ], + }, +}; + +export const AllButtonTypes: Story = { + render: () => ({ + template: ` +
+ +

Primary Button

+
+ + +

Secondary Button

+
+ + +

Danger Button

+
+ + +

Unstyled Button

+
+
+ `, + props: {}, + }), +}; + +export const ConfigurableHeadings: Story = { + render: () => ({ + template: ` +
+ +

H2 Heading

+
+ + +

H4 Heading

+
+
+ `, + props: {}, + }), +}; + +export const PricingGrid: Story = { + render: () => ({ + template: ` +
+ +

Free

+
+ + +

Premium

+
+ + +

Business

+
+
+ `, + props: {}, + }), +}; + +export const WithoutButton: Story = { + render: (args) => ({ + props: args, + template: ` + +

Coming Soon Plan

+
+ `, + }), + args: { + tagline: "This plan will be available soon with exciting new features", + price: { amount: 15, cadence: "monthly" }, + features: ["Advanced security features", "Enhanced collaboration tools", "Premium support"], + }, +}; + +export const ActivePlan: Story = { + render: (args) => ({ + props: args, + template: ` + +

Free

+
+ `, + }), + args: { + tagline: "Your current plan with essential password management features", + features: ["Store unlimited passwords", "Access from any device", "Secure password generator"], + activeBadge: { text: "Active plan" }, + }, +}; + +export const PricingComparison: Story = { + render: () => ({ + template: ` +
+
+ +

Free

+
+
+ +
+ +

Premium

+
+
+ +
+ +

Business

+
+
+
+ `, + props: {}, + }), +}; + +export const WithButtonIcon: Story = { + render: () => ({ + template: ` +
+ + +

Premium

+
+ + + +

Business

+
+
+ `, + props: {}, + }), +}; diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.ts b/libs/pricing/src/components/pricing-card/pricing-card.component.ts new file mode 100644 index 0000000000..b727fb1067 --- /dev/null +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.ts @@ -0,0 +1,42 @@ +import { Component, EventEmitter, input, Output } from "@angular/core"; + +import { + BadgeModule, + BadgeVariant, + ButtonModule, + ButtonType, + IconModule, + TypographyModule, +} from "@bitwarden/components"; + +/** + * A reusable UI-only component that displays pricing information in a card format. + * This component has no external dependencies and performs no logic - it only displays data + * and emits events when the button is clicked. + */ +@Component({ + selector: "billing-pricing-card", + templateUrl: "./pricing-card.component.html", + imports: [BadgeModule, ButtonModule, IconModule, TypographyModule], +}) +export class PricingCardComponent { + tagline = input.required(); + price = input<{ amount: number; cadence: "monthly" | "annually"; showPerUser?: boolean }>(); + button = input<{ + type: ButtonType; + text: string; + disabled?: boolean; + icon?: { type: string; position: "before" | "after" }; + }>(); + features = input(); + activeBadge = input<{ text: string; variant?: BadgeVariant }>(); + + @Output() buttonClick = new EventEmitter(); + + /** + * Handles button click events and emits the buttonClick event + */ + onButtonClick(): void { + this.buttonClick.emit(); + } +} diff --git a/libs/pricing/src/index.ts b/libs/pricing/src/index.ts new file mode 100644 index 0000000000..9eeb2de518 --- /dev/null +++ b/libs/pricing/src/index.ts @@ -0,0 +1,2 @@ +// Components +export * from "./components/pricing-card/pricing-card.component"; diff --git a/libs/pricing/src/pricing.spec.ts b/libs/pricing/src/pricing.spec.ts new file mode 100644 index 0000000000..3b66c8f0e6 --- /dev/null +++ b/libs/pricing/src/pricing.spec.ts @@ -0,0 +1,8 @@ +import * as lib from "./index"; + +describe("pricing", () => { + // This test will fail until something is exported from index.ts + it("should work", () => { + expect(lib).toBeDefined(); + }); +}); diff --git a/libs/pricing/test.setup.ts b/libs/pricing/test.setup.ts new file mode 100644 index 0000000000..159c28d2be --- /dev/null +++ b/libs/pricing/test.setup.ts @@ -0,0 +1,28 @@ +import { webcrypto } from "crypto"; +import "@bitwarden/ui-common/setup-jest"; + +Object.defineProperty(window, "CSS", { value: null }); +Object.defineProperty(window, "getComputedStyle", { + value: () => { + return { + display: "none", + appearance: ["-webkit-appearance"], + }; + }, +}); + +Object.defineProperty(document, "doctype", { + value: "", +}); +Object.defineProperty(document.body.style, "transform", { + value: () => { + return { + enumerable: true, + configurable: true, + }; + }, +}); + +Object.defineProperty(window, "crypto", { + value: webcrypto, +}); diff --git a/libs/pricing/tsconfig.json b/libs/pricing/tsconfig.json new file mode 100644 index 0000000000..62ebbd9464 --- /dev/null +++ b/libs/pricing/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/pricing/tsconfig.lib.json b/libs/pricing/tsconfig.lib.json new file mode 100644 index 0000000000..9cbf673600 --- /dev/null +++ b/libs/pricing/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.js", "src/**/*.spec.ts"] +} diff --git a/libs/pricing/tsconfig.spec.json b/libs/pricing/tsconfig.spec.json new file mode 100644 index 0000000000..1275f148a1 --- /dev/null +++ b/libs/pricing/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/package-lock.json b/package-lock.json index 7c99ed8517..4bd1238b27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -392,6 +392,10 @@ "version": "0.0.0", "license": "GPL-3.0" }, + "libs/pricing": { + "version": "0.0.1", + "license": "GPL-3.0" + }, "libs/serialization": { "name": "@bitwarden/serialization", "version": "0.0.1", @@ -4681,6 +4685,10 @@ "resolved": "libs/platform", "link": true }, + "node_modules/@bitwarden/pricing": { + "resolved": "libs/pricing", + "link": true + }, "node_modules/@bitwarden/sdk-internal": { "version": "0.2.0-main.266", "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.266.tgz", diff --git a/tailwind.config.js b/tailwind.config.js index dff04c897c..bb0489ed10 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -8,6 +8,7 @@ config.content = [ "./libs/billing/src/**/*.{html,ts,mdx}", "./libs/assets/src/**/*.{html,ts}", "./libs/platform/src/**/*.{html,ts,mdx}", + "./libs/pricing/src/**/*.{html,ts,mdx}", "./libs/tools/send/send-ui/src/*.{html,ts,mdx}", "./libs/vault/src/**/*.{html,ts,mdx}", "./apps/web/src/**/*.{html,ts,mdx}", diff --git a/tsconfig.base.json b/tsconfig.base.json index 3d1d2915f6..3f903558f7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -50,6 +50,7 @@ "@bitwarden/nx-plugin": ["libs/nx-plugin/src/index.ts"], "@bitwarden/platform": ["./libs/platform/src"], "@bitwarden/platform/*": ["./libs/platform/src/*"], + "@bitwarden/pricing": ["libs/pricing/src/index.ts"], "@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"], "@bitwarden/serialization": ["libs/serialization/src/index.ts"], "@bitwarden/state": ["libs/state/src/index.ts"],