@cimplify/sdk 0.84.0 → 0.86.0

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.
Files changed (70) hide show
  1. package/dist/advanced.d.mts +3 -3
  2. package/dist/advanced.d.ts +3 -3
  3. package/dist/advanced.js +22 -22
  4. package/dist/advanced.mjs +3 -3
  5. package/dist/{chunk-I6RE4BKH.mjs → chunk-A2GXDCSI.mjs} +2 -2
  6. package/dist/{chunk-XA3ZNR75.js → chunk-AETVRBKI.js} +1 -1
  7. package/dist/{chunk-PLTP6HWW.mjs → chunk-D6JWYYTG.mjs} +20 -20
  8. package/dist/{chunk-ODEPW244.js → chunk-ESGKWRQS.js} +4 -4
  9. package/dist/{chunk-GFM5JW45.js → chunk-HJEKXCG2.js} +62 -62
  10. package/dist/{chunk-AMZXALF6.mjs → chunk-K5EN2W2Q.mjs} +1 -1
  11. package/dist/{chunk-TD3AY34U.mjs → chunk-KFLGQGMC.mjs} +3 -3
  12. package/dist/{chunk-NWUEWUX2.mjs → chunk-MRL53WP6.mjs} +2 -2
  13. package/dist/{chunk-7BM56JET.mjs → chunk-NN3WC7XZ.mjs} +3 -3
  14. package/dist/{chunk-7Y2O3E4D.js → chunk-PTPOQVXS.js} +5 -5
  15. package/dist/{chunk-HPN5R3K2.js → chunk-VUG4HTUM.js} +23 -23
  16. package/dist/{chunk-FKBXZ2A6.js → chunk-X4UNISYG.js} +22 -22
  17. package/dist/{client-CINi3Dw3.d.ts → client-BQSRj4iK.d.mts} +528 -539
  18. package/dist/{client-Ba4Hng9i.d.mts → client-BdI24jUT.d.ts} +528 -539
  19. package/dist/{client-D8AjO8jb.d.ts → client-CMJO_8zo.d.ts} +2 -2
  20. package/dist/{client-BjUJapti.d.mts → client-DowGNrTh.d.mts} +2 -2
  21. package/dist/{index-DCPxHjbj.d.ts → index-BRwHpMY-.d.ts} +3 -3
  22. package/dist/{index-9Bl-zBAI.d.mts → index-DLb_Ldl6.d.mts} +3 -3
  23. package/dist/index.d.mts +6 -6
  24. package/dist/index.d.ts +6 -6
  25. package/dist/index.js +117 -117
  26. package/dist/index.mjs +5 -5
  27. package/dist/mock/cli.mjs +659 -355
  28. package/dist/mock/library.js +20 -20
  29. package/dist/mock/library.mjs +20 -20
  30. package/dist/mock/msw.js +20 -20
  31. package/dist/mock/msw.mjs +20 -20
  32. package/dist/{payment-BVv9leS_.d.ts → payment-BA7KNBxg.d.ts} +3 -3
  33. package/dist/{payment-D61iGd0o.d.mts → payment-CFev-fOE.d.mts} +3 -3
  34. package/dist/{price-BYWCHYO5.d.ts → price-BNhGVB5q.d.ts} +3 -1
  35. package/dist/{price-Cthq6nPb.d.mts → price-nRxqVNGq.d.mts} +3 -1
  36. package/dist/product-BGxIfTqJ.d.mts +5400 -0
  37. package/dist/product-BGxIfTqJ.d.ts +5400 -0
  38. package/dist/react.d.mts +22 -7
  39. package/dist/react.d.ts +22 -7
  40. package/dist/react.js +177 -155
  41. package/dist/react.mjs +57 -36
  42. package/dist/{server-BjQ0xWmc.d.mts → server-9u0LiG5U.d.mts} +1 -1
  43. package/dist/{server-DDaQIYLa.d.ts → server-BH7DTaaz.d.ts} +1 -1
  44. package/dist/server.d.mts +4 -4
  45. package/dist/server.d.ts +4 -4
  46. package/dist/server.js +10 -10
  47. package/dist/server.mjs +4 -4
  48. package/dist/styles.css +1 -1
  49. package/dist/testing/msw.d.mts +2 -2
  50. package/dist/testing/msw.d.ts +2 -2
  51. package/dist/testing/msw.js +3 -3
  52. package/dist/testing/msw.mjs +2 -2
  53. package/dist/testing/suite.d.mts +6 -6
  54. package/dist/testing/suite.d.ts +6 -6
  55. package/dist/testing/suite.js +25 -25
  56. package/dist/testing/suite.mjs +6 -6
  57. package/dist/testing.d.mts +5 -5
  58. package/dist/testing.d.ts +5 -5
  59. package/dist/testing.js +81 -81
  60. package/dist/testing.mjs +7 -7
  61. package/dist/utils.d.mts +4 -4
  62. package/dist/utils.d.ts +4 -4
  63. package/dist/utils.js +29 -29
  64. package/dist/utils.mjs +2 -2
  65. package/package.json +25 -22
  66. package/registry/currency-selector.json +1 -1
  67. package/registry/digital-product-card.json +1 -1
  68. package/registry/digital-product-layout.json +1 -1
  69. package/dist/product-2MAoE0kZ.d.mts +0 -1570
  70. package/dist/product-2MAoE0kZ.d.ts +0 -1570
