@aforoai/storefront-widgets 1.0.4 → 1.0.6

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/CHANGELOG.md CHANGED
@@ -8,8 +8,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.0.6] — 2026-08-26
12
+
11
13
  ### Fixed
12
14
 
15
+ - **PricingCard's published "Card border radius" setting (Embed Studio →
16
+ Appearance) was never applied.** `borderRadius` is a valid field on the
17
+ fetched `embeddableWidget` tenant-defaults object — the tenant config API
18
+ correctly returned it — but `extractTenantDefaults()` only extracted its
19
+ sibling fields (`layout`, `theme`, `maxPlans`, `showFeatures`, `ctaText`)
20
+ and silently dropped `borderRadius`. The plan card's corner radius was
21
+ also hardcoded (`borderRadius: 12`) rather than reading any dynamic value.
22
+ `AforoPricingCardProps` now accepts an explicit `borderRadius` prop,
23
+ `extractTenantDefaults()` reads it from the tenant's published config, and
24
+ the resolved value (prop → tenant default → built-in 12px) is applied to
25
+ the plan card and its loading-skeleton twin. `pricingCardConfigToProps()`
26
+ also maps a `data-border-radius`/`borderRadius` mount-config override for
27
+ Script Tag integrations.
28
+
29
+ ## [1.0.5] — 2026-08-26
30
+
31
+ ### Fixed
32
+
33
+ - **Upgrade/Cancel (and every other widget's) bridge-exchange session refresh
34
+ threw "Malformed bridge exchange response" even though the exchange call
35
+ itself returned HTTP 200.** `BridgeClient.exchange()` — along with
36
+ `fetchHeadlessConfig()`, `fetchSubscriptions()`, and `initiateCheckout()` —
37
+ read the session/config/subscription/checkout payload directly off the
38
+ response body, but storefront-service wraps it in a `{success, data}`
39
+ envelope. Every field lookup came back `undefined`, so `exchange()`'s own
40
+ shape validation correctly rejected the (correctly-wrapped) response as
41
+ malformed. All four methods now unwrap `body.data ?? body` before reading
42
+ fields, so both a flat and an enveloped response parse correctly. This was
43
+ the root cause of "Confirm cancellation" failing on the Upgrade/Cancel
44
+ Script Tag widget after a legitimate session refresh mid-flow.
45
+ - **PricingCard falls back to per-unit rate when `priceCents` is null.**
46
+ Offerings with no flat/base price (pure PER_UNIT rate plans, e.g. $9/unit)
47
+ previously rendered as "Contact us". PricingCard now reads the primary rate
48
+ plan's `perUnitPriceCents` from `OfferingPayload.ratePlans` and renders
49
+ "$9.00 / unit" in both the card-grid and comparison-table layouts.
50
+
51
+ ### Fixed (carried over from unreleased, pre-1.0.5)
52
+
13
53
  - **Script-tag loader now parses the PricingCard presentation attributes
14
54
  `data-cta-text`, `data-cta-url`, `data-max-plans`, and
15
55
  `data-show-features`.** `pricingCardConfigToProps` reads all four
package/dist/index.cjs CHANGED
@@ -171,7 +171,8 @@ var _BridgeClient = class _BridgeClient {
171
171
  throw await this.parseError(resp);
172
172
  }
173
173
  const body = await resp.json();
174
- if (!body.sessionJwt || typeof body.expiresAt !== "number") {
174
+ const data = body?.data ?? body;
175
+ if (!data || !data.sessionJwt || typeof data.expiresAt !== "number") {
175
176
  throw new BridgeClientError(
176
177
  "Malformed bridge exchange response",
177
178
  500,
@@ -180,9 +181,9 @@ var _BridgeClient = class _BridgeClient {
180
181
  );
181
182
  }
182
183
  return {
183
- sessionJwt: body.sessionJwt,
184
- expiresAt: body.expiresAt,
185
- customerId: body.customerId ?? null
184
+ sessionJwt: data.sessionJwt,
185
+ expiresAt: data.expiresAt,
186
+ customerId: data.customerId ?? null
186
187
  };
187
188
  }
188
189
  /**
@@ -268,16 +269,17 @@ var _BridgeClient = class _BridgeClient {
268
269
  try {
269
270
  const body = await resp.json();
270
271
  if (!body || typeof body !== "object") return null;
272
+ const data = body.data ?? body;
271
273
  return {
272
- tenantSlug: body.tenantSlug,
274
+ tenantSlug: data.tenantSlug,
273
275
  branding: {
274
- primaryColor: body.primaryColor ?? null,
275
- secondaryColor: body.secondaryColor ?? null,
276
- logoUrl: body.logoUrl ?? null,
277
- fontFamily: body.fontFamily ?? null
276
+ primaryColor: data.primaryColor ?? null,
277
+ secondaryColor: data.secondaryColor ?? null,
278
+ logoUrl: data.logoUrl ?? null,
279
+ fontFamily: data.fontFamily ?? null
278
280
  },
279
- offerings: Array.isArray(body.offerings) ? body.offerings : [],
280
- embeddableWidget: body.embeddableWidget ?? null
281
+ offerings: Array.isArray(data.offerings) ? data.offerings : [],
282
+ embeddableWidget: data.embeddableWidget ?? null
281
283
  };
282
284
  } catch {
283
285
  return null;
@@ -422,7 +424,8 @@ var _BridgeClient = class _BridgeClient {
422
424
  false
423
425
  );
424
426
  }
425
- if (!body.checkoutUrl || !body.checkoutSessionId || !body.expiresAt) {
427
+ const data = body?.data ?? body ?? {};
428
+ if (!data.checkoutUrl || !data.checkoutSessionId || !data.expiresAt) {
426
429
  throw new BridgeClientError(
427
430
  "Malformed checkout-initiate response (missing required fields)",
428
431
  500,
@@ -431,9 +434,9 @@ var _BridgeClient = class _BridgeClient {
431
434
  );
432
435
  }
433
436
  return {
434
- checkoutUrl: body.checkoutUrl,
435
- checkoutSessionId: body.checkoutSessionId,
436
- expiresAt: body.expiresAt
437
+ checkoutUrl: data.checkoutUrl,
438
+ checkoutSessionId: data.checkoutSessionId,
439
+ expiresAt: data.expiresAt
437
440
  };
438
441
  }
439
442
  /* ────────────────────────────────────────────────────────────────────
@@ -1241,13 +1244,16 @@ var _BridgeClient = class _BridgeClient {
1241
1244
  if (!resp.ok) return emptyPage;
1242
1245
  try {
1243
1246
  const body = await resp.json();
1244
- if (!body || typeof body !== "object") return emptyPage;
1247
+ if (!body || typeof body !== "object") {
1248
+ return emptyPage;
1249
+ }
1250
+ const data = body.data ?? body;
1245
1251
  return {
1246
- content: Array.isArray(body.content) ? body.content : [],
1247
- totalElements: typeof body.totalElements === "number" && Number.isFinite(body.totalElements) ? body.totalElements : 0,
1248
- totalPages: typeof body.totalPages === "number" && Number.isFinite(body.totalPages) ? body.totalPages : 0,
1249
- page: typeof body.page === "number" && Number.isFinite(body.page) ? body.page : filter.page ?? 0,
1250
- size: typeof body.size === "number" && Number.isFinite(body.size) ? body.size : filter.size ?? 10
1252
+ content: Array.isArray(data.content) ? data.content : [],
1253
+ totalElements: typeof data.totalElements === "number" && Number.isFinite(data.totalElements) ? data.totalElements : 0,
1254
+ totalPages: typeof data.totalPages === "number" && Number.isFinite(data.totalPages) ? data.totalPages : 0,
1255
+ page: typeof data.page === "number" && Number.isFinite(data.page) ? data.page : filter.page ?? 0,
1256
+ size: typeof data.size === "number" && Number.isFinite(data.size) ? data.size : filter.size ?? 10
1251
1257
  };
1252
1258
  } catch {
1253
1259
  return emptyPage;
@@ -2666,6 +2672,7 @@ function WidgetShell(props) {
2666
2672
  var DEFAULT_LAYOUT = "horizontal";
2667
2673
  var DEFAULT_MAX_PLANS = 4;
2668
2674
  var DEFAULT_CTA_TEXT = "Get Started";
2675
+ var DEFAULT_CARD_RADIUS = 12;
2669
2676
  function clampLayout(raw) {
2670
2677
  return raw === "horizontal" || raw === "vertical" || raw === "table" ? raw : void 0;
2671
2678
  }
@@ -2676,7 +2683,7 @@ function setTenantDefaultsIdempotent(setter, next) {
2676
2683
  setter((prev) => {
2677
2684
  if (prev === next) return prev;
2678
2685
  if (!prev && !next) return prev;
2679
- if (prev && next && prev.layout === next.layout && prev.theme === next.theme && prev.maxPlans === next.maxPlans && prev.showFeatures === next.showFeatures && prev.ctaText === next.ctaText) {
2686
+ if (prev && next && prev.layout === next.layout && prev.theme === next.theme && prev.maxPlans === next.maxPlans && prev.showFeatures === next.showFeatures && prev.ctaText === next.ctaText && prev.borderRadius === next.borderRadius) {
2680
2687
  return prev;
2681
2688
  }
2682
2689
  return next;
@@ -2691,7 +2698,8 @@ function extractTenantDefaults(config) {
2691
2698
  theme: typeof raw.theme === "string" ? raw.theme : void 0,
2692
2699
  maxPlans: typeof raw.maxPlans === "number" && Number.isFinite(raw.maxPlans) ? raw.maxPlans : void 0,
2693
2700
  showFeatures: typeof raw.showFeatures === "boolean" ? raw.showFeatures : void 0,
2694
- ctaText: typeof raw.ctaText === "string" && raw.ctaText.length > 0 ? raw.ctaText : void 0
2701
+ ctaText: typeof raw.ctaText === "string" && raw.ctaText.length > 0 ? raw.ctaText : void 0,
2702
+ borderRadius: typeof raw.borderRadius === "number" && Number.isFinite(raw.borderRadius) ? raw.borderRadius : void 0
2695
2703
  };
2696
2704
  }
2697
2705
  var DARK_TOKEN_OVERLAY = {
@@ -2751,6 +2759,20 @@ function safeFormatCurrency(priceCents, currency, locale, intlOverride) {
2751
2759
  return `${currency} ${amount.toFixed(2)}`;
2752
2760
  }
2753
2761
  }
2762
+ function resolveDisplayPrice(offering, locale, intlOverride) {
2763
+ const flat = safeFormatCurrency(offering.priceCents, offering.currency, locale, intlOverride);
2764
+ if (flat !== "\u2014") return { text: flat, unitSuffix: null };
2765
+ const perUnitPlan = offering.ratePlans?.find(
2766
+ (rp) => rp.pricingModel === "PER_UNIT" && rp.perUnitPriceCents != null
2767
+ );
2768
+ if (perUnitPlan) {
2769
+ const unitPrice = safeFormatCurrency(perUnitPlan.perUnitPriceCents, offering.currency, locale, intlOverride);
2770
+ if (unitPrice !== "\u2014") {
2771
+ return { text: unitPrice, unitSuffix: perUnitPlan.unitLabel || "unit" };
2772
+ }
2773
+ }
2774
+ return { text: "\u2014", unitSuffix: null };
2775
+ }
2754
2776
  function localizedName(o) {
2755
2777
  return o.localizedDisplayName || o.name || "(unnamed plan)";
2756
2778
  }
@@ -2799,6 +2821,7 @@ function PricingCardBody(props) {
2799
2821
  theme: themeProp,
2800
2822
  locale: localeProp,
2801
2823
  maxPlans: maxPlansProp,
2824
+ borderRadius: borderRadiusProp,
2802
2825
  showFeatures: showFeaturesProp,
2803
2826
  ctaText: ctaTextProp,
2804
2827
  ctaUrl: ctaUrlProp,
@@ -2812,6 +2835,7 @@ function PricingCardBody(props) {
2812
2835
  const maxPlans = typeof maxPlansProp === "number" && Number.isFinite(maxPlansProp) ? maxPlansProp : typeof tenantDefaults?.maxPlans === "number" && Number.isFinite(tenantDefaults.maxPlans) ? tenantDefaults.maxPlans : DEFAULT_MAX_PLANS;
2813
2836
  const showFeatures = typeof showFeaturesProp === "boolean" ? showFeaturesProp : typeof tenantDefaults?.showFeatures === "boolean" ? tenantDefaults.showFeatures : true;
2814
2837
  const ctaTextEffective = ctaTextProp ?? tenantDefaults?.ctaText ?? void 0;
2838
+ const cardRadius = typeof borderRadiusProp === "number" && Number.isFinite(borderRadiusProp) ? borderRadiusProp : typeof tenantDefaults?.borderRadius === "number" && Number.isFinite(tenantDefaults.borderRadius) ? tenantDefaults.borderRadius : DEFAULT_CARD_RADIUS;
2815
2839
  const shell = useWidgetShell();
2816
2840
  const { tokens: shellTokens, bridgeClient, telemetry, bus, tenantSlug } = shell;
2817
2841
  const isDark = React7__namespace.useMemo(() => {
@@ -3029,7 +3053,8 @@ function PricingCardBody(props) {
3029
3053
  {
3030
3054
  layout,
3031
3055
  tokens,
3032
- prefersReducedMotion
3056
+ prefersReducedMotion,
3057
+ cardRadius
3033
3058
  }
3034
3059
  )
3035
3060
  }
@@ -3101,7 +3126,8 @@ function PricingCardBody(props) {
3101
3126
  ctaTextProp: ctaTextEffective,
3102
3127
  featuredOverride: featuredOfferingId,
3103
3128
  onCtaClick: handleCtaClick,
3104
- intlOverride
3129
+ intlOverride,
3130
+ cardRadius
3105
3131
  }
3106
3132
  )
3107
3133
  }
@@ -3116,7 +3142,8 @@ function PlanGrid({
3116
3142
  ctaTextProp,
3117
3143
  featuredOverride,
3118
3144
  onCtaClick,
3119
- intlOverride
3145
+ intlOverride,
3146
+ cardRadius
3120
3147
  }) {
3121
3148
  const isHorizontal = layout === "horizontal";
3122
3149
  const gridStyle2 = isHorizontal ? {
@@ -3139,7 +3166,8 @@ function PlanGrid({
3139
3166
  ctaTextProp,
3140
3167
  featured: offeringIsFeatured(o, featuredOverride),
3141
3168
  onCtaClick,
3142
- intlOverride
3169
+ intlOverride,
3170
+ cardRadius
3143
3171
  },
3144
3172
  o.offeringId
3145
3173
  )) });
@@ -3152,14 +3180,10 @@ function PlanCard({
3152
3180
  ctaTextProp,
3153
3181
  featured,
3154
3182
  onCtaClick,
3155
- intlOverride
3183
+ intlOverride,
3184
+ cardRadius
3156
3185
  }) {
3157
- const price = safeFormatCurrency(
3158
- offering.priceCents,
3159
- offering.currency,
3160
- locale,
3161
- intlOverride
3162
- );
3186
+ const { text: price, unitSuffix } = resolveDisplayPrice(offering, locale, intlOverride);
3163
3187
  const isContactSales = price === "\u2014";
3164
3188
  const planName = localizedName(offering);
3165
3189
  const description = localizedDescription(offering);
@@ -3168,7 +3192,7 @@ function PlanCard({
3168
3192
  display: "flex",
3169
3193
  flexDirection: "column",
3170
3194
  padding: 24,
3171
- borderRadius: 12,
3195
+ borderRadius: cardRadius,
3172
3196
  border: featured ? `2px solid ${tokens.primary}` : `1px solid ${tokens.border}`,
3173
3197
  background: tokens.bg,
3174
3198
  color: tokens.text,
@@ -3180,7 +3204,7 @@ function PlanCard({
3180
3204
  "div",
3181
3205
  {
3182
3206
  role: "group",
3183
- "aria-label": `${planName}, ${price} per ${offering.billingCycle}`,
3207
+ "aria-label": `${planName}, ${price} per ${unitSuffix ?? offering.billingCycle}`,
3184
3208
  className: `aforo-w-pc-card${featured ? " aforo-w-pc-card-featured" : ""}`,
3185
3209
  style: cardStyle2,
3186
3210
  children: [
@@ -3245,14 +3269,14 @@ function PlanCard({
3245
3269
  children: price
3246
3270
  }
3247
3271
  ),
3248
- !isContactSales && offering.billingCycle ? /* @__PURE__ */ jsxRuntime.jsxs(
3272
+ !isContactSales && (unitSuffix || offering.billingCycle) ? /* @__PURE__ */ jsxRuntime.jsxs(
3249
3273
  "span",
3250
3274
  {
3251
3275
  className: "aforo-w-pc-cycle",
3252
3276
  style: { fontSize: 13, color: tokens.textMuted },
3253
3277
  children: [
3254
3278
  "/ ",
3255
- humanCycle(offering.billingCycle)
3279
+ unitSuffix ?? humanCycle(offering.billingCycle)
3256
3280
  ]
3257
3281
  }
3258
3282
  ) : null
@@ -3410,6 +3434,7 @@ function TableLayout({
3410
3434
  /* @__PURE__ */ jsxRuntime.jsx("th", { scope: "col", style: { ...headerCellStyle, color: tokens.textMuted, fontSize: 12 }, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "aforo-w-pc-sr-only", style: srOnlyStyle, children: "Feature" }) }),
3411
3435
  offerings.map((o) => {
3412
3436
  const featured = offeringIsFeatured(o, featuredOverride);
3437
+ const { text: tablePrice, unitSuffix: tableUnitSuffix } = resolveDisplayPrice(o, locale, intlOverride);
3413
3438
  return /* @__PURE__ */ jsxRuntime.jsx(
3414
3439
  "th",
3415
3440
  {
@@ -3439,13 +3464,20 @@ function TableLayout({
3439
3464
  children: "Most popular"
3440
3465
  }
3441
3466
  ) : null,
3442
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 22, fontWeight: 700 }, children: safeFormatCurrency(o.priceCents, o.currency, locale, intlOverride) }),
3467
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 22, fontWeight: 700 }, children: [
3468
+ tablePrice,
3469
+ tableUnitSuffix ? /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontSize: 12, fontWeight: 400, color: tokens.textMuted }, children: [
3470
+ " ",
3471
+ "/ ",
3472
+ tableUnitSuffix
3473
+ ] }) : null
3474
+ ] }),
3443
3475
  /* @__PURE__ */ jsxRuntime.jsx(
3444
3476
  "button",
3445
3477
  {
3446
3478
  type: "button",
3447
3479
  onClick: () => onCtaClick(o),
3448
- "aria-label": `${ctaLabel(o, ctaTextProp, safeFormatCurrency(o.priceCents, o.currency, locale, intlOverride) === "\u2014")} for ${localizedName(o)}`,
3480
+ "aria-label": `${ctaLabel(o, ctaTextProp, tablePrice === "\u2014")} for ${localizedName(o)}`,
3449
3481
  style: {
3450
3482
  inlineSize: "100%",
3451
3483
  padding: "8px 12px",
@@ -3458,7 +3490,7 @@ function TableLayout({
3458
3490
  cursor: "pointer",
3459
3491
  fontFamily: tokens.fontFamily
3460
3492
  },
3461
- children: ctaLabel(o, ctaTextProp, safeFormatCurrency(o.priceCents, o.currency, locale, intlOverride) === "\u2014")
3493
+ children: ctaLabel(o, ctaTextProp, tablePrice === "\u2014")
3462
3494
  }
3463
3495
  )
3464
3496
  ] })
