@aforoai/storefront-widgets 1.0.3 → 1.0.5
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 +22 -0
- package/dist/index.cjs +80 -33
- package/dist/index.d.cts +18 -5
- package/dist/index.d.ts +18 -5
- package/dist/index.mjs +80 -33
- package/dist/loader.js +1 -1
- package/dist/loader.mjs +1 -1
- package/dist/sri.json +11 -11
- package/dist/vanilla/index.cjs +80 -33
- package/dist/vanilla/index.mjs +80 -33
- package/dist/vue/index.cjs +80 -33
- package/dist/vue/index.mjs +80 -33
- package/dist/widgets/checkout-flow.js +5 -5
- package/dist/widgets/invoice-list.js +6 -6
- package/dist/widgets/payment-method.js +5 -5
- package/dist/widgets/pricing-card.js +8 -8
- package/dist/widgets/subscribe-button.js +8 -8
- package/dist/widgets/subscription-manager.js +5 -5
- package/dist/widgets/upgrade-cancel.js +6 -6
- package/dist/widgets/usage-meter.js +5 -5
- package/loader.sri.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,8 +8,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.0.5] — 2026-08-26
|
|
12
|
+
|
|
11
13
|
### Fixed
|
|
12
14
|
|
|
15
|
+
- **Upgrade/Cancel (and every other widget's) bridge-exchange session refresh
|
|
16
|
+
threw "Malformed bridge exchange response" even though the exchange call
|
|
17
|
+
itself returned HTTP 200.** `BridgeClient.exchange()` — along with
|
|
18
|
+
`fetchHeadlessConfig()`, `fetchSubscriptions()`, and `initiateCheckout()` —
|
|
19
|
+
read the session/config/subscription/checkout payload directly off the
|
|
20
|
+
response body, but storefront-service wraps it in a `{success, data}`
|
|
21
|
+
envelope. Every field lookup came back `undefined`, so `exchange()`'s own
|
|
22
|
+
shape validation correctly rejected the (correctly-wrapped) response as
|
|
23
|
+
malformed. All four methods now unwrap `body.data ?? body` before reading
|
|
24
|
+
fields, so both a flat and an enveloped response parse correctly. This was
|
|
25
|
+
the root cause of "Confirm cancellation" failing on the Upgrade/Cancel
|
|
26
|
+
Script Tag widget after a legitimate session refresh mid-flow.
|
|
27
|
+
- **PricingCard falls back to per-unit rate when `priceCents` is null.**
|
|
28
|
+
Offerings with no flat/base price (pure PER_UNIT rate plans, e.g. $9/unit)
|
|
29
|
+
previously rendered as "Contact us". PricingCard now reads the primary rate
|
|
30
|
+
plan's `perUnitPriceCents` from `OfferingPayload.ratePlans` and renders
|
|
31
|
+
"$9.00 / unit" in both the card-grid and comparison-table layouts.
|
|
32
|
+
|
|
33
|
+
### Fixed (carried over from unreleased, pre-1.0.5)
|
|
34
|
+
|
|
13
35
|
- **Script-tag loader now parses the PricingCard presentation attributes
|
|
14
36
|
`data-cta-text`, `data-cta-url`, `data-max-plans`, and
|
|
15
37
|
`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
|
-
|
|
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:
|
|
184
|
-
expiresAt:
|
|
185
|
-
customerId:
|
|
184
|
+
sessionJwt: data.sessionJwt,
|
|
185
|
+
expiresAt: data.expiresAt,
|
|
186
|
+
customerId: data.customerId ?? null
|
|
186
187
|
};
|
|
187
188
|
}
|
|
188
189
|
/**
|
|
@@ -223,10 +224,23 @@ var _BridgeClient = class _BridgeClient {
|
|
|
223
224
|
/**
|
|
224
225
|
* Headless config — the PricingCard widget's primary read path.
|
|
225
226
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
227
|
+
* Hits `GET /api/v1/portal/embed/tenant-config/{slug}` — the SAME
|
|
228
|
+
* embed-key-authenticated endpoint {@link getTenantBrandKit} uses, now
|
|
229
|
+
* extended server-side with `offerings` + `embeddableWidget`. This was
|
|
230
|
+
* previously wired to `GET /api/v1/portal/headless/config`, an unrelated
|
|
231
|
+
* T6 Headless-tier endpoint that requires `X-Storefront-Key`/
|
|
232
|
+
* `X-Tenant-Id` auth — headers the embed key flow never sends. Every
|
|
233
|
+
* embed-tier fetch against that URL failed 400 "Missing tenant
|
|
234
|
+
* identification" by construction; this endpoint is scoped to the same
|
|
235
|
+
* `EmbedKeyAuthFilter` gate as the rest of the embed plane, so the
|
|
236
|
+
* `Authorization: Bearer <embed_key>` header `buildHeaders()` already
|
|
237
|
+
* sends is sufficient.
|
|
238
|
+
*
|
|
239
|
+
* The backend response is still a flat record (`tenantSlug,
|
|
240
|
+
* primaryColor, secondaryColor, logoUrl, fontFamily, offerings,
|
|
241
|
+
* embeddableWidget`) — kept flat so `getTenantBrandKit()`'s existing
|
|
242
|
+
* consumers (the ThemeReader cascade) are unaffected. This method nests
|
|
243
|
+
* the branding fields into the shape `HeadlessConfigResponse` expects.
|
|
230
244
|
*
|
|
231
245
|
* Pattern #18 fail-soft — returns `null` on transport failure, 404, or
|
|
232
246
|
* malformed JSON. Widgets fall through to their empty / error state and
|
|
@@ -241,7 +255,7 @@ var _BridgeClient = class _BridgeClient {
|
|
|
241
255
|
}
|
|
242
256
|
const target = slug || this.tenantSlug;
|
|
243
257
|
if (!target) return null;
|
|
244
|
-
const url = `${this.baseUrl}/api/v1/portal/
|
|
258
|
+
const url = `${this.baseUrl}/api/v1/portal/embed/tenant-config/${encodeURIComponent(target)}`;
|
|
245
259
|
let resp;
|
|
246
260
|
try {
|
|
247
261
|
resp = await this.send(url, {
|
|
@@ -254,7 +268,19 @@ var _BridgeClient = class _BridgeClient {
|
|
|
254
268
|
if (!resp.ok) return null;
|
|
255
269
|
try {
|
|
256
270
|
const body = await resp.json();
|
|
257
|
-
|
|
271
|
+
if (!body || typeof body !== "object") return null;
|
|
272
|
+
const data = body.data ?? body;
|
|
273
|
+
return {
|
|
274
|
+
tenantSlug: data.tenantSlug,
|
|
275
|
+
branding: {
|
|
276
|
+
primaryColor: data.primaryColor ?? null,
|
|
277
|
+
secondaryColor: data.secondaryColor ?? null,
|
|
278
|
+
logoUrl: data.logoUrl ?? null,
|
|
279
|
+
fontFamily: data.fontFamily ?? null
|
|
280
|
+
},
|
|
281
|
+
offerings: Array.isArray(data.offerings) ? data.offerings : [],
|
|
282
|
+
embeddableWidget: data.embeddableWidget ?? null
|
|
283
|
+
};
|
|
258
284
|
} catch {
|
|
259
285
|
return null;
|
|
260
286
|
}
|
|
@@ -398,7 +424,8 @@ var _BridgeClient = class _BridgeClient {
|
|
|
398
424
|
false
|
|
399
425
|
);
|
|
400
426
|
}
|
|
401
|
-
|
|
427
|
+
const data = body?.data ?? body ?? {};
|
|
428
|
+
if (!data.checkoutUrl || !data.checkoutSessionId || !data.expiresAt) {
|
|
402
429
|
throw new BridgeClientError(
|
|
403
430
|
"Malformed checkout-initiate response (missing required fields)",
|
|
404
431
|
500,
|
|
@@ -407,9 +434,9 @@ var _BridgeClient = class _BridgeClient {
|
|
|
407
434
|
);
|
|
408
435
|
}
|
|
409
436
|
return {
|
|
410
|
-
checkoutUrl:
|
|
411
|
-
checkoutSessionId:
|
|
412
|
-
expiresAt:
|
|
437
|
+
checkoutUrl: data.checkoutUrl,
|
|
438
|
+
checkoutSessionId: data.checkoutSessionId,
|
|
439
|
+
expiresAt: data.expiresAt
|
|
413
440
|
};
|
|
414
441
|
}
|
|
415
442
|
/* ────────────────────────────────────────────────────────────────────
|
|
@@ -1217,13 +1244,16 @@ var _BridgeClient = class _BridgeClient {
|
|
|
1217
1244
|
if (!resp.ok) return emptyPage;
|
|
1218
1245
|
try {
|
|
1219
1246
|
const body = await resp.json();
|
|
1220
|
-
if (!body || typeof body !== "object")
|
|
1247
|
+
if (!body || typeof body !== "object") {
|
|
1248
|
+
return emptyPage;
|
|
1249
|
+
}
|
|
1250
|
+
const data = body.data ?? body;
|
|
1221
1251
|
return {
|
|
1222
|
-
content: Array.isArray(
|
|
1223
|
-
totalElements: typeof
|
|
1224
|
-
totalPages: typeof
|
|
1225
|
-
page: typeof
|
|
1226
|
-
size: typeof
|
|
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
|
|
1227
1257
|
};
|
|
1228
1258
|
} catch {
|
|
1229
1259
|
return emptyPage;
|
|
@@ -2727,6 +2757,20 @@ function safeFormatCurrency(priceCents, currency, locale, intlOverride) {
|
|
|
2727
2757
|
return `${currency} ${amount.toFixed(2)}`;
|
|
2728
2758
|
}
|
|
2729
2759
|
}
|
|
2760
|
+
function resolveDisplayPrice(offering, locale, intlOverride) {
|
|
2761
|
+
const flat = safeFormatCurrency(offering.priceCents, offering.currency, locale, intlOverride);
|
|
2762
|
+
if (flat !== "\u2014") return { text: flat, unitSuffix: null };
|
|
2763
|
+
const perUnitPlan = offering.ratePlans?.find(
|
|
2764
|
+
(rp) => rp.pricingModel === "PER_UNIT" && rp.perUnitPriceCents != null
|
|
2765
|
+
);
|
|
2766
|
+
if (perUnitPlan) {
|
|
2767
|
+
const unitPrice = safeFormatCurrency(perUnitPlan.perUnitPriceCents, offering.currency, locale, intlOverride);
|
|
2768
|
+
if (unitPrice !== "\u2014") {
|
|
2769
|
+
return { text: unitPrice, unitSuffix: perUnitPlan.unitLabel || "unit" };
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
return { text: "\u2014", unitSuffix: null };
|
|
2773
|
+
}
|
|
2730
2774
|
function localizedName(o) {
|
|
2731
2775
|
return o.localizedDisplayName || o.name || "(unnamed plan)";
|
|
2732
2776
|
}
|
|
@@ -3130,12 +3174,7 @@ function PlanCard({
|
|
|
3130
3174
|
onCtaClick,
|
|
3131
3175
|
intlOverride
|
|
3132
3176
|
}) {
|
|
3133
|
-
const price =
|
|
3134
|
-
offering.priceCents,
|
|
3135
|
-
offering.currency,
|
|
3136
|
-
locale,
|
|
3137
|
-
intlOverride
|
|
3138
|
-
);
|
|
3177
|
+
const { text: price, unitSuffix } = resolveDisplayPrice(offering, locale, intlOverride);
|
|
3139
3178
|
const isContactSales = price === "\u2014";
|
|
3140
3179
|
const planName = localizedName(offering);
|
|
3141
3180
|
const description = localizedDescription(offering);
|
|
@@ -3156,7 +3195,7 @@ function PlanCard({
|
|
|
3156
3195
|
"div",
|
|
3157
3196
|
{
|
|
3158
3197
|
role: "group",
|
|
3159
|
-
"aria-label": `${planName}, ${price} per ${offering.billingCycle}`,
|
|
3198
|
+
"aria-label": `${planName}, ${price} per ${unitSuffix ?? offering.billingCycle}`,
|
|
3160
3199
|
className: `aforo-w-pc-card${featured ? " aforo-w-pc-card-featured" : ""}`,
|
|
3161
3200
|
style: cardStyle2,
|
|
3162
3201
|
children: [
|
|
@@ -3221,14 +3260,14 @@ function PlanCard({
|
|
|
3221
3260
|
children: price
|
|
3222
3261
|
}
|
|
3223
3262
|
),
|
|
3224
|
-
!isContactSales && offering.billingCycle ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3263
|
+
!isContactSales && (unitSuffix || offering.billingCycle) ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3225
3264
|
"span",
|
|
3226
3265
|
{
|
|
3227
3266
|
className: "aforo-w-pc-cycle",
|
|
3228
3267
|
style: { fontSize: 13, color: tokens.textMuted },
|
|
3229
3268
|
children: [
|
|
3230
3269
|
"/ ",
|
|
3231
|
-
humanCycle(offering.billingCycle)
|
|
3270
|
+
unitSuffix ?? humanCycle(offering.billingCycle)
|
|
3232
3271
|
]
|
|
3233
3272
|
}
|
|
3234
3273
|
) : null
|
|
@@ -3386,6 +3425,7 @@ function TableLayout({
|
|
|
3386
3425
|
/* @__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" }) }),
|
|
3387
3426
|
offerings.map((o) => {
|
|
3388
3427
|
const featured = offeringIsFeatured(o, featuredOverride);
|
|
3428
|
+
const { text: tablePrice, unitSuffix: tableUnitSuffix } = resolveDisplayPrice(o, locale, intlOverride);
|
|
3389
3429
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3390
3430
|
"th",
|
|
3391
3431
|
{
|
|
@@ -3415,13 +3455,20 @@ function TableLayout({
|
|
|
3415
3455
|
children: "Most popular"
|
|
3416
3456
|
}
|
|
3417
3457
|
) : null,
|
|
3418
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3458
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 22, fontWeight: 700 }, children: [
|
|
3459
|
+
tablePrice,
|
|
3460
|
+
tableUnitSuffix ? /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontSize: 12, fontWeight: 400, color: tokens.textMuted }, children: [
|
|
3461
|
+
" ",
|
|
3462
|
+
"/ ",
|
|
3463
|
+
tableUnitSuffix
|
|
3464
|
+
] }) : null
|
|
3465
|
+
] }),
|
|
3419
3466
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3420
3467
|
"button",
|
|
3421
3468
|
{
|
|
3422
3469
|
type: "button",
|
|
3423
3470
|
onClick: () => onCtaClick(o),
|
|
3424
|
-
"aria-label": `${ctaLabel(o, ctaTextProp,
|
|
3471
|
+
"aria-label": `${ctaLabel(o, ctaTextProp, tablePrice === "\u2014")} for ${localizedName(o)}`,
|
|
3425
3472
|
style: {
|
|
3426
3473
|
inlineSize: "100%",
|
|
3427
3474
|
padding: "8px 12px",
|
|
@@ -3434,7 +3481,7 @@ function TableLayout({
|
|
|
3434
3481
|
cursor: "pointer",
|
|
3435
3482
|
fontFamily: tokens.fontFamily
|
|
3436
3483
|
},
|
|
3437
|
-
children: ctaLabel(o, ctaTextProp,
|
|
3484
|
+
children: ctaLabel(o, ctaTextProp, tablePrice === "\u2014")
|
|
3438
3485
|
}
|
|
3439
3486
|
)
|
|
3440
3487
|
] })
|
|
@@ -12151,7 +12198,7 @@ function ResultPanel({
|
|
|
12151
12198
|
}
|
|
12152
12199
|
|
|
12153
12200
|
// src/core/version.ts
|
|
12154
|
-
var VERSION = "1.0.
|
|
12201
|
+
var VERSION = "1.0.5";
|
|
12155
12202
|
|
|
12156
12203
|
// src/core/AforoEmbed.ts
|
|
12157
12204
|
var mountedElements = /* @__PURE__ */ new WeakMap();
|
package/dist/index.d.cts
CHANGED
|
@@ -639,7 +639,7 @@ declare function AforoUpgradeCancel(props: AforoUpgradeCancelProps): React.React
|
|
|
639
639
|
* POST /api/v1/portal/embed/bridge/exchange — bridge JWT → session JWT
|
|
640
640
|
* POST /api/v1/portal/embed/bridge/refresh — session JWT refresh (Prompt 8)
|
|
641
641
|
* POST /api/v1/portal/embed/telemetry — telemetry batch sink
|
|
642
|
-
* GET /api/v1/portal/embed/tenant-config/{slug} — brand kit
|
|
642
|
+
* GET /api/v1/portal/embed/tenant-config/{slug} — brand kit + offerings + embeddableWidget
|
|
643
643
|
* GET /api/v1/portal/embed/health — public liveness
|
|
644
644
|
*
|
|
645
645
|
* Headers:
|
|
@@ -707,10 +707,23 @@ declare class BridgeClient {
|
|
|
707
707
|
/**
|
|
708
708
|
* Headless config — the PricingCard widget's primary read path.
|
|
709
709
|
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
710
|
+
* Hits `GET /api/v1/portal/embed/tenant-config/{slug}` — the SAME
|
|
711
|
+
* embed-key-authenticated endpoint {@link getTenantBrandKit} uses, now
|
|
712
|
+
* extended server-side with `offerings` + `embeddableWidget`. This was
|
|
713
|
+
* previously wired to `GET /api/v1/portal/headless/config`, an unrelated
|
|
714
|
+
* T6 Headless-tier endpoint that requires `X-Storefront-Key`/
|
|
715
|
+
* `X-Tenant-Id` auth — headers the embed key flow never sends. Every
|
|
716
|
+
* embed-tier fetch against that URL failed 400 "Missing tenant
|
|
717
|
+
* identification" by construction; this endpoint is scoped to the same
|
|
718
|
+
* `EmbedKeyAuthFilter` gate as the rest of the embed plane, so the
|
|
719
|
+
* `Authorization: Bearer <embed_key>` header `buildHeaders()` already
|
|
720
|
+
* sends is sufficient.
|
|
721
|
+
*
|
|
722
|
+
* The backend response is still a flat record (`tenantSlug,
|
|
723
|
+
* primaryColor, secondaryColor, logoUrl, fontFamily, offerings,
|
|
724
|
+
* embeddableWidget`) — kept flat so `getTenantBrandKit()`'s existing
|
|
725
|
+
* consumers (the ThemeReader cascade) are unaffected. This method nests
|
|
726
|
+
* the branding fields into the shape `HeadlessConfigResponse` expects.
|
|
714
727
|
*
|
|
715
728
|
* Pattern #18 fail-soft — returns `null` on transport failure, 404, or
|
|
716
729
|
* malformed JSON. Widgets fall through to their empty / error state and
|
package/dist/index.d.ts
CHANGED
|
@@ -639,7 +639,7 @@ declare function AforoUpgradeCancel(props: AforoUpgradeCancelProps): React.React
|
|
|
639
639
|
* POST /api/v1/portal/embed/bridge/exchange — bridge JWT → session JWT
|
|
640
640
|
* POST /api/v1/portal/embed/bridge/refresh — session JWT refresh (Prompt 8)
|
|
641
641
|
* POST /api/v1/portal/embed/telemetry — telemetry batch sink
|
|
642
|
-
* GET /api/v1/portal/embed/tenant-config/{slug} — brand kit
|
|
642
|
+
* GET /api/v1/portal/embed/tenant-config/{slug} — brand kit + offerings + embeddableWidget
|
|
643
643
|
* GET /api/v1/portal/embed/health — public liveness
|
|
644
644
|
*
|
|
645
645
|
* Headers:
|
|
@@ -707,10 +707,23 @@ declare class BridgeClient {
|
|
|
707
707
|
/**
|
|
708
708
|
* Headless config — the PricingCard widget's primary read path.
|
|
709
709
|
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
710
|
+
* Hits `GET /api/v1/portal/embed/tenant-config/{slug}` — the SAME
|
|
711
|
+
* embed-key-authenticated endpoint {@link getTenantBrandKit} uses, now
|
|
712
|
+
* extended server-side with `offerings` + `embeddableWidget`. This was
|
|
713
|
+
* previously wired to `GET /api/v1/portal/headless/config`, an unrelated
|
|
714
|
+
* T6 Headless-tier endpoint that requires `X-Storefront-Key`/
|
|
715
|
+
* `X-Tenant-Id` auth — headers the embed key flow never sends. Every
|
|
716
|
+
* embed-tier fetch against that URL failed 400 "Missing tenant
|
|
717
|
+
* identification" by construction; this endpoint is scoped to the same
|
|
718
|
+
* `EmbedKeyAuthFilter` gate as the rest of the embed plane, so the
|
|
719
|
+
* `Authorization: Bearer <embed_key>` header `buildHeaders()` already
|
|
720
|
+
* sends is sufficient.
|
|
721
|
+
*
|
|
722
|
+
* The backend response is still a flat record (`tenantSlug,
|
|
723
|
+
* primaryColor, secondaryColor, logoUrl, fontFamily, offerings,
|
|
724
|
+
* embeddableWidget`) — kept flat so `getTenantBrandKit()`'s existing
|
|
725
|
+
* consumers (the ThemeReader cascade) are unaffected. This method nests
|
|
726
|
+
* the branding fields into the shape `HeadlessConfigResponse` expects.
|
|
714
727
|
*
|
|
715
728
|
* Pattern #18 fail-soft — returns `null` on transport failure, 404, or
|
|
716
729
|
* malformed JSON. Widgets fall through to their empty / error state and
|
package/dist/index.mjs
CHANGED
|
@@ -149,7 +149,8 @@ var _BridgeClient = class _BridgeClient {
|
|
|
149
149
|
throw await this.parseError(resp);
|
|
150
150
|
}
|
|
151
151
|
const body = await resp.json();
|
|
152
|
-
|
|
152
|
+
const data = body?.data ?? body;
|
|
153
|
+
if (!data || !data.sessionJwt || typeof data.expiresAt !== "number") {
|
|
153
154
|
throw new BridgeClientError(
|
|
154
155
|
"Malformed bridge exchange response",
|
|
155
156
|
500,
|
|
@@ -158,9 +159,9 @@ var _BridgeClient = class _BridgeClient {
|
|
|
158
159
|
);
|
|
159
160
|
}
|
|
160
161
|
return {
|
|
161
|
-
sessionJwt:
|
|
162
|
-
expiresAt:
|
|
163
|
-
customerId:
|
|
162
|
+
sessionJwt: data.sessionJwt,
|
|
163
|
+
expiresAt: data.expiresAt,
|
|
164
|
+
customerId: data.customerId ?? null
|
|
164
165
|
};
|
|
165
166
|
}
|
|
166
167
|
/**
|
|
@@ -201,10 +202,23 @@ var _BridgeClient = class _BridgeClient {
|
|
|
201
202
|
/**
|
|
202
203
|
* Headless config — the PricingCard widget's primary read path.
|
|
203
204
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
205
|
+
* Hits `GET /api/v1/portal/embed/tenant-config/{slug}` — the SAME
|
|
206
|
+
* embed-key-authenticated endpoint {@link getTenantBrandKit} uses, now
|
|
207
|
+
* extended server-side with `offerings` + `embeddableWidget`. This was
|
|
208
|
+
* previously wired to `GET /api/v1/portal/headless/config`, an unrelated
|
|
209
|
+
* T6 Headless-tier endpoint that requires `X-Storefront-Key`/
|
|
210
|
+
* `X-Tenant-Id` auth — headers the embed key flow never sends. Every
|
|
211
|
+
* embed-tier fetch against that URL failed 400 "Missing tenant
|
|
212
|
+
* identification" by construction; this endpoint is scoped to the same
|
|
213
|
+
* `EmbedKeyAuthFilter` gate as the rest of the embed plane, so the
|
|
214
|
+
* `Authorization: Bearer <embed_key>` header `buildHeaders()` already
|
|
215
|
+
* sends is sufficient.
|
|
216
|
+
*
|
|
217
|
+
* The backend response is still a flat record (`tenantSlug,
|
|
218
|
+
* primaryColor, secondaryColor, logoUrl, fontFamily, offerings,
|
|
219
|
+
* embeddableWidget`) — kept flat so `getTenantBrandKit()`'s existing
|
|
220
|
+
* consumers (the ThemeReader cascade) are unaffected. This method nests
|
|
221
|
+
* the branding fields into the shape `HeadlessConfigResponse` expects.
|
|
208
222
|
*
|
|
209
223
|
* Pattern #18 fail-soft — returns `null` on transport failure, 404, or
|
|
210
224
|
* malformed JSON. Widgets fall through to their empty / error state and
|
|
@@ -219,7 +233,7 @@ var _BridgeClient = class _BridgeClient {
|
|
|
219
233
|
}
|
|
220
234
|
const target = slug || this.tenantSlug;
|
|
221
235
|
if (!target) return null;
|
|
222
|
-
const url = `${this.baseUrl}/api/v1/portal/
|
|
236
|
+
const url = `${this.baseUrl}/api/v1/portal/embed/tenant-config/${encodeURIComponent(target)}`;
|
|
223
237
|
let resp;
|
|
224
238
|
try {
|
|
225
239
|
resp = await this.send(url, {
|
|
@@ -232,7 +246,19 @@ var _BridgeClient = class _BridgeClient {
|
|
|
232
246
|
if (!resp.ok) return null;
|
|
233
247
|
try {
|
|
234
248
|
const body = await resp.json();
|
|
235
|
-
|
|
249
|
+
if (!body || typeof body !== "object") return null;
|
|
250
|
+
const data = body.data ?? body;
|
|
251
|
+
return {
|
|
252
|
+
tenantSlug: data.tenantSlug,
|
|
253
|
+
branding: {
|
|
254
|
+
primaryColor: data.primaryColor ?? null,
|
|
255
|
+
secondaryColor: data.secondaryColor ?? null,
|
|
256
|
+
logoUrl: data.logoUrl ?? null,
|
|
257
|
+
fontFamily: data.fontFamily ?? null
|
|
258
|
+
},
|
|
259
|
+
offerings: Array.isArray(data.offerings) ? data.offerings : [],
|
|
260
|
+
embeddableWidget: data.embeddableWidget ?? null
|
|
261
|
+
};
|
|
236
262
|
} catch {
|
|
237
263
|
return null;
|
|
238
264
|
}
|
|
@@ -376,7 +402,8 @@ var _BridgeClient = class _BridgeClient {
|
|
|
376
402
|
false
|
|
377
403
|
);
|
|
378
404
|
}
|
|
379
|
-
|
|
405
|
+
const data = body?.data ?? body ?? {};
|
|
406
|
+
if (!data.checkoutUrl || !data.checkoutSessionId || !data.expiresAt) {
|
|
380
407
|
throw new BridgeClientError(
|
|
381
408
|
"Malformed checkout-initiate response (missing required fields)",
|
|
382
409
|
500,
|
|
@@ -385,9 +412,9 @@ var _BridgeClient = class _BridgeClient {
|
|
|
385
412
|
);
|
|
386
413
|
}
|
|
387
414
|
return {
|
|
388
|
-
checkoutUrl:
|
|
389
|
-
checkoutSessionId:
|
|
390
|
-
expiresAt:
|
|
415
|
+
checkoutUrl: data.checkoutUrl,
|
|
416
|
+
checkoutSessionId: data.checkoutSessionId,
|
|
417
|
+
expiresAt: data.expiresAt
|
|
391
418
|
};
|
|
392
419
|
}
|
|
393
420
|
/* ────────────────────────────────────────────────────────────────────
|
|
@@ -1195,13 +1222,16 @@ var _BridgeClient = class _BridgeClient {
|
|
|
1195
1222
|
if (!resp.ok) return emptyPage;
|
|
1196
1223
|
try {
|
|
1197
1224
|
const body = await resp.json();
|
|
1198
|
-
if (!body || typeof body !== "object")
|
|
1225
|
+
if (!body || typeof body !== "object") {
|
|
1226
|
+
return emptyPage;
|
|
1227
|
+
}
|
|
1228
|
+
const data = body.data ?? body;
|
|
1199
1229
|
return {
|
|
1200
|
-
content: Array.isArray(
|
|
1201
|
-
totalElements: typeof
|
|
1202
|
-
totalPages: typeof
|
|
1203
|
-
page: typeof
|
|
1204
|
-
size: typeof
|
|
1230
|
+
content: Array.isArray(data.content) ? data.content : [],
|
|
1231
|
+
totalElements: typeof data.totalElements === "number" && Number.isFinite(data.totalElements) ? data.totalElements : 0,
|
|
1232
|
+
totalPages: typeof data.totalPages === "number" && Number.isFinite(data.totalPages) ? data.totalPages : 0,
|
|
1233
|
+
page: typeof data.page === "number" && Number.isFinite(data.page) ? data.page : filter.page ?? 0,
|
|
1234
|
+
size: typeof data.size === "number" && Number.isFinite(data.size) ? data.size : filter.size ?? 10
|
|
1205
1235
|
};
|
|
1206
1236
|
} catch {
|
|
1207
1237
|
return emptyPage;
|
|
@@ -2705,6 +2735,20 @@ function safeFormatCurrency(priceCents, currency, locale, intlOverride) {
|
|
|
2705
2735
|
return `${currency} ${amount.toFixed(2)}`;
|
|
2706
2736
|
}
|
|
2707
2737
|
}
|
|
2738
|
+
function resolveDisplayPrice(offering, locale, intlOverride) {
|
|
2739
|
+
const flat = safeFormatCurrency(offering.priceCents, offering.currency, locale, intlOverride);
|
|
2740
|
+
if (flat !== "\u2014") return { text: flat, unitSuffix: null };
|
|
2741
|
+
const perUnitPlan = offering.ratePlans?.find(
|
|
2742
|
+
(rp) => rp.pricingModel === "PER_UNIT" && rp.perUnitPriceCents != null
|
|
2743
|
+
);
|
|
2744
|
+
if (perUnitPlan) {
|
|
2745
|
+
const unitPrice = safeFormatCurrency(perUnitPlan.perUnitPriceCents, offering.currency, locale, intlOverride);
|
|
2746
|
+
if (unitPrice !== "\u2014") {
|
|
2747
|
+
return { text: unitPrice, unitSuffix: perUnitPlan.unitLabel || "unit" };
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
return { text: "\u2014", unitSuffix: null };
|
|
2751
|
+
}
|
|
2708
2752
|
function localizedName(o) {
|
|
2709
2753
|
return o.localizedDisplayName || o.name || "(unnamed plan)";
|
|
2710
2754
|
}
|
|
@@ -3108,12 +3152,7 @@ function PlanCard({
|
|
|
3108
3152
|
onCtaClick,
|
|
3109
3153
|
intlOverride
|
|
3110
3154
|
}) {
|
|
3111
|
-
const price =
|
|
3112
|
-
offering.priceCents,
|
|
3113
|
-
offering.currency,
|
|
3114
|
-
locale,
|
|
3115
|
-
intlOverride
|
|
3116
|
-
);
|
|
3155
|
+
const { text: price, unitSuffix } = resolveDisplayPrice(offering, locale, intlOverride);
|
|
3117
3156
|
const isContactSales = price === "\u2014";
|
|
3118
3157
|
const planName = localizedName(offering);
|
|
3119
3158
|
const description = localizedDescription(offering);
|
|
@@ -3134,7 +3173,7 @@ function PlanCard({
|
|
|
3134
3173
|
"div",
|
|
3135
3174
|
{
|
|
3136
3175
|
role: "group",
|
|
3137
|
-
"aria-label": `${planName}, ${price} per ${offering.billingCycle}`,
|
|
3176
|
+
"aria-label": `${planName}, ${price} per ${unitSuffix ?? offering.billingCycle}`,
|
|
3138
3177
|
className: `aforo-w-pc-card${featured ? " aforo-w-pc-card-featured" : ""}`,
|
|
3139
3178
|
style: cardStyle2,
|
|
3140
3179
|
children: [
|
|
@@ -3199,14 +3238,14 @@ function PlanCard({
|
|
|
3199
3238
|
children: price
|
|
3200
3239
|
}
|
|
3201
3240
|
),
|
|
3202
|
-
!isContactSales && offering.billingCycle ? /* @__PURE__ */ jsxs(
|
|
3241
|
+
!isContactSales && (unitSuffix || offering.billingCycle) ? /* @__PURE__ */ jsxs(
|
|
3203
3242
|
"span",
|
|
3204
3243
|
{
|
|
3205
3244
|
className: "aforo-w-pc-cycle",
|
|
3206
3245
|
style: { fontSize: 13, color: tokens.textMuted },
|
|
3207
3246
|
children: [
|
|
3208
3247
|
"/ ",
|
|
3209
|
-
humanCycle(offering.billingCycle)
|
|
3248
|
+
unitSuffix ?? humanCycle(offering.billingCycle)
|
|
3210
3249
|
]
|
|
3211
3250
|
}
|
|
3212
3251
|
) : null
|
|
@@ -3364,6 +3403,7 @@ function TableLayout({
|
|
|
3364
3403
|
/* @__PURE__ */ jsx("th", { scope: "col", style: { ...headerCellStyle, color: tokens.textMuted, fontSize: 12 }, children: /* @__PURE__ */ jsx("span", { className: "aforo-w-pc-sr-only", style: srOnlyStyle, children: "Feature" }) }),
|
|
3365
3404
|
offerings.map((o) => {
|
|
3366
3405
|
const featured = offeringIsFeatured(o, featuredOverride);
|
|
3406
|
+
const { text: tablePrice, unitSuffix: tableUnitSuffix } = resolveDisplayPrice(o, locale, intlOverride);
|
|
3367
3407
|
return /* @__PURE__ */ jsx(
|
|
3368
3408
|
"th",
|
|
3369
3409
|
{
|
|
@@ -3393,13 +3433,20 @@ function TableLayout({
|
|
|
3393
3433
|
children: "Most popular"
|
|
3394
3434
|
}
|
|
3395
3435
|
) : null,
|
|
3396
|
-
/* @__PURE__ */
|
|
3436
|
+
/* @__PURE__ */ jsxs("div", { style: { fontSize: 22, fontWeight: 700 }, children: [
|
|
3437
|
+
tablePrice,
|
|
3438
|
+
tableUnitSuffix ? /* @__PURE__ */ jsxs("span", { style: { fontSize: 12, fontWeight: 400, color: tokens.textMuted }, children: [
|
|
3439
|
+
" ",
|
|
3440
|
+
"/ ",
|
|
3441
|
+
tableUnitSuffix
|
|
3442
|
+
] }) : null
|
|
3443
|
+
] }),
|
|
3397
3444
|
/* @__PURE__ */ jsx(
|
|
3398
3445
|
"button",
|
|
3399
3446
|
{
|
|
3400
3447
|
type: "button",
|
|
3401
3448
|
onClick: () => onCtaClick(o),
|
|
3402
|
-
"aria-label": `${ctaLabel(o, ctaTextProp,
|
|
3449
|
+
"aria-label": `${ctaLabel(o, ctaTextProp, tablePrice === "\u2014")} for ${localizedName(o)}`,
|
|
3403
3450
|
style: {
|
|
3404
3451
|
inlineSize: "100%",
|
|
3405
3452
|
padding: "8px 12px",
|
|
@@ -3412,7 +3459,7 @@ function TableLayout({
|
|
|
3412
3459
|
cursor: "pointer",
|
|
3413
3460
|
fontFamily: tokens.fontFamily
|
|
3414
3461
|
},
|
|
3415
|
-
children: ctaLabel(o, ctaTextProp,
|
|
3462
|
+
children: ctaLabel(o, ctaTextProp, tablePrice === "\u2014")
|
|
3416
3463
|
}
|
|
3417
3464
|
)
|
|
3418
3465
|
] })
|
|
@@ -12129,7 +12176,7 @@ function ResultPanel({
|
|
|
12129
12176
|
}
|
|
12130
12177
|
|
|
12131
12178
|
// src/core/version.ts
|
|
12132
|
-
var VERSION = "1.0.
|
|
12179
|
+
var VERSION = "1.0.5";
|
|
12133
12180
|
|
|
12134
12181
|
// src/core/AforoEmbed.ts
|
|
12135
12182
|
var mountedElements = /* @__PURE__ */ new WeakMap();
|
package/dist/loader.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var AforoEmbedLoader=(function(exports){'use strict';var T="1.0.
|
|
1
|
+
var AforoEmbedLoader=(function(exports){'use strict';var T="1.0.5";var y=new WeakMap,c=new Map,f={},A=false;function h(){A||typeof window!="undefined"&&(window.addEventListener("message",e=>{let t=e.data;!t||typeof t!="object"||typeof t.type!="string"||t.type.startsWith("aforo.")&&$(t);}),A=true);}function $(e){let t=c.get(e.type);if(t)for(let o of t)try{o(e);}catch(i){}let n=c.get("*");if(n)for(let o of n)try{o(e);}catch(i){}}var p=new Map;function N(e,t){p.set(e,t);}function w(e){return typeof e=="object"&&e!==null&&typeof e.tagName=="string"}var b={version:T,configure(e){f={...f,...e};},getConfig(){return {...f}},mount(e,t){var r,l,u,s,d;if(!w(e)||y.has(e))return false;let n=t.widget,o=p.get(n);if(!o)return typeof console!="undefined"&&console.warn(`[aforo:embed] No mount handler registered for widget "${n}". Loader bundle may not have finished loading.`),false;let i={widget:n,tenantSlug:(l=(r=t.tenantSlug)!=null?r:f.tenantSlug)!=null?l:"",embedKey:(s=(u=t.embedKey)!=null?u:f.embedKey)!=null?s:"",bridgeToken:(d=t.bridgeToken)!=null?d:f.bridgeToken,layout:t.layout,themeOverrides:t.themeOverrides,config:t.config};if(!i.tenantSlug||!i.embedKey)return typeof console!="undefined"&&console.error(`[aforo:embed] Widget "${n}" requires tenantSlug + embedKey. Provide via AforoEmbed.configure() or data-* attributes.`),false;h();let a=o(e,i);return y.set(e,{element:e,widget:n,teardown:a}),true},unmount(e){if(!w(e))return false;let t=y.get(e);if(!t)return false;try{t.teardown();}catch(n){}return y.delete(e),true},on(e,t){h();let n=c.get(e);return n||(n=new Set,c.set(e,n)),n.add(t),()=>{let o=c.get(e);o&&(o.delete(t),o.size===0&&c.delete(e));}},_registerWidget:N,_registeredWidgetsForTesting(){return Array.from(p.keys())},_resetForTesting(){p.clear(),c.clear(),f={};}};function k(e){if(!e)return {};try{let t=JSON.parse(e);if(t&&typeof t=="object"&&!Array.isArray(t)){let n={},o=["primary","primaryContrast","text","textMuted","bg","border","radius","fontFamily"],i=t;for(let a of o){let r=i[a];typeof r=="string"&&r.length>0&&(n[a]=r);}return n}}catch(t){}return {}}var U=new Set(["pricing-card","subscribe-button","checkout-flow","subscription-manager","invoice-list","usage-meter","payment-method","upgrade-cancel"]),B="https://embed.aforo.ai/v1/widgets",g="[aforo:loader]";function D(){let e=typeof document!="undefined"?document.currentScript:null;if(e&&e.tagName==="SCRIPT")return e;if(typeof document!="undefined"){let t=document.getElementsByTagName("script");for(let n=t.length-1;n>=0;n--){let o=t[n];if(!o)continue;let i=o.getAttribute("src")||"";if(/(\/v1)?\/loader(\.umd)?\.js$/.test(i))return o}}return null}function G(e){let t=e==null?void 0:e.getAttribute("src");if(!t)return null;try{let n=typeof location!="undefined"?location.href:void 0,o=new URL(t,n),i=o.pathname.replace(/\/loader(\.umd)?\.js$/,"");return `${o.origin}${i}/widgets`}catch(n){return null}}function v(e,t){let n=e.getAttribute(t);if(n==null||n.trim()==="")return;let o=Number(n);return Number.isFinite(o)?o:void 0}function j(e,t){let n=e.getAttribute(t);if(n==null)return;let o=n.trim().toLowerCase();if(o==="true")return true;if(o==="false")return false}function V(e){let t=(e.getAttribute("data-aforo-widget")||"").trim(),n=e.getAttribute("data-tenant-slug")||e.getAttribute("data-tenant")||"",o=e.getAttribute("data-embed-key")||"",i=e.getAttribute("data-bridge-token")||void 0,a=e.getAttribute("data-layout")||void 0,r=k(e.getAttribute("data-theme-overrides")),l=e.getAttribute("data-offering-id")||void 0,u=e.getAttribute("data-subscription-id")||void 0,s=e.getAttribute("data-metric")||void 0,d=e.getAttribute("data-featured-offering-id")||void 0,m=e.getAttribute("data-mode")||void 0,M=e.getAttribute("data-return-url")||void 0,S=e.getAttribute("data-default-status")||void 0,F=e.getAttribute("data-render-mode")||void 0,_=e.getAttribute("data-locale")||void 0,C=v(e,"data-page-size"),O=v(e,"data-poll-interval-ms"),I=e.getAttribute("data-theme")||void 0,R=e.getAttribute("data-cart-type")||void 0,W=e.getAttribute("data-target-id")||void 0,H=e.getAttribute("data-cta-text")||void 0,x=e.getAttribute("data-cta-url")||void 0,P=v(e,"data-max-plans"),K=j(e,"data-show-features");return {widget:t,tenantSlug:n,embedKey:o,bridgeToken:i,layout:a,themeOverrides:r,config:{offeringId:l,subscriptionId:u,metricName:s,featuredOfferingId:d,mode:m,returnUrl:M,defaultStatus:S,renderMode:F,locale:_,pageSize:C,pollIntervalMs:O,theme:I,cartType:R,targetId:W,ctaText:H,ctaUrl:x,maxPlans:P,showFeatures:K}}}function z(e,t,n){return new Promise((o,i)=>{if(typeof document=="undefined")return i(new Error("no document"));let a=document.querySelector(`script[data-aforo-bundle="${e}"]`);if(a){if(a.dataset.aforoLoaded==="1")return o();a.addEventListener("load",()=>o()),a.addEventListener("error",()=>i(new Error(`bundle load failed: ${e}`)));return}let r=document.createElement("script");r.src=`${t}/${e}.js`,r.async=true,r.crossOrigin="anonymous",r.dataset.aforoBundle=e,n&&r.setAttribute("nonce",n),r.addEventListener("load",()=>{r.dataset.aforoLoaded="1",o();}),r.addEventListener("error",()=>i(new Error(`bundle load failed: ${e}`))),document.head.appendChild(r);})}function L(e){if(!e.__aforoEmbedReadyFired){e.__aforoEmbedReadyFired=true;try{typeof e.aforoEmbedReady=="function"&&e.aforoEmbedReady();}catch(t){typeof console!="undefined"&&console.error(`${g} aforoEmbedReady callback threw`,t);}if(typeof document!="undefined"&&typeof CustomEvent!="undefined")try{document.dispatchEvent(new CustomEvent("aforoEmbedReady"));}catch(t){}}}async function E(e){if(typeof window=="undefined"||typeof document=="undefined")return;let t=window;if(window.location.protocol!=="https:"&&window.location.hostname!=="localhost"){typeof console!="undefined"&&console.error(`${g} Refusing to mount Aforo widgets on a non-HTTPS page. Embed Plugin requires HTTPS (FR-TIER-12). Current origin: ${window.location.origin}`);return}if(t.__aforoEmbedLoaderInitialized){typeof console!="undefined"&&console.warn(`${g} Loader already initialised on this page \u2014 skipping duplicate run.`);return}t.__aforoEmbedLoaderInitialized=true,t.aforoEmbed||(t.aforoEmbed=b);let n=D(),o=(n==null?void 0:n.getAttribute("nonce"))||null,i=(e==null?void 0:e.bundleBaseUrl)||G(n)||B,a=Array.from(document.querySelectorAll("[data-aforo-widget]"));if(a.length===0){L(t);return}let r=new Map;for(let u of a){let s=(u.getAttribute("data-aforo-widget")||"").trim();if(!U.has(s)){typeof console!="undefined"&&console.warn(`${g} Unknown widget id "${s}" \u2014 skipping`);continue}let d=r.get(s);d||(d=[],r.set(s,d)),d.push(u);}let l=Array.from(r.entries()).map(async([u,s])=>{try{await z(u,i,o);for(let d of s){let m=V(d);if(!m.widget||!m.tenantSlug||!m.embedKey){typeof console!="undefined"&&console.error(`${g} Placeholder missing required attributes (data-tenant-slug + data-embed-key). Skipping element.`,d);continue}b.mount(d,m);}}catch(d){typeof console!="undefined"&&console.error(`${g} Failed to load bundle for "${u}"`,d);}});await Promise.all(l).catch(()=>{}),L(t);}typeof document!="undefined"&&document.readyState!=="loading"?Promise.resolve().then(()=>{E();}):typeof document!="undefined"&&document.addEventListener("DOMContentLoaded",()=>{E();});b._loader={bootstrap:E};
|
|
2
2
|
exports.AforoEmbed=b;exports.bootstrap=E;return exports;})({});//# sourceMappingURL=loader.js.map
|
|
3
3
|
//# sourceMappingURL=loader.js.map
|