package/dist/utils.js CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkL5GKVQVJ_js = require('./chunk-L5GKVQVJ.js');
4
- var chunk7Y2O3E4D_js = require('./chunk-7Y2O3E4D.js');
5
- require('./chunk-XA3ZNR75.js');
4
+ var chunkPTPOQVXS_js = require('./chunk-PTPOQVXS.js');
5
+ require('./chunk-AETVRBKI.js');
6
6
  require('./chunk-OWW5GUSB.js');
7
7
 
8
8
 
@@ -21,109 +21,109 @@ Object.defineProperty(exports, "resolvePhoneCountryCode", {
21
21
  });
22
22
  Object.defineProperty(exports, "CURRENCY_SYMBOLS", {
23
23
  enumerable: true,
24
- get: function () { return chunk7Y2O3E4D_js.CURRENCY_SYMBOLS; }
24
+ get: function () { return chunkPTPOQVXS_js.CURRENCY_SYMBOLS; }
25
25
  });
26
26
  Object.defineProperty(exports, "MOBILE_MONEY_PROVIDERS", {
27
27
  enumerable: true,
28
- get: function () { return chunk7Y2O3E4D_js.MOBILE_MONEY_PROVIDERS; }
28
+ get: function () { return chunkPTPOQVXS_js.MOBILE_MONEY_PROVIDERS; }
29
29
  });
30
30
  Object.defineProperty(exports, "categorizePaymentError", {
31
31
  enumerable: true,
32
- get: function () { return chunk7Y2O3E4D_js.categorizePaymentError; }
32
+ get: function () { return chunkPTPOQVXS_js.categorizePaymentError; }
33
33
  });
34
34
  Object.defineProperty(exports, "detectMobileMoneyProvider", {
35
35
  enumerable: true,
36
- get: function () { return chunk7Y2O3E4D_js.detectMobileMoneyProvider; }
36
+ get: function () { return chunkPTPOQVXS_js.detectMobileMoneyProvider; }
37
37
  });
38
38
  Object.defineProperty(exports, "formatMoney", {
39
39
  enumerable: true,
40
- get: function () { return chunk7Y2O3E4D_js.formatMoney; }
40
+ get: function () { return chunkPTPOQVXS_js.formatMoney; }
41
41
  });
42
42
  Object.defineProperty(exports, "formatNumberCompact", {
43
43
  enumerable: true,
44
- get: function () { return chunk7Y2O3E4D_js.formatNumberCompact; }
44
+ get: function () { return chunkPTPOQVXS_js.formatNumberCompact; }
45
45
  });
46
46
  Object.defineProperty(exports, "formatPrice", {
47
47
  enumerable: true,
48
- get: function () { return chunk7Y2O3E4D_js.formatPrice; }
48
+ get: function () { return chunkPTPOQVXS_js.formatPrice; }
49
49
  });
50
50
  Object.defineProperty(exports, "formatPriceAdjustment", {
51
51
  enumerable: true,
52
- get: function () { return chunk7Y2O3E4D_js.formatPriceAdjustment; }
52
+ get: function () { return chunkPTPOQVXS_js.formatPriceAdjustment; }
53
53
  });
54
54
  Object.defineProperty(exports, "formatPriceCompact", {
55
55
  enumerable: true,
56
- get: function () { return chunk7Y2O3E4D_js.formatPriceCompact; }
56
+ get: function () { return chunkPTPOQVXS_js.formatPriceCompact; }
57
57
  });
58
58
  Object.defineProperty(exports, "formatPriceWithTax", {
59
59
  enumerable: true,
60
- get: function () { return chunk7Y2O3E4D_js.formatPriceWithTax; }
60
+ get: function () { return chunkPTPOQVXS_js.formatPriceWithTax; }
61
61
  });
62
62
  Object.defineProperty(exports, "formatProductPrice", {
63
63
  enumerable: true,
64
- get: function () { return chunk7Y2O3E4D_js.formatProductPrice; }
64
+ get: function () { return chunkPTPOQVXS_js.formatProductPrice; }
65
65
  });
66
66
  Object.defineProperty(exports, "getBasePrice", {
67
67
  enumerable: true,
68
- get: function () { return chunk7Y2O3E4D_js.getBasePrice; }
68
+ get: function () { return chunkPTPOQVXS_js.getBasePrice; }
69
69
  });
70
70
  Object.defineProperty(exports, "getCurrencySymbol", {
71
71
  enumerable: true,
72
- get: function () { return chunk7Y2O3E4D_js.getCurrencySymbol; }
72
+ get: function () { return chunkPTPOQVXS_js.getCurrencySymbol; }
73
73
  });
74
74
  Object.defineProperty(exports, "getDiscountPercentage", {
75
75
  enumerable: true,
76
- get: function () { return chunk7Y2O3E4D_js.getDiscountPercentage; }
76
+ get: function () { return chunkPTPOQVXS_js.getDiscountPercentage; }
77
77
  });
78
78
  Object.defineProperty(exports, "getDisplayPrice", {
79
79
  enumerable: true,
80
- get: function () { return chunk7Y2O3E4D_js.getDisplayPrice; }
80
+ get: function () { return chunkPTPOQVXS_js.getDisplayPrice; }
81
81
  });
82
82
  Object.defineProperty(exports, "getMarkupPercentage", {
83
83
  enumerable: true,
84
- get: function () { return chunk7Y2O3E4D_js.getMarkupPercentage; }
84
+ get: function () { return chunkPTPOQVXS_js.getMarkupPercentage; }
85
85
  });
