@burdenoff/website-sdk 2026.1014.1 → 2026.1021.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -548,15 +548,33 @@ Newsletter subscription component with rate limiting.
548
548
 
549
549
  ### PricingPage / PricingSection
550
550
 
551
- Dynamic pricing tables with API integration and billing cycle toggle.
551
+ Pricing cards driven by the live billing catalog (`publicPlans`), with a
552
+ billing cycle toggle and a currency picker.
552
553
 
553
554
  **Props:**
554
- - `productName` (required): Product name
555
- - `productId` (required): Product ID for API requests
556
- - `apiBaseUrl` (required): API base URL
555
+ - `productName`: Product name
556
+ - `productId`: the product **slug** (e.g. `"vibecontrols"`); falls back to `WebSDKConfig.productId`.
557
+ A product UUID is mapped to its slug (via `ProductProvider`, else `publicProducts`);
558
+ a UUID no public product owns is refused with an error state, never a blank grid.
557
559
  - `appLoginUrl` (required): App login URL for signup
558
560
  - `contactUrl`: Contact page URL (default: '/contact')
561
+ - `planMetadata`: per-plan copy keyed by the lowercased catalog name
562
+ (`"vibecontrols - pro (monthly)"`) or the clean name (`"pro"`)
559
563
  - `fallbackFeatures`: Fallback features when API doesn't provide them
564
+ - `defaultCurrency`: ISO code to show first; otherwise the base currency, then the
565
+ visitor's regional currency (time zone, then browser language) when the catalog sells in it
566
+ - `showCurrencySelector`: show the currency picker (default `true`; only rendered when
567
+ every plan is priced in more than one currency)
568
+
569
+ Behaviour:
570
+ - Card names drop the product prefix and billing period
571
+ (`"VibeControls - Hobby (Monthly)"` → `"Hobby"`); `planMetadata.displayName` wins.
572
+ - An empty or all-inactive catalog renders the error state and logs the product key.
573
+ - A `$0` contact-sales plan (e.g. Enterprise) shows as **Custom** and sorts last.
574
+ - A monthly-only free plan stays visible on the Yearly toggle.
575
+ - The catalog helpers (`cleanPlanDisplayName`, `planPriceIn`, `availablePlanCurrencies`,
576
+ `resolvePricingProductKey`, `isUuidLike`, `PricingProductKeyError`) are exported so a
577
+ website can pin its own `planMetadata` keys and tests to the same rules.
560
578
 
561
579
  ## License
562
580
 
@@ -1,30 +1,68 @@
1
1
  import * as react from 'react';
2
2
 
3
3
  /**
4
- * Pricing Component
5
- * GraphQL-powered pricing component with quota-based features and monthly/yearly toggle
4
+ * Pure helpers that turn the billing catalog (`publicPlans`) into what the
5
+ * pricing page shows: which product key to query, the display name of a
6
+ * plan, which currencies a visitor can pick, and the price in that currency.
6
7
  *
7
- * @example
8
- * ```tsx
9
- * import { PricingPage, PricingSection } from '@burdenoff/website-sdk/components/pricing'
8
+ * Kept free of React so every rule here is unit-tested directly
9
+ * (see `pricing-catalog.test.ts`).
10
+ */
11
+ /** True when `value` is shaped like a UUID (a product registry id, not a slug). */
12
+ declare function isUuidLike(value: string): boolean;
13
+ /** A product as returned by `publicProduct` / `publicProducts`. */
14
+ interface ProductKeyCandidate {
15
+ id: string;
16
+ slug: string;
17
+ }
18
+ /**
19
+ * Thrown when the pricing page is given a product UUID that no public product
20
+ * owns. `publicPlans(productId)` matches on the product SLUG (a plan's
21
+ * `productID` is e.g. `"bigconsole"`), so a registry UUID silently returns an
22
+ * empty list — the page must refuse it instead of rendering a blank grid.
23
+ */
24
+ declare class PricingProductKeyError extends Error {
25
+ constructor(productId: string);
26
+ }
27
+ /**
28
+ * Resolve the key `publicPlans` expects.
10
29
  *
11
- * // Full page with hero
12
- * <PricingPage
13
- * productName="BigConsole"
14
- * productId="bigconsole-123"
15
- * appLoginUrl="https://app.bigconsole.com/login"
16
- * contactUrl="/contact"
17
- * fallbackFeatures={{
18
- * basic: ['Up to 5 dashboards', '10 data sources'],
19
- * professional: ['Unlimited dashboards', 'API access'],
20
- * enterprise: ['Everything in Professional', 'Dedicated support']
21
- * }}
22
- * />
30
+ * - A slug is returned unchanged (trimmed).
31
+ * - A UUID is mapped to its product's slug using `candidates` (the product
32
+ * from `ProductProvider` and/or the `publicProducts` list).
33
+ * - A UUID with no matching candidate returns `null`: the caller must look it
34
+ * up or fail with {@link PricingProductKeyError}.
35
+ */
36
+ declare function resolvePricingProductKey(productId: string, candidates: ReadonlyArray<ProductKeyCandidate | null | undefined>): string | null;
37
+ /**
38
+ * Catalog plan names carry the product and the billing period, e.g.
39
+ * `"VibeControls - Hobby (Monthly)"`. The card already sits on the product's
40
+ * own site and the period is chosen by the Monthly/Yearly toggle, so the
41
+ * visitor sees `"Hobby"`.
23
42
  *
24
- * // Embeddable section
25
- * <PricingSection {...props} id="pricing" className="bg-muted/30" />
26
- * ```
43
+ * Only the FIRST `" - "` separates the product, so `"Acme - Pro - Teams"`
44
+ * becomes `"Pro - Teams"`. A name that would end up empty is returned as is.
45
+ */
46
+ declare function cleanPlanDisplayName(name: string): string;
47
+ /** The price-bearing fields of a catalog plan. */
48
+ interface PricedPlan {
49
+ price: number;
50
+ currency: string;
51
+ currencyPrices?: Record<string, number> | null;
52
+ }
53
+ /**
54
+ * The price of `plan` in `currency`, or `null` when the catalog has no price
55
+ * for it. A free plan costs 0 in every currency.
56
+ */
57
+ declare function planPriceIn(plan: PricedPlan, currency: string): number | null;
58
+ /**
59
+ * The currencies a visitor can switch to: those in which EVERY plan has a
60
+ * price (free plans count as priced everywhere). A currency missing from a
61
+ * single paid plan is left out, so switching never blanks a card. Sorted with
62
+ * the catalog's base currency first, then alphabetically.
27
63
  */