@@ -3510,7 +3542,8 @@ function cellAria(label, feature) {
3510
3542
  function LoadingSkeleton({
3511
3543
  layout,
3512
3544
  tokens,
3513
- prefersReducedMotion
3545
+ prefersReducedMotion,
3546
+ cardRadius
3514
3547
  }) {
3515
3548
  const shimmerStyle = prefersReducedMotion ? { background: tokens.border, opacity: 0.6 } : {
3516
3549
  background: `linear-gradient(90deg, ${tokens.border} 0%, ${tokens.bg} 50%, ${tokens.border} 100%)`,
@@ -3543,7 +3576,7 @@ function LoadingSkeleton({
3543
3576
  "aria-hidden": "true",
3544
3577
  style: {
3545
3578
  padding: 24,
3546
- borderRadius: 12,
3579
+ borderRadius: cardRadius,
3547
3580
  border: `1px solid ${tokens.border}`,
3548
3581
  background: tokens.bg,
3549
3582
  minHeight: 280,
@@ -12175,7 +12208,7 @@ function ResultPanel({
12175
12208
  }
12176
12209
 
12177
12210
  // src/core/version.ts
12178
- var VERSION = "1.0.4";
12211
+ var VERSION = "1.0.6";
12179
12212
 
12180
12213
  // src/core/AforoEmbed.ts
12181
12214
  var mountedElements = /* @__PURE__ */ new WeakMap();
package/dist/index.d.cts CHANGED
@@ -43,6 +43,8 @@ interface AforoPricingCardProps {
43
43
  locale?: string;
44
44
  /** Max plans to render — operator-set per FR-EMB-02. Default 4. */
45
45
  maxPlans?: number;
46
+ /** Card corner radius in px — operator-set per Embed Studio Appearance (0–32px). Default 12. */
47
+ borderRadius?: number;
46
48
  /** Whether to show the per-plan features list. Default `true`. */
47
49
  showFeatures?: boolean;
48
50
  /** Default CTA label when not overridden per-offering. Default `Get Started` (FR-EMB-03). */
package/dist/index.d.ts CHANGED
@@ -43,6 +43,8 @@ interface AforoPricingCardProps {
43
43
  locale?: string;
44
44
  /** Max plans to render — operator-set per FR-EMB-02. Default 4. */
45
45
  maxPlans?: number;
46
+ /** Card corner radius in px — operator-set per Embed Studio Appearance (0–32px). Default 12. */
47
+ borderRadius?: number;
46
48
  /** Whether to show the per-plan features list. Default `true`. */
47
49
  showFeatures?: boolean;
48
50
  /** Default CTA label when not overridden per-offering. Default `Get Started` (FR-EMB-03). */