86
86
  Object.defineProperty(exports, "getProductCurrency", {
87
87
  enumerable: true,
88
- get: function () { return chunk7Y2O3E4D_js.getProductCurrency; }
88
+ get: function () { return chunkPTPOQVXS_js.getProductCurrency; }
89
89
  });
90
90
  Object.defineProperty(exports, "getTaxAmount", {
91
91
  enumerable: true,
92
- get: function () { return chunk7Y2O3E4D_js.getTaxAmount; }
92
+ get: function () { return chunkPTPOQVXS_js.getTaxAmount; }
93
93
  });
94
94
  Object.defineProperty(exports, "hasTaxInfo", {
95
95
  enumerable: true,
96
- get: function () { return chunk7Y2O3E4D_js.hasTaxInfo; }
96
+ get: function () { return chunkPTPOQVXS_js.hasTaxInfo; }
97
97
  });
98
98
  Object.defineProperty(exports, "isOnSale", {
99
99
  enumerable: true,
100
- get: function () { return chunk7Y2O3E4D_js.isOnSale; }
100
+ get: function () { return chunkPTPOQVXS_js.isOnSale; }
101
101
  });
102
102
  Object.defineProperty(exports, "isPaymentStatusFailure", {
103
103
  enumerable: true,
104
- get: function () { return chunk7Y2O3E4D_js.isPaymentStatusFailure; }
104
+ get: function () { return chunkPTPOQVXS_js.isPaymentStatusFailure; }
105
105
  });
106
106
  Object.defineProperty(exports, "isPaymentStatusRequiresAction", {
107
107
  enumerable: true,
108
- get: function () { return chunk7Y2O3E4D_js.isPaymentStatusRequiresAction; }
108
+ get: function () { return chunkPTPOQVXS_js.isPaymentStatusRequiresAction; }
109
109
  });
110
110
  Object.defineProperty(exports, "isPaymentStatusSuccess", {
111
111
  enumerable: true,
112
- get: function () { return chunk7Y2O3E4D_js.isPaymentStatusSuccess; }
112
+ get: function () { return chunkPTPOQVXS_js.isPaymentStatusSuccess; }
113
113
  });
114
114
  Object.defineProperty(exports, "isTaxInclusive", {
115
115
  enumerable: true,
116
- get: function () { return chunk7Y2O3E4D_js.isTaxInclusive; }
116
+ get: function () { return chunkPTPOQVXS_js.isTaxInclusive; }
117
117
  });
118
118
  Object.defineProperty(exports, "normalizePaymentResponse", {
119
119
  enumerable: true,
120
- get: function () { return chunk7Y2O3E4D_js.normalizePaymentResponse; }
120
+ get: function () { return chunkPTPOQVXS_js.normalizePaymentResponse; }
121
121
  });
122
122
  Object.defineProperty(exports, "normalizeStatusResponse", {
123
123
  enumerable: true,
124
- get: function () { return chunk7Y2O3E4D_js.normalizeStatusResponse; }
124
+ get: function () { return chunkPTPOQVXS_js.normalizeStatusResponse; }
125
125
  });
126
126
  Object.defineProperty(exports, "parsePrice", {
127
127
  enumerable: true,
128
- get: function () { return chunk7Y2O3E4D_js.parsePrice; }
128
+ get: function () { return chunkPTPOQVXS_js.parsePrice; }
129
129
  });
package/dist/utils.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  export { isValidPhoneForContext, normalizePhoneToE164, resolvePhoneCountryCode } from './chunk-YOIVZ6OA.mjs';
2
- export { CURRENCY_SYMBOLS, MOBILE_MONEY_PROVIDERS, categorizePaymentError, detectMobileMoneyProvider, formatMoney, formatNumberCompact, formatPrice, formatPriceAdjustment, formatPriceCompact, formatPriceWithTax, formatProductPrice, getBasePrice, getCurrencySymbol, getDiscountPercentage, getDisplayPrice, getMarkupPercentage, getProductCurrency, getTaxAmount, hasTaxInfo, isOnSale, isPaymentStatusFailure, isPaymentStatusRequiresAction, isPaymentStatusSuccess, isTaxInclusive, normalizePaymentResponse, normalizeStatusResponse, parsePrice } from './chunk-TD3AY34U.mjs';
3
- import './chunk-AMZXALF6.mjs';
2
+ export { CURRENCY_SYMBOLS, MOBILE_MONEY_PROVIDERS, categorizePaymentError, detectMobileMoneyProvider, formatMoney, formatNumberCompact, formatPrice, formatPriceAdjustment, formatPriceCompact, formatPriceWithTax, formatProductPrice, getBasePrice, getCurrencySymbol, getDiscountPercentage, getDisplayPrice, getMarkupPercentage, getProductCurrency, getTaxAmount, hasTaxInfo, isOnSale, isPaymentStatusFailure, isPaymentStatusRequiresAction, isPaymentStatusSuccess, isTaxInclusive, normalizePaymentResponse, normalizeStatusResponse, parsePrice } from './chunk-KFLGQGMC.mjs';
3
+ import './chunk-K5EN2W2Q.mjs';
4
4
  import './chunk-3G6RQLXK.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/sdk",
3
- "version": "0.84.0",
3
+ "version": "0.86.0",
4
4
  "description": "Cimplify Commerce SDK for storefronts",