64
+ declare function availablePlanCurrencies(plans: ReadonlyArray<PricedPlan>): string[];
65
+
28
66
  interface PlanCard {
29
67
  id: string;
30
68
  name: string;
@@ -91,6 +129,18 @@ interface PricingProps {
91
129
  * Billing interval display text
92
130
  */
93
131
  billingIntervals?: Record<string, string>;
132
+ /**
133
+ * Currency to show first (ISO 4217, e.g. "INR"). When omitted the page shows
134
+ * the catalog's base currency until the browser reveals the visitor's
135
+ * region, then switches to that region's currency if the catalog sells in it.
136
+ */
137
+ defaultCurrency?: string;
138
+ /**
139
+ * Show the currency picker when the catalog prices every plan in more than
140
+ * one currency.
141
+ * @default true
142
+ */
143
+ showCurrencySelector?: boolean;
94
144
  /**
95
145
  * Plan metadata (tier info, CTA text, etc.)
96
146
  */
@@ -281,4 +331,4 @@ declare function PricingSection(props: PricingProps): react.JSX.Element;
281
331
  */
282
332
  declare function PricingPage(props: PricingProps): react.JSX.Element;
283
333
 
284
- export { type PlanCard, PricingPage, type PricingProps, PricingSection, resolveEnterprisePlans, splitPlansByAudience, transformFeatureToLabel };
334
+ export { type PlanCard, PricingPage, PricingProductKeyError, type PricingProps, PricingSection, availablePlanCurrencies, cleanPlanDisplayName, isUuidLike, planPriceIn, resolveEnterprisePlans, resolvePricingProductKey, splitPlansByAudience, transformFeatureToLabel };
@@ -1,30 +1,68 @@
1
1
  import * as react from 'react';
2
2
 
3
3
  /**
4
- * Pricing Component
5
- * GraphQL-powered pricing component with quota-based features and monthly/yearly toggle
4
+ * Pure helpers that turn the billing catalog (`publicPlans`) into what the
5
+ * pricing page shows: which product key to query, the display name of a
6
+ * plan, which currencies a visitor can pick, and the price in that currency.
6
7
  *
7
- * @example
8
- * ```tsx
9
- * import { PricingPage, PricingSection } from '@burdenoff/website-sdk/components/pricing'
8
+ * Kept free of React so every rule here is unit-tested directly
9
+ * (see `pricing-catalog.test.ts`).
10
+ */
11
+ /** True when `value` is shaped like a UUID (a product registry id, not a slug). */
12
+ declare function isUuidLike(value: string): boolean;
13
+ /** A product as returned by `publicProduct` / `publicProducts`. */
14
+ interface ProductKeyCandidate {
15
+ id: string;
16
+ slug: string;
17
+ }
18
+ /**
19
+ * Thrown when the pricing page is given a product UUID that no public product
20
+ * owns. `publicPlans(productId)` matches on the product SLUG (a plan's
21
+ * `productID` is e.g. `"bigconsole"`), so a registry UUID silently returns an
22
+ * empty list — the page must refuse it instead of rendering a blank grid.
23
+ */
24
+ declare class PricingProductKeyError extends Error {
25
+ constructor(productId: string);
26
+ }
27
+ /**
28
+ * Resolve the key `publicPlans` expects.
10
29
  *
11
- * // Full page with hero
12
- * <PricingPage
13
- * productName="BigConsole"
14
- * productId="bigconsole-123"
15
- * appLoginUrl="https://app.bigconsole.com/login"
16
- * contactUrl="/contact"
17
- * fallbackFeatures={{
18
- * basic: ['Up to 5 dashboards', '10 data sources'],
19
- * professional: ['Unlimited dashboards', 'API access'],
20
- * enterprise: ['Everything in Professional', 'Dedicated support']
21
- * }}
22
- * />
30
+ * - A slug is returned unchanged (trimmed).
31
+ * - A UUID is mapped to its product's slug using `candidates` (the product
32
+ * from `ProductProvider` and/or the `publicProducts` list).
33
+ * - A UUID with no matching candidate returns `null`: the caller must look it
34
+ * up or fail with {@link PricingProductKeyError}.
35
+ */
36
+ declare function resolvePricingProductKey(productId: string, candidates: ReadonlyArray<ProductKeyCandidate | null | undefined>): string | null;
37
+ /**
38
+ * Catalog plan names carry the product and the billing period, e.g.
39
+ * `"VibeControls - Hobby (Monthly)"`. The card already sits on the product's
40
+ * own site and the period is chosen by the Monthly/Yearly toggle, so the
41
+ * visitor sees `"Hobby"`.
23
42
  *
24
- * // Embeddable section
25
- * <PricingSection {...props} id="pricing" className="bg-muted/30" />
26
- * ```
43
+ * Only the FIRST `" - "` separates the product, so `"Acme - Pro - Teams"`
44
+ * becomes `"Pro - Teams"`. A name that would end up empty is returned as is.
45
+ */
46
+ declare function cleanPlanDisplayName(name: string): string;
47
+ /** The price-bearing fields of a catalog plan. */
48
+ interface PricedPlan {
49
+ price: number;
50
+ currency: string;
51
+ currencyPrices?: Record<string, number> | null;
52
+ }
53
+ /**
54
+ * The price of `plan` in `currency`, or `null` when the catalog has no price
55
+ * for it. A free plan costs 0 in every currency.
56
+ */
57
+ declare function planPriceIn(plan: PricedPlan, currency: string): number | null;
58
+ /**
59
+ * The currencies a visitor can switch to: those in which EVERY plan has a
60
+ * price (free plans count as priced everywhere). A currency missing from a
61
+ * single paid plan is left out, so switching never blanks a card. Sorted with
62
+ * the catalog's base currency first, then alphabetically.
27
63
  */
64
+ declare function availablePlanCurrencies(plans: ReadonlyArray<PricedPlan>): string[];
65
+
28
66
  interface PlanCard {
29
67
  id: string;
30
68
  name: string;
@@ -91,6 +129,18 @@ interface PricingProps {
91
129
  * Billing interval display text
92
130
  */
93
131
  billingIntervals?: Record<string, string>;
132
+ /**
133
+ * Currency to show first (ISO 4217, e.g. "INR"). When omitted the page shows
134
+ * the catalog's base currency until the browser reveals the visitor's
135
+ * region, then switches to that region's currency if the catalog sells in it.
136
+ */
137
+ defaultCurrency?: string;
138
+ /**
139
+ * Show the currency picker when the catalog prices every plan in more than
140
+ * one currency.
141
+ * @default true
142
+ */
143
+ showCurrencySelector?: boolean;
94
144
  /**
95
145
  * Plan metadata (tier info, CTA text, etc.)
96
146
  */
@@ -281,4 +331,4 @@ declare function PricingSection(props: PricingProps): react.JSX.Element;
281
331
  */
282
332
  declare function PricingPage(props: PricingProps): react.JSX.Element;
283
333
 
284
- export { type PlanCard, PricingPage, type PricingProps, PricingSection, resolveEnterprisePlans, splitPlansByAudience, transformFeatureToLabel };
334
+ export { type PlanCard, PricingPage, PricingProductKeyError, type PricingProps, PricingSection, availablePlanCurrencies, cleanPlanDisplayName, isUuidLike, planPriceIn, resolveEnterprisePlans, resolvePricingProductKey, splitPlansByAudience, transformFeatureToLabel };