mirror of
https://github.com/bitwarden/browser.git
synced 2025-12-05 09:14:28 +01:00
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
This commit is contained in:
parent
866f56f2d5
commit
8c7faf49d5
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
5
libs/pricing/README.md
Normal file
5
libs/pricing/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# pricing
|
||||
|
||||
Owned by: billing
|
||||
|
||||
Components and services that facilitate the retrieval and display of Bitwarden's pricing.
|
||||
16
libs/pricing/jest.config.js
Normal file
16
libs/pricing/jest.config.js
Normal file
@ -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: ["<rootDir>/test.setup.ts"],
|
||||
coverageDirectory: "../../coverage/libs/pricing",
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||
prefix: "<rootDir>/../../",
|
||||
}),
|
||||
};
|
||||
21
libs/pricing/package.json
Normal file
21
libs/pricing/package.json
Normal file
@ -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
|
||||
}
|
||||
33
libs/pricing/project.json
Normal file
33
libs/pricing/project.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
<div
|
||||
class="tw-box-border tw-bg-background tw-text-main tw-border tw-border-secondary-100 tw-rounded-3xl tw-p-8 tw-shadow-sm tw-size-full tw-flex tw-flex-col"
|
||||
>
|
||||
<!-- Title Section with Active Badge -->
|
||||
<div class="tw-flex tw-items-center tw-justify-between tw-mb-2">
|
||||
<ng-content select="[slot=title]"></ng-content>
|
||||
|
||||
<!-- Active Badge (if provided) -->
|
||||
@if (activeBadge(); as activeBadgeValue) {
|
||||
<span bitBadge [variant]="activeBadgeValue.variant || 'primary'" class="tw-ml-3">
|
||||
{{ activeBadgeValue.text }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Tagline with consistent height (exactly 2 lines) -->
|
||||
<div class="tw-mb-6 tw-h-12">
|
||||
<p bitTypography="helper" class="tw-text-muted tw-m-0 tw-leading-relaxed tw-line-clamp-2">
|
||||
{{ tagline() }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Price Section (if provided) -->
|
||||
@if (price(); as priceValue) {
|
||||
<div class="tw-mb-6">
|
||||
<div class="tw-flex tw-items-baseline tw-gap-1 tw-flex-wrap">
|
||||
<span class="tw-text-3xl tw-font-bold tw-leading-none tw-m-0"
|
||||
>${{ priceValue.amount }}</span
|
||||
>
|
||||
<span bitTypography="helper" class="tw-text-muted">
|
||||
/ {{ priceValue.cadence }}
|
||||
@if (priceValue.showPerUser) {
|
||||
per user
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Button space (always reserved) -->
|
||||
<div class="tw-mb-6 tw-h-12">
|
||||
@if (button(); as buttonConfig) {
|
||||
<button
|
||||
bitButton
|
||||
[buttonType]="buttonConfig.type"
|
||||
[block]="true"
|
||||
[disabled]="buttonConfig.disabled"
|
||||
(click)="onButtonClick()"
|
||||
type="button"
|
||||
>
|
||||
@if (buttonConfig.icon?.position === "before") {
|
||||
<i class="bwi {{ buttonConfig.icon.type }} tw-me-2" aria-hidden="true"></i>
|
||||
}
|
||||
{{ buttonConfig.text }}
|
||||
@if (
|
||||
buttonConfig.icon &&
|
||||
(buttonConfig.icon.position === "after" || !buttonConfig.icon.position)
|
||||
) {
|
||||
<i class="bwi {{ buttonConfig.icon.type }} tw-ms-2" aria-hidden="true"></i>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Features List -->
|
||||
<div class="tw-flex-grow">
|
||||
@if (features(); as featureList) {
|
||||
@if (featureList.length > 0) {
|
||||
<ul class="tw-list-none tw-p-0 tw-m-0">
|
||||
@for (feature of featureList; track feature) {
|
||||
<li class="tw-flex tw-items-start tw-gap-2 tw-mb-2 last:tw-mb-0">
|
||||
<i
|
||||
class="bwi bwi-check tw-text-primary-600 tw-mt-0.5 tw-flex-shrink-0"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span bitTypography="helper" class="tw-text-muted tw-leading-relaxed">{{
|
||||
feature
|
||||
}}</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,228 @@
|
||||
import { Meta, Story, Canvas } from "@storybook/addon-docs";
|
||||
import * as PricingCardStories from "./pricing-card.component.stories";
|
||||
|
||||
<Meta of={PricingCardStories} />
|
||||
|
||||
# Pricing Card
|
||||
|
||||
A reusable UI component for displaying pricing plans with consistent styling and behavior across
|
||||
Bitwarden applications.
|
||||
|
||||
<Canvas of={PricingCardStories.Default} />
|
||||
|
||||
## 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
|
||||
<billing-pricing-card
|
||||
tagline="Everything you need for secure password management"
|
||||
[price]="{ amount: 10, cadence: 'monthly', showPerUser: true }"
|
||||
[button]="{ text: 'Choose Premium', type: 'primary' }"
|
||||
[features]="features"
|
||||
[activeBadge]="{ text: 'Active plan', show: true }"
|
||||
(buttonClick)="onPlanSelected()"
|
||||
>
|
||||
<h2 slot="title" class="tw-m-0" bitTypography="h3">Premium Plan</h2>
|
||||
</billing-pricing-card>
|
||||
```
|
||||
|
||||
## 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
|
||||
<!-- Different semantic levels -->
|
||||
<h1 slot="title" class="tw-m-0" bitTypography="h3">Main Plan</h1>
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Sub Plan</h3>
|
||||
|
||||
<!-- Custom styling -->
|
||||
<h2 slot="title" class="tw-m-0 tw-text-primary-600" bitTypography="h2">Featured Plan</h2>
|
||||
```
|
||||
|
||||
| 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:
|
||||
|
||||
<Canvas of={PricingCardStories.WithoutPrice} />
|
||||
|
||||
```html
|
||||
<billing-pricing-card
|
||||
title="Free Plan"
|
||||
tagline="Get started with essential features"
|
||||
[button]="{ text: 'Get Started', type: 'secondary' }"
|
||||
[features]="freeFeatures"
|
||||
>
|
||||
</billing-pricing-card>
|
||||
```
|
||||
|
||||
### Business Plan with Per User Pricing
|
||||
|
||||
Show business plans with "per user" text:
|
||||
|
||||
<Canvas of={PricingCardStories.LongTagline} />
|
||||
|
||||
```html
|
||||
<billing-pricing-card
|
||||
title="Business Plan"
|
||||
tagline="Advanced security and management for teams"
|
||||
[price]="{ amount: 5, cadence: 'monthly', showPerUser: true }"
|
||||
[button]="{ text: 'Start Business Trial', type: 'primary' }"
|
||||
[features]="businessFeatures"
|
||||
>
|
||||
</billing-pricing-card>
|
||||
```
|
||||
|
||||
### Annual Pricing
|
||||
|
||||
Show annual pricing with different cadence:
|
||||
|
||||
<Canvas of={PricingCardStories.Annual} />
|
||||
|
||||
```html
|
||||
<billing-pricing-card
|
||||
title="Premium Plan"
|
||||
tagline="Save with annual billing"
|
||||
[price]="{ amount: 120, cadence: 'annually' }"
|
||||
[button]="{ text: 'Choose Annual', type: 'primary' }"
|
||||
[features]="premiumFeatures"
|
||||
>
|
||||
</billing-pricing-card>
|
||||
```
|
||||
|
||||
### Configurable Heading Levels
|
||||
|
||||
For accessibility, you can configure the heading level:
|
||||
|
||||
<Canvas of={PricingCardStories.ConfigurableHeadings} />
|
||||
|
||||
```html
|
||||
<!-- In a section with h1, use h2 for plan titles -->
|
||||
<billing-pricing-card
|
||||
title="Premium Plan"
|
||||
tagline="Best value option"
|
||||
[titleLevel]="'h2'"
|
||||
[price]="{ amount: 10, cadence: 'monthly' }"
|
||||
[button]="{ text: 'Choose Plan', type: 'primary' }"
|
||||
>
|
||||
</billing-pricing-card>
|
||||
|
||||
<!-- In a subsection, use h4 for plan titles -->
|
||||
<billing-pricing-card
|
||||
title="Add-on Plan"
|
||||
tagline="Extra features"
|
||||
[titleLevel]="'h4'"
|
||||
[price]="{ amount: 5, cadence: 'monthly' }"
|
||||
[button]="{ text: 'Add to Plan', type: 'secondary' }"
|
||||
>
|
||||
</billing-pricing-card>
|
||||
```
|
||||
|
||||
### Disabled State
|
||||
|
||||
For coming soon or unavailable plans:
|
||||
|
||||
<Canvas of={PricingCardStories.Disabled} />
|
||||
|
||||
```html
|
||||
<billing-pricing-card
|
||||
title="Enterprise Plan"
|
||||
tagline="Advanced features coming soon"
|
||||
[price]="{ amount: 50, cadence: 'monthly' }"
|
||||
[button]="{ text: 'Coming Soon', type: 'secondary', disabled: true }"
|
||||
[features]="enterpriseFeatures"
|
||||
>
|
||||
</billing-pricing-card>
|
||||
```
|
||||
|
||||
### Pricing Grid Layout
|
||||
|
||||
Multiple cards displayed together:
|
||||
|
||||
<Canvas of={PricingCardStories.PricingGrid} />
|
||||
|
||||
## 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
|
||||
@ -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: `
|
||||
<billing-pricing-card
|
||||
[tagline]="tagline"
|
||||
[price]="price"
|
||||
[button]="button"
|
||||
[features]="features"
|
||||
[activeBadge]="activeBadge"
|
||||
(buttonClick)="onButtonClick()"
|
||||
>
|
||||
<ng-container [ngSwitch]="titleLevel">
|
||||
<h1 *ngSwitchCase="'h1'" slot="title" class="tw-m-0" bitTypography="h3">{{ titleText }}</h1>
|
||||
|
||||
<h2 *ngSwitchCase="'h2'" slot="title" class="tw-m-0" bitTypography="h3">{{ titleText }}</h2>
|
||||
|
||||
<h3 *ngSwitchCase="'h3'" slot="title" class="tw-m-0" bitTypography="h3">{{ titleText }}</h3>
|
||||
|
||||
<h4 *ngSwitchCase="'h4'" slot="title" class="tw-m-0" bitTypography="h3">{{ titleText }}</h4>
|
||||
|
||||
<h5 *ngSwitchCase="'h5'" slot="title" class="tw-m-0" bitTypography="h3">{{ titleText }}</h5>
|
||||
|
||||
<h6 *ngSwitchCase="'h6'" slot="title" class="tw-m-0" bitTypography="h3">{{ titleText }}</h6>
|
||||
</ng-container>
|
||||
</billing-pricing-card>
|
||||
`,
|
||||
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<PricingCardComponent>;
|
||||
let hostComponent: TestHostComponent;
|
||||
let hostFixture: ComponentFixture<TestHostComponent>;
|
||||
|
||||
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
|
||||
});
|
||||
});
|
||||
@ -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<PricingCardComponent>;
|
||||
|
||||
type Story = StoryObj<PricingCardComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<billing-pricing-card
|
||||
[tagline]="tagline"
|
||||
[price]="price"
|
||||
[button]="button"
|
||||
[features]="features">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Premium Plan</h3>
|
||||
</billing-pricing-card>
|
||||
`,
|
||||
}),
|
||||
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: `
|
||||
<billing-pricing-card
|
||||
[tagline]="tagline"
|
||||
[button]="button"
|
||||
[features]="features">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Free Plan</h3>
|
||||
</billing-pricing-card>
|
||||
`,
|
||||
}),
|
||||
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: `
|
||||
<billing-pricing-card
|
||||
[tagline]="tagline"
|
||||
[price]="price"
|
||||
[button]="button">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Enterprise Plan</h3>
|
||||
</billing-pricing-card>
|
||||
`,
|
||||
}),
|
||||
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: `
|
||||
<billing-pricing-card
|
||||
[tagline]="tagline"
|
||||
[price]="price"
|
||||
[button]="button"
|
||||
[features]="features">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Premium Plan</h3>
|
||||
</billing-pricing-card>
|
||||
`,
|
||||
}),
|
||||
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: `
|
||||
<billing-pricing-card
|
||||
[tagline]="tagline"
|
||||
[price]="price"
|
||||
[button]="button"
|
||||
[features]="features">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Coming Soon</h3>
|
||||
</billing-pricing-card>
|
||||
`,
|
||||
}),
|
||||
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: `
|
||||
<billing-pricing-card
|
||||
[tagline]="tagline"
|
||||
[price]="price"
|
||||
[button]="button"
|
||||
[features]="features">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Business Plan</h3>
|
||||
</billing-pricing-card>
|
||||
`,
|
||||
}),
|
||||
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: `
|
||||
<div class="tw-flex tw-flex-wrap tw-gap-4 tw-justify-center">
|
||||
<billing-pricing-card
|
||||
tagline="Example with primary button styling"
|
||||
[price]="{ amount: 10, cadence: 'monthly' }"
|
||||
[button]="{ text: 'Primary Action', type: 'primary' }"
|
||||
[features]="['Feature 1', 'Feature 2']">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Primary Button</h3>
|
||||
</billing-pricing-card>
|
||||
|
||||
<billing-pricing-card
|
||||
tagline="Example with secondary button styling"
|
||||
[price]="{ amount: 5, cadence: 'monthly' }"
|
||||
[button]="{ text: 'Secondary Action', type: 'secondary' }"
|
||||
[features]="['Feature 1', 'Feature 2']">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Secondary Button</h3>
|
||||
</billing-pricing-card>
|
||||
|
||||
<billing-pricing-card
|
||||
tagline="Example with danger button styling"
|
||||
[price]="{ amount: 15, cadence: 'monthly' }"
|
||||
[button]="{ text: 'Delete Plan', type: 'danger' }"
|
||||
[features]="['Feature 1', 'Feature 2']">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Danger Button</h3>
|
||||
</billing-pricing-card>
|
||||
|
||||
<billing-pricing-card
|
||||
tagline="Example with unstyled button"
|
||||
[price]="{ amount: 0, cadence: 'monthly' }"
|
||||
[button]="{ text: 'Learn More', type: 'unstyled' }"
|
||||
[features]="['Feature 1', 'Feature 2']">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Unstyled Button</h3>
|
||||
</billing-pricing-card>
|
||||
</div>
|
||||
`,
|
||||
props: {},
|
||||
}),
|
||||
};
|
||||
|
||||
export const ConfigurableHeadings: Story = {
|
||||
render: () => ({
|
||||
template: `
|
||||
<div class="tw-flex tw-flex-wrap tw-gap-6 tw-p-4 tw-justify-center">
|
||||
<billing-pricing-card
|
||||
tagline="Example with h2 heading for accessibility"
|
||||
[price]="{ amount: 10, cadence: 'monthly' }"
|
||||
[button]="{ text: 'Choose Plan', type: 'primary' }"
|
||||
[features]="['Feature 1', 'Feature 2']">
|
||||
<h2 slot="title" class="tw-m-0" bitTypography="h3">H2 Heading</h2>
|
||||
</billing-pricing-card>
|
||||
|
||||
<billing-pricing-card
|
||||
tagline="Example with h4 heading for nested content"
|
||||
[price]="{ amount: 15, cadence: 'monthly' }"
|
||||
[button]="{ text: 'Choose Plan', type: 'secondary' }"
|
||||
[features]="['Feature 1', 'Feature 2']">
|
||||
<h4 slot="title" class="tw-m-0" bitTypography="h3">H4 Heading</h4>
|
||||
</billing-pricing-card>
|
||||
</div>
|
||||
`,
|
||||
props: {},
|
||||
}),
|
||||
};
|
||||
|
||||
export const PricingGrid: Story = {
|
||||
render: () => ({
|
||||
template: `
|
||||
<div class="tw-flex tw-flex-wrap tw-gap-6 tw-p-4 tw-justify-center">
|
||||
<billing-pricing-card
|
||||
tagline="For personal use with essential features"
|
||||
[button]="{ text: 'Get Started', type: 'secondary' }"
|
||||
[features]="['Store unlimited passwords', 'Access from any device', 'Secure password generator']">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Free</h3>
|
||||
</billing-pricing-card>
|
||||
|
||||
<billing-pricing-card
|
||||
tagline="Everything you need for secure password management"
|
||||
[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']">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Premium</h3>
|
||||
</billing-pricing-card>
|
||||
|
||||
<billing-pricing-card
|
||||
tagline="Advanced security and management for teams"
|
||||
[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']">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Business</h3>
|
||||
</billing-pricing-card>
|
||||
</div>
|
||||
`,
|
||||
props: {},
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithoutButton: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<billing-pricing-card
|
||||
[tagline]="tagline"
|
||||
[price]="price"
|
||||
[features]="features">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Coming Soon Plan</h3>
|
||||
</billing-pricing-card>
|
||||
`,
|
||||
}),
|
||||
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: `
|
||||
<billing-pricing-card
|
||||
[tagline]="tagline"
|
||||
[features]="features"
|
||||
[activeBadge]="activeBadge">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Free</h3>
|
||||
</billing-pricing-card>
|
||||
`,
|
||||
}),
|
||||
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: `
|
||||
<div class="tw-flex tw-flex-wrap tw-gap-6 tw-p-4 tw-justify-center tw-items-stretch">
|
||||
<div class="tw-w-80 tw-flex">
|
||||
<billing-pricing-card
|
||||
tagline="Your current plan with essential features"
|
||||
[price]="{ amount: 0, cadence: 'monthly' }"
|
||||
[features]="['Store unlimited passwords', 'Access from any device', 'Secure password generator']"
|
||||
[activeBadge]="{ text: 'Active plan' }">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Free</h3>
|
||||
</billing-pricing-card>
|
||||
</div>
|
||||
|
||||
<div class="tw-w-80 tw-flex">
|
||||
<billing-pricing-card
|
||||
tagline="Everything you need for secure password management"
|
||||
[price]="{ amount: 10, cadence: 'monthly' }"
|
||||
[button]="{ text: 'Upgrade to Premium', type: 'primary' }"
|
||||
[features]="['Unlimited passwords and passkeys', 'Secure password sharing', 'Integrated 2FA authenticator', 'Advanced 2FA options', 'Priority customer support']">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Premium</h3>
|
||||
</billing-pricing-card>
|
||||
</div>
|
||||
|
||||
<div class="tw-w-80 tw-flex">
|
||||
<billing-pricing-card
|
||||
tagline="Advanced security and management for teams"
|
||||
[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']">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Business</h3>
|
||||
</billing-pricing-card>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
props: {},
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithButtonIcon: Story = {
|
||||
render: () => ({
|
||||
template: `
|
||||
<div class="tw-flex tw-gap-6 tw-p-4 tw-flex-wrap tw-justify-center">
|
||||
<!-- Test card with external link icon after text -->
|
||||
<billing-pricing-card
|
||||
tagline="Upgrade for advanced features"
|
||||
[price]="{ amount: 10, cadence: 'monthly' }"
|
||||
[button]="{ text: 'Upgrade Now', type: 'primary', icon: { type: 'bwi-external-link', position: 'after' } }"
|
||||
[features]="['Advanced security', 'Priority support', 'Extra storage']">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Premium</h3>
|
||||
</billing-pricing-card>
|
||||
|
||||
<!-- Test card with plus icon before text -->
|
||||
<billing-pricing-card
|
||||
tagline="Add more features to your plan"
|
||||
[price]="{ amount: 5, cadence: 'monthly', showPerUser: true }"
|
||||
[button]="{ text: 'Add Features', type: 'secondary', icon: { type: 'bwi-plus', position: 'before' } }"
|
||||
[features]="['Team management', 'Enhanced reporting', 'Custom branding']">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">Business</h3>
|
||||
</billing-pricing-card>
|
||||
</div>
|
||||
`,
|
||||
props: {},
|
||||
}),
|
||||
};
|
||||
@ -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<string>();
|
||||
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<string[]>();
|
||||
activeBadge = input<{ text: string; variant?: BadgeVariant }>();
|
||||
|
||||
@Output() buttonClick = new EventEmitter<void>();
|
||||
|
||||
/**
|
||||
* Handles button click events and emits the buttonClick event
|
||||
*/
|
||||
onButtonClick(): void {
|
||||
this.buttonClick.emit();
|
||||
}
|
||||
}
|
||||
2
libs/pricing/src/index.ts
Normal file
2
libs/pricing/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// Components
|
||||
export * from "./components/pricing-card/pricing-card.component";
|
||||
8
libs/pricing/src/pricing.spec.ts
Normal file
8
libs/pricing/src/pricing.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
28
libs/pricing/test.setup.ts
Normal file
28
libs/pricing/test.setup.ts
Normal file
@ -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: "<!DOCTYPE html>",
|
||||
});
|
||||
Object.defineProperty(document.body.style, "transform", {
|
||||
value: () => {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: webcrypto,
|
||||
});
|
||||
13
libs/pricing/tsconfig.json
Normal file
13
libs/pricing/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
libs/pricing/tsconfig.lib.json
Normal file
10
libs/pricing/tsconfig.lib.json
Normal file
@ -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"]
|
||||
}
|
||||
10
libs/pricing/tsconfig.spec.json
Normal file
10
libs/pricing/tsconfig.spec.json
Normal file
@ -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"]
|
||||
}
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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}",
|
||||
|
||||
@ -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"],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user