5
5
  "keywords": [
6
6
  "cimplify",
@@ -107,10 +107,12 @@
107
107
  "contracts:diff": "bun scripts/diff-contracts.ts",
108
108
  "contracts:smoke": "bun scripts/diff-contracts.ts --smoke",
109
109
  "contracts": "bun run contracts:emit && bun run contracts:diff",
110
- "schema:add": "bun scripts/schema-add.ts"
110
+ "schema:add": "bun scripts/schema-add.ts",
111
+ "gen:api": "openapi-typescript ../../contracts/storefront-openapi.json -o src/types/generated.ts",
112
+ "gen:api:check": "bun run gen:api && git diff --exit-code src/types/generated.ts"
111
113
  },
112
114
  "peerDependencies": {
113
- "@paystack/inline-js": "^2.22.8",
115
+ "@paystack/inline-js": "^2.23.0",
114
116
  "react": ">=17.0.0",
115
117
  "msw": ">=2.0.0",
116
118
  "vitest": ">=2.0.0",
@@ -134,35 +136,36 @@
134
136
  }
135
137
  },
136
138
  "devDependencies": {
137
- "@cloudflare/workers-types": "^4.20260503.1",
138
- "@hono/node-server": "^1.13.7",
139
- "@opennextjs/aws": "^3.7.10",
139
+ "@cloudflare/workers-types": "^4.20260616.1",
140
+ "@hono/node-server": "^2.0.5",
141
+ "@opennextjs/aws": "^4.0.3",
140
142
  "@opennextjs/cloudflare": "^1.19.11",
141
- "@tailwindcss/cli": "4.2.2",
143
+ "@tailwindcss/cli": "4.3.1",
142
144
  "@testing-library/dom": "^10.4.1",
143
145
  "@testing-library/react": "^16.3.2",
144
- "@types/node": "^25.6.0",
146
+ "@types/node": "^25.9.3",
145
147
  "@types/react": "^19.2.14",
146
- "@types/react-dom": "^19",
147
- "@typescript/native-preview": "^7.0.0-dev.20260503.1",
148
- "hono": "^4.7.7",
148
+ "@types/react-dom": "^19.2.3",
149
+ "@typescript/native-preview": "^7.0.0-dev.20260616.1",
150
+ "hono": "^4.12.25",
149
151
  "jsdom": "^29.1.1",
150
- "msw": "^2.14.2",
151
- "react": "^19.2.5",
152
- "react-dom": "^19.2.5",
153
- "tailwindcss": "4.2.2",
152
+ "msw": "^2.14.6",
153
+ "openapi-typescript": "^7.13.0",
154
+ "react": "^19.2.7",
155
+ "react-dom": "^19.2.7",
156
+ "tailwindcss": "4.3.1",
154
157
  "tsup": "^8.5.1",
155
- "typescript": "5.9.3",
156
- "vitest": "^4.1.5"
158
+ "typescript": "6.0.3",
159
+ "vitest": "^4.1.9"
157
160
  },
158
161
  "dependencies": {
159
- "@base-ui/react": "^1.4.1",
162
+ "@base-ui/react": "^1.5.0",
160
163
  "clsx": "^2.1.1",
161
- "date-fns": "^4.1.0",
164
+ "date-fns": "^4.4.0",
162
165
  "jose": "^6.2.3",
163
- "libphonenumber-js": "1.12.41",
164
- "react-day-picker": "^9.14.0",
165
- "tailwind-merge": "^3.5.0",
166
+ "libphonenumber-js": "1.13.6",
167
+ "react-day-picker": "^10.0.1",
168
+ "tailwind-merge": "^3.6.0",
166
169
  "xss": "^1.0.15",
167
170
  "zod": "^4.4.3"
168
171
  }
@@ -9,7 +9,7 @@
9
9
  "files": [
10
10
  {
11
11
  "path": "currency-selector.tsx",
12
- "content": "\"use client\";\n\nimport React, { useState, useEffect, useRef } from \"react\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport type { CimplifyError } from \"@cimplify/sdk\";\nimport type { FxRateResponse } from \"../types/fx\";\nimport { useOptionalCimplifyClient } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface CurrencySelectorClassNames {\n root?: string;\n option?: string;\n activeOption?: string;\n rate?: string;\n label?: string;\n loading?: string;\n}\n\nexport interface CurrencySelectorProps {\n /** Available currencies to display. */\n currencies: CurrencyCode[];\n /** Currently selected currency. */\n currentCurrency?: CurrencyCode;\n /** Called when a currency is selected. */\n onCurrencyChange?: (currency: CurrencyCode) => void;\n /** Show the exchange rate next to each currency. Requires `baseCurrency`. */\n showRate?: boolean;\n /** Base currency used for rate display. */\n baseCurrency?: CurrencyCode;\n /** Custom currency option renderer. */\n renderCurrency?: (\n currency: CurrencyCode,\n isActive: boolean,\n rate: FxRateResponse | null,\n ) => React.ReactNode;\n className?: string;\n classNames?: CurrencySelectorClassNames;\n}\n\n/**\n * CurrencySelector — currency switcher for international customers.\n *\n * Renders selectable currency options with optional exchange rate display.\n * Returns `null` when there is only one currency (no selector needed).\n */\nexport function CurrencySelector({\n currencies,\n currentCurrency,\n onCurrencyChange,\n showRate = false,\n baseCurrency,\n renderCurrency,\n className,\n classNames,\n}: CurrencySelectorProps): React.ReactElement | null {\n const context = useOptionalCimplifyClient();\n const client = context?.client ?? null;\n\n const [rates, setRates] = useState<Record<string, FxRateResponse>>({});\n const [isLoadingRates, setIsLoadingRates] = useState(false);\n const requestIdRef = useRef(0);\n\n useEffect(() => {\n if (!showRate || !baseCurrency || !client) return;\n\n const targets = currencies.filter((c) => c !== baseCurrency);\n if (targets.length === 0) return;\n\n const nextRequestId = ++requestIdRef.current;\n setIsLoadingRates(true);\n\n void (async () => {\n const fetched: Record<string, FxRateResponse> = {};\n\n await Promise.all(\n targets.map(async (currency) => {\n const res = await client.fx.getRate(baseCurrency, currency);\n if (res.ok) {\n fetched[currency] = res.value;\n }\n }),\n );\n\n if (nextRequestId !== requestIdRef.current) return;\n\n setRates(fetched);\n setIsLoadingRates(false);\n })();\n }, [client, showRate, baseCurrency, currencies]);\n\n if (currencies.length <= 1) {\n return null;\n }\n\n return (\n <div data-cimplify-currency-selector className={cn(className, classNames?.root)}>\n {currencies.map((currency) => {\n const isActive = currentCurrency === currency;\n const rate = rates[currency] ?? null;\n\n return (\n <button\n key={currency}\n type=\"button\"\n onClick={() => onCurrencyChange?.(currency)}\n data-cimplify-currency-option\n data-active={isActive || undefined}\n aria-pressed={isActive}\n className={cn(\n classNames?.option,\n isActive && classNames?.activeOption,\n )}\n >\n {renderCurrency ? (\n renderCurrency(currency, isActive, rate)\n ) : (\n <>\n <span data-cimplify-currency-label className={classNames?.label}>\n {currency}\n </span>\n {showRate && baseCurrency && currency !== baseCurrency && (\n <span data-cimplify-currency-rate className={classNames?.rate}>\n {isLoadingRates\n ? \"...\"\n : rate\n ? `1 ${baseCurrency} = ${rate.rate.toFixed(4)} ${currency}`\n : null}\n </span>\n )}\n </>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"
12
+ "content": "\"use client\";\n\nimport React, { useState, useEffect, useRef } from \"react\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport type { CimplifyError } from \"@cimplify/sdk\";\nimport type { FxRateResponse } from \"../types/fx\";\nimport { useOptionalCimplifyClient } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface CurrencySelectorClassNames {\n root?: string;\n option?: string;\n activeOption?: string;\n rate?: string;\n label?: string;\n loading?: string;\n}\n\nexport interface CurrencySelectorProps {\n /** Available currencies to display. */\n currencies: CurrencyCode[];\n /** Currently selected currency. */\n currentCurrency?: CurrencyCode;\n /** Called when a currency is selected. */\n onCurrencyChange?: (currency: CurrencyCode) => void;\n /** Show the exchange rate next to each currency. Requires `baseCurrency`. */\n showRate?: boolean;\n /** Base currency used for rate display. */\n baseCurrency?: CurrencyCode;\n /** Custom currency option renderer. */\n renderCurrency?: (\n currency: CurrencyCode,\n isActive: boolean,\n rate: FxRateResponse | null,\n ) => React.ReactNode;\n className?: string;\n classNames?: CurrencySelectorClassNames;\n}\n\n/**\n * CurrencySelector — currency switcher for international customers.\n *\n * Renders selectable currency options with optional exchange rate display.\n * Returns `null` when there is only one currency (no selector needed).\n */\nexport function CurrencySelector({\n currencies,\n currentCurrency,\n onCurrencyChange,\n showRate = false,\n baseCurrency,\n renderCurrency,\n className,\n classNames,\n}: CurrencySelectorProps): React.ReactElement | null {\n const context = useOptionalCimplifyClient();\n const client = context?.client ?? null;\n\n const [rates, setRates] = useState<Record<string, FxRateResponse>>({});\n const [isLoadingRates, setIsLoadingRates] = useState(false);\n const requestIdRef = useRef(0);\n\n useEffect(() => {\n if (!showRate || !baseCurrency || !client) return;\n\n const targets = currencies.filter((c) => c !== baseCurrency);\n if (targets.length === 0) return;\n\n const nextRequestId = ++requestIdRef.current;\n setIsLoadingRates(true);\n\n void (async () => {\n const fetched: Record<string, FxRateResponse> = {};\n\n await Promise.all(\n targets.map(async (currency) => {\n const res = await client.fx.getRate(baseCurrency, currency);\n if (res.ok) {\n fetched[currency] = res.value;\n }\n }),\n );\n\n if (nextRequestId !== requestIdRef.current) return;\n\n setRates(fetched);\n setIsLoadingRates(false);\n })();\n }, [client, showRate, baseCurrency, currencies]);\n\n if (currencies.length <= 1) {\n return null;\n }\n\n return (\n <div data-cimplify-currency-selector className={cn(className, classNames?.root)}>\n {currencies.map((currency) => {\n const isActive = currentCurrency === currency;\n const rate = rates[currency] ?? null;\n\n return (\n <button\n key={currency}\n type=\"button\"\n onClick={() => onCurrencyChange?.(currency)}\n data-cimplify-currency-option\n data-active={isActive || undefined}\n aria-pressed={isActive}\n className={cn(\n classNames?.option,\n isActive && classNames?.activeOption,\n )}\n >\n {renderCurrency ? (\n renderCurrency(currency, isActive, rate)\n ) : (\n <>\n <span data-cimplify-currency-label className={classNames?.label}>\n {currency}\n </span>\n {showRate && baseCurrency && currency !== baseCurrency && (\n <span data-cimplify-currency-rate className={classNames?.rate}>\n {isLoadingRates\n ? \"...\"\n : rate\n ? `1 ${baseCurrency} = ${Number(rate.rate).toFixed(4)} ${currency}`\n : null}\n </span>\n )}\n </>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"
13
13
  }
14
14
  ]
15
15
  }
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "cards/digital-product-card.tsx",
14
- "content": "\"use client\";\n\nimport React from \"react\";\nimport { stripHtmlToText } from \"@cimplify/sdk/react\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { getProductCurrency } from \"@cimplify/sdk\";\n\nconst TYPE_BADGES: Record<string, { label: string; color: string }> = {\n download: { label: \"Download\", color: \"bg-sky-50 text-sky-700 border-sky-200/60\" },\n license: { label: \"License\", color: \"bg-blue-50 text-blue-700 border-blue-200/60\" },\n event_ticket: { label: \"Event\", color: \"bg-violet-50 text-violet-700 border-violet-200/60\" },\n access_pass: { label: \"Access\", color: \"bg-emerald-50 text-emerald-700 border-emerald-200/60\" },\n gift_card: { label: \"Gift Card\", color: \"bg-amber-50 text-amber-700 border-amber-200/60\" },\n};\n\nexport function DigitalProductCard({\n product,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const currency = getProductCurrency(product);\n const typeBadge = product.digital_type ? TYPE_BADGES[product.digital_type] : null;\n const isTicket = product.digital_type === \"event_ticket\";\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"4/3\" renderImage={renderImage}>\n {typeBadge && (\n <span className={`absolute top-3 left-3 text-[11px] font-semibold tracking-wide border px-2 py-0.5 rounded-md ${typeBadge.color}`}>\n {typeBadge.label}\n </span>\n )}\n </CardImage>\n\n <div className=\"p-3.5\">\n <h3 className=\"text-[14.5px] font-semibold text-foreground leading-snug tracking-tight truncate\">\n {product.name}\n </h3>\n\n {/* Event: date + venue */}\n {isTicket && (product.event_date || product.venue) && (\n <p className=\"text-[12px] text-muted-foreground mt-1 truncate\">\n {product.event_date && new Date(product.event_date).toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" })}\n {product.event_date && product.venue && \" · \"}\n {product.venue}\n </p>\n )}\n\n {/* Non-event: description */}\n {!isTicket && product.description && (\n <p className=\"text-[12px] text-muted-foreground mt-1 line-clamp-1\">\n {stripHtmlToText(product.description)}\n </p>\n )}\n\n <div className=\"flex items-center justify-between mt-2.5\">\n <Price amount={product.default_price} currency={currency} className=\"text-sm font-bold\" />\n\n {/* File info pill */}\n {product.file_type && (\n <span className=\"text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded\">\n {product.file_type.toUpperCase()}\n {product.file_size_mb != null && ` · ${product.file_size_mb >= 1024 ? `${(product.file_size_mb / 1024).toFixed(1)}GB` : `${product.file_size_mb}MB`}`}\n </span>\n )}\n </div>\n </div>\n </CardShell>\n );\n}\n"
14
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport { stripHtmlToText } from \"@cimplify/sdk/react\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { getProductCurrency } from \"@cimplify/sdk\";\n\nconst TYPE_BADGES: Record<string, { label: string; color: string }> = {\n download: { label: \"Download\", color: \"bg-sky-50 text-sky-700 border-sky-200/60\" },\n license_key: { label: \"License\", color: \"bg-blue-50 text-blue-700 border-blue-200/60\" },\n ticket: { label: \"Event\", color: \"bg-violet-50 text-violet-700 border-violet-200/60\" },\n access_grant: { label: \"Access\", color: \"bg-emerald-50 text-emerald-700 border-emerald-200/60\" },\n redemption_code: { label: \"Code\", color: \"bg-amber-50 text-amber-700 border-amber-200/60\" },\n};\n\nexport function DigitalProductCard({\n product,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const currency = getProductCurrency(product);\n const typeBadge = product.digital_type ? TYPE_BADGES[product.digital_type] : null;\n const isTicket = product.digital_type === \"ticket\";\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"4/3\" renderImage={renderImage}>\n {typeBadge && (\n <span className={`absolute top-3 left-3 text-[11px] font-semibold tracking-wide border px-2 py-0.5 rounded-md ${typeBadge.color}`}>\n {typeBadge.label}\n </span>\n )}\n </CardImage>\n\n <div className=\"p-3.5\">\n <h3 className=\"text-[14.5px] font-semibold text-foreground leading-snug tracking-tight truncate\">\n {product.name}\n </h3>\n\n {/* Event: date + venue */}\n {isTicket && (product.event_date || product.venue) && (\n <p className=\"text-[12px] text-muted-foreground mt-1 truncate\">\n {product.event_date && new Date(product.event_date).toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" })}\n {product.event_date && product.venue && \" · \"}\n {product.venue}\n </p>\n )}\n\n {/* Non-event: description */}\n {!isTicket && product.description && (\n <p className=\"text-[12px] text-muted-foreground mt-1 line-clamp-1\">\n {stripHtmlToText(product.description)}\n </p>\n )}\n\n <div className=\"flex items-center justify-between mt-2.5\">\n <Price amount={product.default_price} currency={currency} className=\"text-sm font-bold\" />\n\n {/* File info pill */}\n {product.file_type && (\n <span className=\"text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded\">\n {product.file_type.toUpperCase()}\n {product.file_size_mb != null && ` · ${product.file_size_mb >= 1024 ? `${(product.file_size_mb / 1024).toFixed(1)}GB` : `${product.file_size_mb}MB`}`}\n </span>\n )}\n </div>\n </div>\n </CardShell>\n );\n}\n"
15
15
  }
16
16
  ]
17
17
  }
@@ -13,7 +13,7 @@
13
13
  "files": [
14
14
  {
15
15
  "path": "layouts/digital-product-layout.tsx",
16
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n TagPills,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { getProductCurrency } from \"@cimplify/sdk\";\nimport { RichText } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst DIGITAL_TYPE_LABELS: Record<string, string> = {\n download: \"Digital download\",\n license: \"Software license\",\n event_ticket: \"Event ticket\",\n access_pass: \"Access pass\",\n gift_card: \"Gift card\",\n};\n\nexport function DigitalProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const currency = getProductCurrency(product);\n const typeLabel = product.digital_type\n ? DIGITAL_TYPE_LABELS[product.digital_type] || product.digital_type\n : \"Digital product\";\n\n const isTicket = product.digital_type === \"event_ticket\";\n const isAccessPass = product.digital_type === \"access_pass\";\n const isDownload = product.digital_type === \"download\" || product.digital_type === \"license\";\n\n return (\n <div data-cimplify-product-layout=\"digital\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Type badge + tags */}\n <div className=\"flex flex-wrap items-center gap-2\">\n <span data-cimplify-product-layout-badge className=\"text-xs font-semibold uppercase tracking-wider text-primary bg-primary/10 px-2.5 py-1 rounded-full\">\n {typeLabel}\n </span>\n <TagPills tags={product.tags || []} />\n </div>\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-extrabold tracking-tight\">\n {product.name}\n </h1>\n <Price amount={product.default_price} currency={currency} className=\"text-2xl font-bold mt-2 block\" />\n </div>\n\n {/* Description */}\n <RichText html={product.description} data-cimplify-product-layout-description />\n\n {/* Delivery notice */}\n {isDownload && (\n <div data-cimplify-product-layout-delivery className=\"flex items-center gap-3 px-4 py-3 bg-muted rounded-lg\">\n <svg className=\"w-5 h-5 text-primary shrink-0\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 10V3L4 14h7v7l9-11h-7z\" />\n </svg>\n <div>\n <p className=\"text-sm font-medium\">Instant delivery</p>\n <p className=\"text-xs text-muted-foreground\">Download immediately after purchase</p>\n </div>\n </div>\n )}\n\n {/* Event info */}\n {isTicket && (product.event_date || product.venue) && (\n <div data-cimplify-product-layout-event className=\"border border-border p-4 space-y-2\">\n {product.event_date && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Date:</span>\n <span className=\"font-medium\">{new Date(product.event_date).toLocaleDateString(undefined, { weekday: \"long\", year: \"numeric\", month: \"long\", day: \"numeric\" })}</span>\n </div>\n )}\n {product.venue && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Venue:</span>\n <span className=\"font-medium\">{product.venue}</span>\n </div>\n )}\n {product.ticket_type && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Type:</span>\n <span className=\"font-medium capitalize\">{product.ticket_type}</span>\n </div>\n )}\n </div>\n )}\n\n {/* Access pass info */}\n {isAccessPass && (product.access_type || product.access_duration_days) && (\n <div data-cimplify-product-layout-access className=\"border border-border p-4 space-y-2\">\n {product.access_type && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Access:</span>\n <span className=\"font-medium capitalize\">{product.access_type}</span>\n </div>\n )}\n {product.access_level && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Level:</span>\n <span className=\"font-medium capitalize\">{product.access_level}</span>\n </div>\n )}\n {product.access_duration_days && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Duration:</span>\n <span className=\"font-medium\">{product.access_duration_days} days</span>\n </div>\n )}\n </div>\n )}\n\n {/* File details */}\n {isDownload && (product.file_size_mb || product.file_type || product.version || product.max_downloads) && (\n <div data-cimplify-product-layout-file-details className=\"grid grid-cols-2 gap-3\">\n {product.file_type && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Format</p>\n <p className=\"text-sm font-semibold\">{product.file_type.toUpperCase()}</p>\n </div>\n )}\n {product.file_size_mb != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Size</p>\n <p className=\"text-sm font-semibold\">{product.file_size_mb >= 1024 ? `${(product.file_size_mb / 1024).toFixed(1)} GB` : `${product.file_size_mb} MB`}</p>\n </div>\n )}\n {product.max_downloads != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Downloads</p>\n <p className=\"text-sm font-semibold\">{product.max_downloads}</p>\n </div>\n )}\n {product.version && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Version</p>\n <p className=\"text-sm font-semibold\">v{product.version}</p>\n </div>\n )}\n {product.download_expires_days != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Expires</p>\n <p className=\"text-sm font-semibold\">{product.download_expires_days} days</p>\n </div>\n )}\n {product.max_activations != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Activations</p>\n <p className=\"text-sm font-semibold\">{product.max_activations}</p>\n </div>\n )}\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Customizer (license variants, billing plans, add to cart) */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n showSpecialInstructions={false}\n />\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n title=\"More digital products\"\n />\n )}\n </div>\n );\n}\n"
16
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n CustomAttributesTable,\n TagPills,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { getProductCurrency } from \"@cimplify/sdk\";\nimport { RichText } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst DIGITAL_TYPE_LABELS: Record<string, string> = {\n download: \"Digital download\",\n license_key: \"Software license\",\n ticket: \"Event ticket\",\n access_grant: \"Access pass\",\n redemption_code: \"Redemption code\",\n};\n\nexport function DigitalProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const currency = getProductCurrency(product);\n const typeLabel = product.digital_type\n ? DIGITAL_TYPE_LABELS[product.digital_type] || product.digital_type\n : \"Digital product\";\n\n const isTicket = product.digital_type === \"ticket\";\n const isAccessPass = product.digital_type === \"access_grant\";\n const isDownload = product.digital_type === \"download\" || product.digital_type === \"license_key\";\n\n return (\n <div data-cimplify-product-layout=\"digital\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Type badge + tags */}\n <div className=\"flex flex-wrap items-center gap-2\">\n <span data-cimplify-product-layout-badge className=\"text-xs font-semibold uppercase tracking-wider text-primary bg-primary/10 px-2.5 py-1 rounded-full\">\n {typeLabel}\n </span>\n <TagPills tags={product.tags || []} />\n </div>\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-extrabold tracking-tight\">\n {product.name}\n </h1>\n <Price amount={product.default_price} currency={currency} className=\"text-2xl font-bold mt-2 block\" />\n </div>\n\n {/* Description */}\n <RichText html={product.description} data-cimplify-product-layout-description />\n\n {/* Delivery notice */}\n {isDownload && (\n <div data-cimplify-product-layout-delivery className=\"flex items-center gap-3 px-4 py-3 bg-muted rounded-lg\">\n <svg className=\"w-5 h-5 text-primary shrink-0\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 10V3L4 14h7v7l9-11h-7z\" />\n </svg>\n <div>\n <p className=\"text-sm font-medium\">Instant delivery</p>\n <p className=\"text-xs text-muted-foreground\">Download immediately after purchase</p>\n </div>\n </div>\n )}\n\n {/* Event info */}\n {isTicket && (product.event_date || product.venue) && (\n <div data-cimplify-product-layout-event className=\"border border-border p-4 space-y-2\">\n {product.event_date && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Date:</span>\n <span className=\"font-medium\">{new Date(product.event_date).toLocaleDateString(undefined, { weekday: \"long\", year: \"numeric\", month: \"long\", day: \"numeric\" })}</span>\n </div>\n )}\n {product.venue && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Venue:</span>\n <span className=\"font-medium\">{product.venue}</span>\n </div>\n )}\n {product.ticket_type && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Type:</span>\n <span className=\"font-medium capitalize\">{product.ticket_type}</span>\n </div>\n )}\n </div>\n )}\n\n {/* Access pass info */}\n {isAccessPass && (product.access_type || product.access_duration_days) && (\n <div data-cimplify-product-layout-access className=\"border border-border p-4 space-y-2\">\n {product.access_type && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Access:</span>\n <span className=\"font-medium capitalize\">{product.access_type}</span>\n </div>\n )}\n {product.access_level && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Level:</span>\n <span className=\"font-medium capitalize\">{product.access_level}</span>\n </div>\n )}\n {product.access_duration_days && (\n <div className=\"flex items-center gap-2 text-sm\">\n <span className=\"text-muted-foreground\">Duration:</span>\n <span className=\"font-medium\">{product.access_duration_days} days</span>\n </div>\n )}\n </div>\n )}\n\n {/* File details */}\n {isDownload && (product.file_size_mb || product.file_type || product.version || product.max_downloads) && (\n <div data-cimplify-product-layout-file-details className=\"grid grid-cols-2 gap-3\">\n {product.file_type && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Format</p>\n <p className=\"text-sm font-semibold\">{product.file_type.toUpperCase()}</p>\n </div>\n )}\n {product.file_size_mb != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Size</p>\n <p className=\"text-sm font-semibold\">{product.file_size_mb >= 1024 ? `${(product.file_size_mb / 1024).toFixed(1)} GB` : `${product.file_size_mb} MB`}</p>\n </div>\n )}\n {product.max_downloads != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Downloads</p>\n <p className=\"text-sm font-semibold\">{product.max_downloads}</p>\n </div>\n )}\n {product.version && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Version</p>\n <p className=\"text-sm font-semibold\">v{product.version}</p>\n </div>\n )}\n {product.download_expires_days != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Expires</p>\n <p className=\"text-sm font-semibold\">{product.download_expires_days} days</p>\n </div>\n )}\n {product.max_activations != null && (\n <div className=\"px-3 py-2 bg-muted text-center\">\n <p className=\"text-xs text-muted-foreground\">Activations</p>\n <p className=\"text-sm font-semibold\">{product.max_activations}</p>\n </div>\n )}\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Customizer (license variants, billing plans, add to cart) */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n showSpecialInstructions={false}\n />\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n title=\"More digital products\"\n />\n )}\n </div>\n );\n}\n"
17
17
  }
18
18
  ]
19
19
  }