@appfunnel-dev/sdk 2.0.0-canary.5 → 2.0.0-canary.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{capabilities-7_hy5f5G.d.cts → capabilities-CsnkVwna.d.cts} +34 -1
- package/dist/{capabilities-7_hy5f5G.d.ts → capabilities-CsnkVwna.d.ts} +34 -1
- package/dist/{checkout-7Dy6IedP.d.ts → checkout-3PkKwPd7.d.ts} +14 -14
- package/dist/{checkout-Dz8cGkB_.d.cts → checkout-Cg20l53c.d.cts} +14 -14
- package/dist/{chunk-VAOR77NW.cjs → chunk-FY3TMSRU.cjs} +7 -7
- package/dist/{chunk-VAOR77NW.cjs.map → chunk-FY3TMSRU.cjs.map} +1 -1
- package/dist/{chunk-CFFMZYPE.js → chunk-PWXMQGO6.js} +3 -3
- package/dist/{chunk-CFFMZYPE.js.map → chunk-PWXMQGO6.js.map} +1 -1
- package/dist/{chunk-TNLRQPVY.js → chunk-RVJLR4RS.js} +36 -8
- package/dist/chunk-RVJLR4RS.js.map +1 -0
- package/dist/{chunk-QMAZGLGV.cjs → chunk-RY3LOSVZ.cjs} +37 -7
- package/dist/chunk-RY3LOSVZ.cjs.map +1 -0
- package/dist/{chunk-AXXG3ODP.js → chunk-VW2HVPR4.js} +63 -90
- package/dist/chunk-VW2HVPR4.js.map +1 -0
- package/dist/{chunk-G44BUG6G.cjs → chunk-Y4YNJ2EX.cjs} +65 -93
- package/dist/chunk-Y4YNJ2EX.cjs.map +1 -0
- package/dist/driver-paddle.cjs +22 -22
- package/dist/driver-paddle.d.cts +2 -2
- package/dist/driver-paddle.d.ts +2 -2
- package/dist/driver-paddle.js +2 -2
- package/dist/driver-stripe.cjs +264 -212
- package/dist/driver-stripe.cjs.map +1 -1
- package/dist/driver-stripe.d.cts +19 -3
- package/dist/driver-stripe.d.ts +19 -3
- package/dist/driver-stripe.js +244 -193
- package/dist/driver-stripe.js.map +1 -1
- package/dist/index.cjs +310 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -25
- package/dist/index.d.ts +25 -25
- package/dist/index.js +262 -82
- package/dist/index.js.map +1 -1
- package/dist/{manifest-Dl7faaKe.d.cts → manifest-B3Tdab0M.d.cts} +157 -85
- package/dist/{manifest-Dl7faaKe.d.ts → manifest-B3Tdab0M.d.ts} +157 -85
- package/dist/manifest-entry.cjs +118 -49
- package/dist/manifest-entry.cjs.map +1 -1
- package/dist/manifest-entry.d.cts +12 -29
- package/dist/manifest-entry.d.ts +12 -29
- package/dist/manifest-entry.js +79 -14
- package/dist/manifest-entry.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-AXXG3ODP.js.map +0 -1
- package/dist/chunk-G44BUG6G.cjs.map +0 -1
- package/dist/chunk-QMAZGLGV.cjs.map +0 -1
- package/dist/chunk-TNLRQPVY.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commerce/capabilities.ts"],"names":[],"mappings":"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","file":"chunk-RVJLR4RS.js","sourcesContent":["/**\n * The provider capability matrix + checkout error taxonomy — the PURE half of the\n * checkout seam, extracted from checkout.tsx so it ships through the **manifest\n * entry** (no React): the API's publish validation, the editor, AI authoring, and\n * local dev all run the SAME checks the runtime primitives use (phase-3 §3.1b —\n * \"the SDK ships this as data\"). The React half (driver seam, `useCheckout`,\n * `<Checkout>`/`<Upsell>`) stays in ./checkout, which re-exports everything here.\n */\n\nexport type CheckoutSurface =\n | 'express' // wallet quick-buttons (Apple/Google Pay) — inline\n | 'element' // inline card form — inline\n | 'embedded' // provider-managed inline checkout — inline\n | 'sheet' // bottom-sheet popup — opens on click\n | 'popup' // provider-hosted overlay — opens on click\n | 'redirect' // provider-hosted page — opens on click\n\nexport type CheckoutProvider = 'stripe' | 'paddle' | 'whop' | 'primer' | 'solidgate'\nexport type CheckoutIntent = 'purchase' | 'upsell'\n\n/** Surfaces that render inline (vs. those that open on click). */\nexport const INLINE_SURFACES: ReadonlySet<CheckoutSurface> = new Set<CheckoutSurface>([\n 'express',\n 'element',\n 'embedded',\n])\n\nexport function isInlineSurface(surface: CheckoutSurface): boolean {\n return INLINE_SURFACES.has(surface)\n}\n\n/**\n * The kind of off-session upsell (researched against Funnelfox's changelog, which\n * ships these as distinct features). The discriminating real-world constraint is\n * **stacking a 2nd concurrent subscription** — Paddle can't, SolidGate/Stripe can.\n */\nexport type UpsellKind =\n | 'subscription' // charge a NEW, 2nd concurrent subscription off-session (stacking)\n | 'one-time' // charge a one-time add-on off-session\n | 'upgrade' // change the existing subscription's plan\n\n/** Reliability of an off-session upsell charge AFTER a purchase on a surface. */\nexport type OffSessionReliability =\n | 'reliable' // we hold the saved method (Stripe Elements / SolidGate) or the provider charges it (Paddle / Whop API)\n | 'conditional' // possible but may decline — Stripe **managed** checkout (vault charge, SCA not guaranteed) or an orchestrator's PSP-dependent routing (Primer)\n | 'none'\n\nexport interface SurfaceCapability {\n purchase: boolean\n /** Apple/Google Pay available on this surface. */\n wallets: boolean\n /** Reliability of an off-session upsell after a purchase on this surface. */\n offSession: OffSessionReliability\n /**\n * Per-surface DRIVER readiness. Absent = renderable now (the provider's driver mounts it).\n * `'planned'` = the provider genuinely ships this surface (cite it in the profile) but OUR\n * driver doesn't render it yet — publish validation FAILS CLOSED on it so we never promise a\n * surface the runtime can't produce. Mirrors the provider-level {@link ProviderProfile.status}\n * seam at surface granularity: implement the driver, drop the flag, and it flips on cleanly.\n */\n driver?: 'planned'\n}\n\n/**\n * How a provider composes a TRIAL — the scattered provider `if`s made a matrix dimension\n * (§7 item 4). Stripe trials are **composed** (a separate fee/vault step precedes the sub);\n * Paddle trials are **native** (the price itself carries the trial, MoR collects up front):\n * - `paid_composed` — Stripe: trial-fee PaymentIntent → subscription with `trial_period_days`.\n * - `free_vault` — Stripe: SetupIntent vaults the card (no charge) → trial subscription.\n * - `paid_native` — Paddle: the price carries the paid trial; one item, collected up front.\n * - `free_native` — Paddle: native free trial on the price.\n * A provider with an EMPTY `trialShapes` can't run a trial offer (planned providers → fail closed).\n */\nexport type TrialShape = 'paid_composed' | 'free_vault' | 'paid_native' | 'free_native'\n\nexport interface ProviderProfile {\n /** Merchant-of-Record (handles tax/compliance) vs. a PSP you settle through. */\n isMoR: boolean\n /** Upsell kinds this provider can charge off-session (one-click, no re-entry). */\n offSessionUpsells: UpsellKind[]\n /** Who charges the saved method off-session: you (PSP) or the provider (MoR/managed). */\n tokenModel: 'merchant' | 'provider'\n /** Routes across multiple processors (Primer; SolidGate also offers this) vs. a single acquirer. */\n orchestrator: boolean\n /**\n * Driver tier (Wave-4 decision, Simon 2026-07-02): `full` = a real driver ships\n * (Stripe, Paddle); `planned` = profile only — authoring/publish validate against\n * it, but no charge path exists yet (Whop, Primer, SolidGate). A new provider\n * becomes buildable by writing a driver; its profile already gates what authors\n * can express, so funnels written today stay valid when the driver lands.\n */\n status: 'full' | 'planned'\n /**\n * TRIAL shapes this provider can run (§7 item 4). Empty = no trial path (fail closed at\n * publish — a paid/free-trial funnel on this provider can't be produced yet).\n */\n trialShapes: TrialShape[]\n /** Supported surfaces → per-surface capabilities. **Absent = not supported.** */\n surfaces: Partial<Record<CheckoutSurface, SurfaceCapability>>\n}\n\n/** Reliable off-session after this surface (we/the provider hold a chargeable method). */\nconst ON: SurfaceCapability = { purchase: true, wallets: true, offSession: 'reliable' }\n/** Stripe-managed checkout: a purchase works, but the off-session upsell may decline. */\nconst MANAGED: SurfaceCapability = { purchase: true, wallets: true, offSession: 'conditional' }\n/** Orchestrator-routed (Primer): off-session depends on which PSP vaulted the method. */\nconst ROUTED: SurfaceCapability = { purchase: true, wallets: true, offSession: 'conditional' }\n/**\n * Paddle **Express Checkout** (Apple-Pay-first mobile variant of Paddle.js checkout, shipped\n * 2026-04-27 — https://developer.paddle.com/changelog/2026/apple-pay-express-checkout/). Paddle\n * genuinely offers it (MoR, so it's their hosted/inline frame surfaced wallet-first, NOT a raw\n * wallet element in our UI), which is why it's a distinct `express` surface here rather than only\n * the `wallets` flag already true on Paddle's embedded/popup. But our Paddle DRIVER doesn't render\n * it yet (drivers/paddle.tsx opens the standard embedded/overlay checkout regardless of surface),\n * so `driver: 'planned'` fails it closed at publish — the matrix records reality without promising\n * a wallet-first render the runtime can't produce. Off-session stays reliable (provider-token, MoR).\n */\nconst PADDLE_EXPRESS: SurfaceCapability = { purchase: true, wallets: true, offSession: 'reliable', driver: 'planned' }\nconst ALL_UPSELLS: UpsellKind[] = ['subscription', 'one-time', 'upgrade']\n\n/**\n * Provider capabilities (researched 2026-06-23 against provider docs + Funnelfox;\n * trial mechanics re-verified 2026-07-02, doc 09 §4.4).\n *\n * Three real-world corrections to the naive model:\n * 1. **Off-session upsell support is per-provider by *kind*** (`offSessionUpsells`):\n * Paddle can't stack a 2nd concurrent subscription (upgrade + one-time only);\n * Stripe/SolidGate/Whop/Primer can do all kinds.\n * 2. **Off-session *reliability* varies by the paywall surface** (`SurfaceCapability.\n * offSession`): Stripe **managed** checkout (`embedded`/`redirect`) upsells as a\n * direct charge against Stripe's vault → `conditional` (may decline); Elements\n * surfaces are `reliable`. Primer is `conditional` everywhere (PSP-dependent).\n * 3. **Surface availability is a hard constraint.** MoR/managed providers can't\n * expose a raw card field to your own UI: Paddle has no `express`/`element`/your\n * `sheet`; Whop has no raw `element`/`express`; Primer has no hosted `redirect`;\n * Stripe/SolidGate have no `popup`. A missing entry means \"not possible.\"\n */\nexport const PROVIDER_PROFILES: Record<CheckoutProvider, ProviderProfile> = {\n // PSP; you hold the PaymentMethod and fire your own off-session PaymentIntent on\n // the Elements surfaces (reliable). **Managed** Checkout (`embedded`/`redirect`)\n // upsells too, but as a direct charge against Stripe's vaulted card → conditional.\n // No provider \"popup\" surface.\n stripe: {\n isMoR: false, offSessionUpsells: ALL_UPSELLS, tokenModel: 'merchant', orchestrator: false, status: 'full',\n // Composed trials: paid = fee PaymentIntent + sub (trial_period_days); free = SetupIntent vault\n // + trial sub (services/checkout/stripe-trial.ts).\n trialShapes: ['paid_composed', 'free_vault'],\n surfaces: { express: ON, element: ON, sheet: ON, embedded: MANAGED, redirect: MANAGED },\n },\n // Merchant-of-Record; raw card entry is always Paddle's frame → no own element/sheet.\n // `express` IS now present (Apple-Pay-first Express Checkout, shipped 2026-04-27) but is\n // driver-planned (PADDLE_EXPRESS) → publish fails it closed until the driver renders it.\n // Off-session via subscription one-time charge / collection_mode, but **no 2nd concurrent\n // subscription** (upgrade + one-time only), provider-initiated.\n paddle: {\n isMoR: true, offSessionUpsells: ['one-time', 'upgrade'], tokenModel: 'provider', orchestrator: false, status: 'full',\n // Native trials: the price itself carries the trial, Paddle collects up front / vaults\n // (services/checkout/paddle-checkout.ts — \"Paddle paid trials are NATIVE\").\n trialShapes: ['paid_native', 'free_native'],\n surfaces: { embedded: ON, popup: ON, redirect: ON, express: PADDLE_EXPRESS },\n },\n // Merchant-of-Record; iframe-only (no raw card field). Off-session works\n // (provider-initiated, async via webhooks). `sheet` = our chrome hosting their embed.\n // Trial shape not yet researched → empty (fail closed until the driver + profile land).\n whop: {\n isMoR: true, offSessionUpsells: ALL_UPSELLS, tokenModel: 'provider', orchestrator: false, status: 'planned',\n trialShapes: [],\n surfaces: { embedded: ON, redirect: ON, sheet: ON },\n },\n // Pure orchestrator (PSP-side); Headless renders express/element/sheet/embedded.\n // No hosted redirect page, no popup checkout. Off-session is PSP-conditional —\n // the vault lives with whichever processor Primer routed to (data matches this\n // comment as of 2026-07-02; it previously said conditional but coded reliable).\n primer: {\n isMoR: false, offSessionUpsells: ALL_UPSELLS, tokenModel: 'merchant', orchestrator: true, status: 'planned',\n trialShapes: [],\n surfaces: { express: ROUTED, element: ROUTED, sheet: ROUTED, embedded: ROUTED },\n },\n // PSP **and** orchestrator (own acquiring + connectors routing across Stripe/Adyen/\n // PayPal). One Payment Form (element/sheet/embedded) + hosted page + wallet button.\n // Strong native off-session (Recurring API). No popup.\n solidgate: {\n isMoR: false, offSessionUpsells: ALL_UPSELLS, tokenModel: 'merchant', orchestrator: true, status: 'planned',\n trialShapes: [],\n surfaces: { express: ON, element: ON, sheet: ON, embedded: ON, redirect: ON },\n },\n}\n\nexport interface ValidationResult {\n ok: boolean\n reason?: string\n /** Non-fatal caveat (e.g. Paddle upgrade-only). */\n note?: string\n}\n\n/** Surfaces a provider supports, for the editor/AI to steer authors. */\nexport function surfacesFor(provider: CheckoutProvider): CheckoutSurface[] {\n return Object.keys(PROVIDER_PROFILES[provider]?.surfaces ?? {}) as CheckoutSurface[]\n}\n\n/** Whether a provider is a Merchant of Record (it owns tax/compliance). */\nexport function isMerchantOfRecord(provider: CheckoutProvider): boolean {\n return PROVIDER_PROFILES[provider]?.isMoR ?? false\n}\n\n/** Whether a provider routes across multiple processors (Primer, SolidGate). */\nexport function isOrchestrator(provider: CheckoutProvider): boolean {\n return PROVIDER_PROFILES[provider]?.orchestrator ?? false\n}\n\n/**\n * Validate a (provider, surface) for a **purchase**: the surface must exist for\n * the provider and accept a payment. Surface availability is the real constraint\n * (e.g. Paddle has no `element`).\n */\nexport function validateCheckout(provider: CheckoutProvider, surface: CheckoutSurface): ValidationResult {\n const cap = PROVIDER_PROFILES[provider]?.surfaces[surface]\n if (!cap) return { ok: false, reason: `${provider} has no '${surface}' surface` }\n if (!cap.purchase) return { ok: false, reason: `'${surface}' can't take a purchase on ${provider}` }\n // The provider offers this surface but our driver can't render it yet (e.g. Paddle Express\n // Checkout) → fail closed so publish never promises a surface the runtime can't produce.\n if (cap.driver === 'planned') return { ok: false, reason: `${provider} '${surface}' checkout isn't renderable yet (the provider ships it, but our driver doesn't render it)` }\n return { ok: true }\n}\n\n/** Trial shapes a provider can compose (empty = it can't run a trial offer yet). */\nexport function trialShapesFor(provider: CheckoutProvider): TrialShape[] {\n return PROVIDER_PROFILES[provider]?.trialShapes ?? []\n}\n\n/**\n * Validate that a provider can run a TRIAL (§7 item 4). Pass a specific {@link TrialShape} to\n * check that exact shape; omit it to assert the provider supports *any* trial (the cheap gate\n * check when only the offering's TRIAL-role binding existence is known). A provider with no\n * trial shapes (every planned provider) fails closed.\n */\nexport function validateTrial(provider: CheckoutProvider, shape?: TrialShape): ValidationResult {\n const profile = PROVIDER_PROFILES[provider]\n if (!profile) return { ok: false, reason: `unknown provider '${provider}'` }\n const shapes = profile.trialShapes\n if (shapes.length === 0) return { ok: false, reason: `${provider} can't run a trial offer yet` }\n if (shape && !shapes.includes(shape)) {\n return { ok: false, reason: `${provider} doesn't support a '${shape}' trial (it supports: ${shapes.join(', ')})` }\n }\n return { ok: true }\n}\n\n/**\n * Validate an **off-session upsell** of `kind` after a purchase made on\n * `paywallSurface`. Combines provider support for the kind (Paddle can't stack a\n * 2nd subscription) with the paywall surface's off-session reliability (Stripe\n * managed checkout is `conditional` — returned as a `note`, not a hard block).\n */\nexport function validateUpsell(\n provider: CheckoutProvider,\n paywallSurface: CheckoutSurface,\n kind: UpsellKind = 'subscription',\n): ValidationResult {\n const profile = PROVIDER_PROFILES[provider]\n if (!profile) return { ok: false, reason: `unknown provider '${provider}'` }\n if (!profile.offSessionUpsells.includes(kind)) {\n const can = profile.offSessionUpsells.length ? ` (it supports: ${profile.offSessionUpsells.join(', ')})` : ''\n return { ok: false, reason: `${provider} can't charge a '${kind}' upsell off-session${can}` }\n }\n const cap = profile.surfaces[paywallSurface]\n if (!cap) return { ok: false, reason: `${provider} has no '${paywallSurface}' surface to upsell after` }\n if (cap.offSession === 'none') {\n return { ok: false, reason: `a purchase on '${paywallSurface}' retains no chargeable method for an upsell` }\n }\n if (cap.offSession === 'conditional') {\n return { ok: true, note: `off-session upsell after ${provider} '${paywallSurface}' may decline — prefer a reliable surface or wire an on-session fallback` }\n }\n return { ok: true }\n}\n\n// ── Checkout error taxonomy (pure — drivers + server both normalize into it) ──\n\n/**\n * Normalized failure category — the driver/platform maps each provider's decline\n * codes into one of these so funnels can route/recover by reason. `authentication_\n * required` (3DS/SCA) and `requires_payment_method` are the cases an **off-session**\n * upsell can't satisfy → recover on-session (re-collect a card).\n */\nexport type CheckoutErrorCategory =\n | 'card_declined'\n | 'insufficient_funds'\n | 'expired_card'\n | 'prepaid_card' // card funding is prepaid — trials/subscriptions commonly decline; route to a real-card recapture page\n | 'authentication_required' // SCA/3DS needed — off-session can't challenge\n | 'requires_payment_method' // no usable saved method\n | 'processing_error'\n | 'canceled'\n | 'unknown'\n\nexport interface CheckoutError {\n category: CheckoutErrorCategory\n message: string\n /** Raw provider decline code, if any (e.g. Stripe `card_declined`/`insufficient_funds`). */\n declineCode?: string\n /** Whether re-attempting (e.g. an on-session re-collection) may succeed. */\n retryable: boolean\n}\n\n/** Build a {@link CheckoutError} (driver/test helper). */\nexport function checkoutError(\n category: CheckoutErrorCategory,\n message: string,\n extra: { declineCode?: string; retryable?: boolean } = {},\n): CheckoutError {\n const retryable = extra.retryable ?? (category === 'authentication_required' || category === 'requires_payment_method' || category === 'processing_error')\n return { category, message, declineCode: extra.declineCode, retryable }\n}\n"]}
|
|
@@ -12,6 +12,7 @@ function isInlineSurface(surface) {
|
|
|
12
12
|
var ON = { purchase: true, wallets: true, offSession: "reliable" };
|
|
13
13
|
var MANAGED = { purchase: true, wallets: true, offSession: "conditional" };
|
|
14
14
|
var ROUTED = { purchase: true, wallets: true, offSession: "conditional" };
|
|
15
|
+
var PADDLE_EXPRESS = { purchase: true, wallets: true, offSession: "reliable", driver: "planned" };
|
|
15
16
|
var ALL_UPSELLS = ["subscription", "one-time", "upgrade"];
|
|
16
17
|
var PROVIDER_PROFILES = {
|
|
17
18
|
// PSP; you hold the PaymentMethod and fire your own off-session PaymentIntent on
|
|
@@ -24,27 +25,37 @@ var PROVIDER_PROFILES = {
|
|
|
24
25
|
tokenModel: "merchant",
|
|
25
26
|
orchestrator: false,
|
|
26
27
|
status: "full",
|
|
28
|
+
// Composed trials: paid = fee PaymentIntent + sub (trial_period_days); free = SetupIntent vault
|
|
29
|
+
// + trial sub (services/checkout/stripe-trial.ts).
|
|
30
|
+
trialShapes: ["paid_composed", "free_vault"],
|
|
27
31
|
surfaces: { express: ON, element: ON, sheet: ON, embedded: MANAGED, redirect: MANAGED }
|
|
28
32
|
},
|
|
29
|
-
// Merchant-of-Record; card entry is always Paddle's frame → no own element/sheet
|
|
30
|
-
// express
|
|
31
|
-
//
|
|
33
|
+
// Merchant-of-Record; raw card entry is always Paddle's frame → no own element/sheet.
|
|
34
|
+
// `express` IS now present (Apple-Pay-first Express Checkout, shipped 2026-04-27) but is
|
|
35
|
+
// driver-planned (PADDLE_EXPRESS) → publish fails it closed until the driver renders it.
|
|
36
|
+
// Off-session via subscription one-time charge / collection_mode, but **no 2nd concurrent
|
|
37
|
+
// subscription** (upgrade + one-time only), provider-initiated.
|
|
32
38
|
paddle: {
|
|
33
39
|
isMoR: true,
|
|
34
40
|
offSessionUpsells: ["one-time", "upgrade"],
|
|
35
41
|
tokenModel: "provider",
|
|
36
42
|
orchestrator: false,
|
|
37
43
|
status: "full",
|
|
38
|
-
|
|
44
|
+
// Native trials: the price itself carries the trial, Paddle collects up front / vaults
|
|
45
|
+
// (services/checkout/paddle-checkout.ts — "Paddle paid trials are NATIVE").
|
|
46
|
+
trialShapes: ["paid_native", "free_native"],
|
|
47
|
+
surfaces: { embedded: ON, popup: ON, redirect: ON, express: PADDLE_EXPRESS }
|
|
39
48
|
},
|
|
40
49
|
// Merchant-of-Record; iframe-only (no raw card field). Off-session works
|
|
41
50
|
// (provider-initiated, async via webhooks). `sheet` = our chrome hosting their embed.
|
|
51
|
+
// Trial shape not yet researched → empty (fail closed until the driver + profile land).
|
|
42
52
|
whop: {
|
|
43
53
|
isMoR: true,
|
|
44
54
|
offSessionUpsells: ALL_UPSELLS,
|
|
45
55
|
tokenModel: "provider",
|
|
46
56
|
orchestrator: false,
|
|
47
57
|
status: "planned",
|
|
58
|
+
trialShapes: [],
|
|
48
59
|
surfaces: { embedded: ON, redirect: ON, sheet: ON }
|
|
49
60
|
},
|
|
50
61
|
// Pure orchestrator (PSP-side); Headless renders express/element/sheet/embedded.
|
|
@@ -57,6 +68,7 @@ var PROVIDER_PROFILES = {
|
|
|
57
68
|
tokenModel: "merchant",
|
|
58
69
|
orchestrator: true,
|
|
59
70
|
status: "planned",
|
|
71
|
+
trialShapes: [],
|
|
60
72
|
surfaces: { express: ROUTED, element: ROUTED, sheet: ROUTED, embedded: ROUTED }
|
|
61
73
|
},
|
|
62
74
|
// PSP **and** orchestrator (own acquiring + connectors routing across Stripe/Adyen/
|
|
@@ -68,6 +80,7 @@ var PROVIDER_PROFILES = {
|
|
|
68
80
|
tokenModel: "merchant",
|
|
69
81
|
orchestrator: true,
|
|
70
82
|
status: "planned",
|
|
83
|
+
trialShapes: [],
|
|
71
84
|
surfaces: { express: ON, element: ON, sheet: ON, embedded: ON, redirect: ON }
|
|
72
85
|
}
|
|
73
86
|
};
|
|
@@ -83,7 +96,22 @@ function isOrchestrator(provider) {
|
|
|
83
96
|
function validateCheckout(provider, surface) {
|
|
84
97
|
const cap = PROVIDER_PROFILES[provider]?.surfaces[surface];
|
|
85
98
|
if (!cap) return { ok: false, reason: `${provider} has no '${surface}' surface` };
|
|
86
|
-
|
|
99
|
+
if (!cap.purchase) return { ok: false, reason: `'${surface}' can't take a purchase on ${provider}` };
|
|
100
|
+
if (cap.driver === "planned") return { ok: false, reason: `${provider} '${surface}' checkout isn't renderable yet (the provider ships it, but our driver doesn't render it)` };
|
|
101
|
+
return { ok: true };
|
|
102
|
+
}
|
|
103
|
+
function trialShapesFor(provider) {
|
|
104
|
+
return PROVIDER_PROFILES[provider]?.trialShapes ?? [];
|
|
105
|
+
}
|
|
106
|
+
function validateTrial(provider, shape) {
|
|
107
|
+
const profile = PROVIDER_PROFILES[provider];
|
|
108
|
+
if (!profile) return { ok: false, reason: `unknown provider '${provider}'` };
|
|
109
|
+
const shapes = profile.trialShapes;
|
|
110
|
+
if (shapes.length === 0) return { ok: false, reason: `${provider} can't run a trial offer yet` };
|
|
111
|
+
if (shape && !shapes.includes(shape)) {
|
|
112
|
+
return { ok: false, reason: `${provider} doesn't support a '${shape}' trial (it supports: ${shapes.join(", ")})` };
|
|
113
|
+
}
|
|
114
|
+
return { ok: true };
|
|
87
115
|
}
|
|
88
116
|
function validateUpsell(provider, paywallSurface, kind = "subscription") {
|
|
89
117
|
const profile = PROVIDER_PROFILES[provider];
|
|
@@ -114,7 +142,9 @@ exports.isInlineSurface = isInlineSurface;
|
|
|
114
142
|
exports.isMerchantOfRecord = isMerchantOfRecord;
|
|
115
143
|
exports.isOrchestrator = isOrchestrator;
|
|
116
144
|
exports.surfacesFor = surfacesFor;
|
|
145
|
+
exports.trialShapesFor = trialShapesFor;
|
|
117
146
|
exports.validateCheckout = validateCheckout;
|
|
147
|
+
exports.validateTrial = validateTrial;
|
|
118
148
|
exports.validateUpsell = validateUpsell;
|
|
119
|
-
//# sourceMappingURL=chunk-
|
|
120
|
-
//# sourceMappingURL=chunk-
|
|
149
|
+
//# sourceMappingURL=chunk-RY3LOSVZ.cjs.map
|
|
150
|
+
//# sourceMappingURL=chunk-RY3LOSVZ.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commerce/capabilities.ts"],"names":[],"mappings":"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","file":"chunk-RY3LOSVZ.cjs","sourcesContent":["/**\n * The provider capability matrix + checkout error taxonomy — the PURE half of the\n * checkout seam, extracted from checkout.tsx so it ships through the **manifest\n * entry** (no React): the API's publish validation, the editor, AI authoring, and\n * local dev all run the SAME checks the runtime primitives use (phase-3 §3.1b —\n * \"the SDK ships this as data\"). The React half (driver seam, `useCheckout`,\n * `<Checkout>`/`<Upsell>`) stays in ./checkout, which re-exports everything here.\n */\n\nexport type CheckoutSurface =\n | 'express' // wallet quick-buttons (Apple/Google Pay) — inline\n | 'element' // inline card form — inline\n | 'embedded' // provider-managed inline checkout — inline\n | 'sheet' // bottom-sheet popup — opens on click\n | 'popup' // provider-hosted overlay — opens on click\n | 'redirect' // provider-hosted page — opens on click\n\nexport type CheckoutProvider = 'stripe' | 'paddle' | 'whop' | 'primer' | 'solidgate'\nexport type CheckoutIntent = 'purchase' | 'upsell'\n\n/** Surfaces that render inline (vs. those that open on click). */\nexport const INLINE_SURFACES: ReadonlySet<CheckoutSurface> = new Set<CheckoutSurface>([\n 'express',\n 'element',\n 'embedded',\n])\n\nexport function isInlineSurface(surface: CheckoutSurface): boolean {\n return INLINE_SURFACES.has(surface)\n}\n\n/**\n * The kind of off-session upsell (researched against Funnelfox's changelog, which\n * ships these as distinct features). The discriminating real-world constraint is\n * **stacking a 2nd concurrent subscription** — Paddle can't, SolidGate/Stripe can.\n */\nexport type UpsellKind =\n | 'subscription' // charge a NEW, 2nd concurrent subscription off-session (stacking)\n | 'one-time' // charge a one-time add-on off-session\n | 'upgrade' // change the existing subscription's plan\n\n/** Reliability of an off-session upsell charge AFTER a purchase on a surface. */\nexport type OffSessionReliability =\n | 'reliable' // we hold the saved method (Stripe Elements / SolidGate) or the provider charges it (Paddle / Whop API)\n | 'conditional' // possible but may decline — Stripe **managed** checkout (vault charge, SCA not guaranteed) or an orchestrator's PSP-dependent routing (Primer)\n | 'none'\n\nexport interface SurfaceCapability {\n purchase: boolean\n /** Apple/Google Pay available on this surface. */\n wallets: boolean\n /** Reliability of an off-session upsell after a purchase on this surface. */\n offSession: OffSessionReliability\n /**\n * Per-surface DRIVER readiness. Absent = renderable now (the provider's driver mounts it).\n * `'planned'` = the provider genuinely ships this surface (cite it in the profile) but OUR\n * driver doesn't render it yet — publish validation FAILS CLOSED on it so we never promise a\n * surface the runtime can't produce. Mirrors the provider-level {@link ProviderProfile.status}\n * seam at surface granularity: implement the driver, drop the flag, and it flips on cleanly.\n */\n driver?: 'planned'\n}\n\n/**\n * How a provider composes a TRIAL — the scattered provider `if`s made a matrix dimension\n * (§7 item 4). Stripe trials are **composed** (a separate fee/vault step precedes the sub);\n * Paddle trials are **native** (the price itself carries the trial, MoR collects up front):\n * - `paid_composed` — Stripe: trial-fee PaymentIntent → subscription with `trial_period_days`.\n * - `free_vault` — Stripe: SetupIntent vaults the card (no charge) → trial subscription.\n * - `paid_native` — Paddle: the price carries the paid trial; one item, collected up front.\n * - `free_native` — Paddle: native free trial on the price.\n * A provider with an EMPTY `trialShapes` can't run a trial offer (planned providers → fail closed).\n */\nexport type TrialShape = 'paid_composed' | 'free_vault' | 'paid_native' | 'free_native'\n\nexport interface ProviderProfile {\n /** Merchant-of-Record (handles tax/compliance) vs. a PSP you settle through. */\n isMoR: boolean\n /** Upsell kinds this provider can charge off-session (one-click, no re-entry). */\n offSessionUpsells: UpsellKind[]\n /** Who charges the saved method off-session: you (PSP) or the provider (MoR/managed). */\n tokenModel: 'merchant' | 'provider'\n /** Routes across multiple processors (Primer; SolidGate also offers this) vs. a single acquirer. */\n orchestrator: boolean\n /**\n * Driver tier (Wave-4 decision, Simon 2026-07-02): `full` = a real driver ships\n * (Stripe, Paddle); `planned` = profile only — authoring/publish validate against\n * it, but no charge path exists yet (Whop, Primer, SolidGate). A new provider\n * becomes buildable by writing a driver; its profile already gates what authors\n * can express, so funnels written today stay valid when the driver lands.\n */\n status: 'full' | 'planned'\n /**\n * TRIAL shapes this provider can run (§7 item 4). Empty = no trial path (fail closed at\n * publish — a paid/free-trial funnel on this provider can't be produced yet).\n */\n trialShapes: TrialShape[]\n /** Supported surfaces → per-surface capabilities. **Absent = not supported.** */\n surfaces: Partial<Record<CheckoutSurface, SurfaceCapability>>\n}\n\n/** Reliable off-session after this surface (we/the provider hold a chargeable method). */\nconst ON: SurfaceCapability = { purchase: true, wallets: true, offSession: 'reliable' }\n/** Stripe-managed checkout: a purchase works, but the off-session upsell may decline. */\nconst MANAGED: SurfaceCapability = { purchase: true, wallets: true, offSession: 'conditional' }\n/** Orchestrator-routed (Primer): off-session depends on which PSP vaulted the method. */\nconst ROUTED: SurfaceCapability = { purchase: true, wallets: true, offSession: 'conditional' }\n/**\n * Paddle **Express Checkout** (Apple-Pay-first mobile variant of Paddle.js checkout, shipped\n * 2026-04-27 — https://developer.paddle.com/changelog/2026/apple-pay-express-checkout/). Paddle\n * genuinely offers it (MoR, so it's their hosted/inline frame surfaced wallet-first, NOT a raw\n * wallet element in our UI), which is why it's a distinct `express` surface here rather than only\n * the `wallets` flag already true on Paddle's embedded/popup. But our Paddle DRIVER doesn't render\n * it yet (drivers/paddle.tsx opens the standard embedded/overlay checkout regardless of surface),\n * so `driver: 'planned'` fails it closed at publish — the matrix records reality without promising\n * a wallet-first render the runtime can't produce. Off-session stays reliable (provider-token, MoR).\n */\nconst PADDLE_EXPRESS: SurfaceCapability = { purchase: true, wallets: true, offSession: 'reliable', driver: 'planned' }\nconst ALL_UPSELLS: UpsellKind[] = ['subscription', 'one-time', 'upgrade']\n\n/**\n * Provider capabilities (researched 2026-06-23 against provider docs + Funnelfox;\n * trial mechanics re-verified 2026-07-02, doc 09 §4.4).\n *\n * Three real-world corrections to the naive model:\n * 1. **Off-session upsell support is per-provider by *kind*** (`offSessionUpsells`):\n * Paddle can't stack a 2nd concurrent subscription (upgrade + one-time only);\n * Stripe/SolidGate/Whop/Primer can do all kinds.\n * 2. **Off-session *reliability* varies by the paywall surface** (`SurfaceCapability.\n * offSession`): Stripe **managed** checkout (`embedded`/`redirect`) upsells as a\n * direct charge against Stripe's vault → `conditional` (may decline); Elements\n * surfaces are `reliable`. Primer is `conditional` everywhere (PSP-dependent).\n * 3. **Surface availability is a hard constraint.** MoR/managed providers can't\n * expose a raw card field to your own UI: Paddle has no `express`/`element`/your\n * `sheet`; Whop has no raw `element`/`express`; Primer has no hosted `redirect`;\n * Stripe/SolidGate have no `popup`. A missing entry means \"not possible.\"\n */\nexport const PROVIDER_PROFILES: Record<CheckoutProvider, ProviderProfile> = {\n // PSP; you hold the PaymentMethod and fire your own off-session PaymentIntent on\n // the Elements surfaces (reliable). **Managed** Checkout (`embedded`/`redirect`)\n // upsells too, but as a direct charge against Stripe's vaulted card → conditional.\n // No provider \"popup\" surface.\n stripe: {\n isMoR: false, offSessionUpsells: ALL_UPSELLS, tokenModel: 'merchant', orchestrator: false, status: 'full',\n // Composed trials: paid = fee PaymentIntent + sub (trial_period_days); free = SetupIntent vault\n // + trial sub (services/checkout/stripe-trial.ts).\n trialShapes: ['paid_composed', 'free_vault'],\n surfaces: { express: ON, element: ON, sheet: ON, embedded: MANAGED, redirect: MANAGED },\n },\n // Merchant-of-Record; raw card entry is always Paddle's frame → no own element/sheet.\n // `express` IS now present (Apple-Pay-first Express Checkout, shipped 2026-04-27) but is\n // driver-planned (PADDLE_EXPRESS) → publish fails it closed until the driver renders it.\n // Off-session via subscription one-time charge / collection_mode, but **no 2nd concurrent\n // subscription** (upgrade + one-time only), provider-initiated.\n paddle: {\n isMoR: true, offSessionUpsells: ['one-time', 'upgrade'], tokenModel: 'provider', orchestrator: false, status: 'full',\n // Native trials: the price itself carries the trial, Paddle collects up front / vaults\n // (services/checkout/paddle-checkout.ts — \"Paddle paid trials are NATIVE\").\n trialShapes: ['paid_native', 'free_native'],\n surfaces: { embedded: ON, popup: ON, redirect: ON, express: PADDLE_EXPRESS },\n },\n // Merchant-of-Record; iframe-only (no raw card field). Off-session works\n // (provider-initiated, async via webhooks). `sheet` = our chrome hosting their embed.\n // Trial shape not yet researched → empty (fail closed until the driver + profile land).\n whop: {\n isMoR: true, offSessionUpsells: ALL_UPSELLS, tokenModel: 'provider', orchestrator: false, status: 'planned',\n trialShapes: [],\n surfaces: { embedded: ON, redirect: ON, sheet: ON },\n },\n // Pure orchestrator (PSP-side); Headless renders express/element/sheet/embedded.\n // No hosted redirect page, no popup checkout. Off-session is PSP-conditional —\n // the vault lives with whichever processor Primer routed to (data matches this\n // comment as of 2026-07-02; it previously said conditional but coded reliable).\n primer: {\n isMoR: false, offSessionUpsells: ALL_UPSELLS, tokenModel: 'merchant', orchestrator: true, status: 'planned',\n trialShapes: [],\n surfaces: { express: ROUTED, element: ROUTED, sheet: ROUTED, embedded: ROUTED },\n },\n // PSP **and** orchestrator (own acquiring + connectors routing across Stripe/Adyen/\n // PayPal). One Payment Form (element/sheet/embedded) + hosted page + wallet button.\n // Strong native off-session (Recurring API). No popup.\n solidgate: {\n isMoR: false, offSessionUpsells: ALL_UPSELLS, tokenModel: 'merchant', orchestrator: true, status: 'planned',\n trialShapes: [],\n surfaces: { express: ON, element: ON, sheet: ON, embedded: ON, redirect: ON },\n },\n}\n\nexport interface ValidationResult {\n ok: boolean\n reason?: string\n /** Non-fatal caveat (e.g. Paddle upgrade-only). */\n note?: string\n}\n\n/** Surfaces a provider supports, for the editor/AI to steer authors. */\nexport function surfacesFor(provider: CheckoutProvider): CheckoutSurface[] {\n return Object.keys(PROVIDER_PROFILES[provider]?.surfaces ?? {}) as CheckoutSurface[]\n}\n\n/** Whether a provider is a Merchant of Record (it owns tax/compliance). */\nexport function isMerchantOfRecord(provider: CheckoutProvider): boolean {\n return PROVIDER_PROFILES[provider]?.isMoR ?? false\n}\n\n/** Whether a provider routes across multiple processors (Primer, SolidGate). */\nexport function isOrchestrator(provider: CheckoutProvider): boolean {\n return PROVIDER_PROFILES[provider]?.orchestrator ?? false\n}\n\n/**\n * Validate a (provider, surface) for a **purchase**: the surface must exist for\n * the provider and accept a payment. Surface availability is the real constraint\n * (e.g. Paddle has no `element`).\n */\nexport function validateCheckout(provider: CheckoutProvider, surface: CheckoutSurface): ValidationResult {\n const cap = PROVIDER_PROFILES[provider]?.surfaces[surface]\n if (!cap) return { ok: false, reason: `${provider} has no '${surface}' surface` }\n if (!cap.purchase) return { ok: false, reason: `'${surface}' can't take a purchase on ${provider}` }\n // The provider offers this surface but our driver can't render it yet (e.g. Paddle Express\n // Checkout) → fail closed so publish never promises a surface the runtime can't produce.\n if (cap.driver === 'planned') return { ok: false, reason: `${provider} '${surface}' checkout isn't renderable yet (the provider ships it, but our driver doesn't render it)` }\n return { ok: true }\n}\n\n/** Trial shapes a provider can compose (empty = it can't run a trial offer yet). */\nexport function trialShapesFor(provider: CheckoutProvider): TrialShape[] {\n return PROVIDER_PROFILES[provider]?.trialShapes ?? []\n}\n\n/**\n * Validate that a provider can run a TRIAL (§7 item 4). Pass a specific {@link TrialShape} to\n * check that exact shape; omit it to assert the provider supports *any* trial (the cheap gate\n * check when only the offering's TRIAL-role binding existence is known). A provider with no\n * trial shapes (every planned provider) fails closed.\n */\nexport function validateTrial(provider: CheckoutProvider, shape?: TrialShape): ValidationResult {\n const profile = PROVIDER_PROFILES[provider]\n if (!profile) return { ok: false, reason: `unknown provider '${provider}'` }\n const shapes = profile.trialShapes\n if (shapes.length === 0) return { ok: false, reason: `${provider} can't run a trial offer yet` }\n if (shape && !shapes.includes(shape)) {\n return { ok: false, reason: `${provider} doesn't support a '${shape}' trial (it supports: ${shapes.join(', ')})` }\n }\n return { ok: true }\n}\n\n/**\n * Validate an **off-session upsell** of `kind` after a purchase made on\n * `paywallSurface`. Combines provider support for the kind (Paddle can't stack a\n * 2nd subscription) with the paywall surface's off-session reliability (Stripe\n * managed checkout is `conditional` — returned as a `note`, not a hard block).\n */\nexport function validateUpsell(\n provider: CheckoutProvider,\n paywallSurface: CheckoutSurface,\n kind: UpsellKind = 'subscription',\n): ValidationResult {\n const profile = PROVIDER_PROFILES[provider]\n if (!profile) return { ok: false, reason: `unknown provider '${provider}'` }\n if (!profile.offSessionUpsells.includes(kind)) {\n const can = profile.offSessionUpsells.length ? ` (it supports: ${profile.offSessionUpsells.join(', ')})` : ''\n return { ok: false, reason: `${provider} can't charge a '${kind}' upsell off-session${can}` }\n }\n const cap = profile.surfaces[paywallSurface]\n if (!cap) return { ok: false, reason: `${provider} has no '${paywallSurface}' surface to upsell after` }\n if (cap.offSession === 'none') {\n return { ok: false, reason: `a purchase on '${paywallSurface}' retains no chargeable method for an upsell` }\n }\n if (cap.offSession === 'conditional') {\n return { ok: true, note: `off-session upsell after ${provider} '${paywallSurface}' may decline — prefer a reliable surface or wire an on-session fallback` }\n }\n return { ok: true }\n}\n\n// ── Checkout error taxonomy (pure — drivers + server both normalize into it) ──\n\n/**\n * Normalized failure category — the driver/platform maps each provider's decline\n * codes into one of these so funnels can route/recover by reason. `authentication_\n * required` (3DS/SCA) and `requires_payment_method` are the cases an **off-session**\n * upsell can't satisfy → recover on-session (re-collect a card).\n */\nexport type CheckoutErrorCategory =\n | 'card_declined'\n | 'insufficient_funds'\n | 'expired_card'\n | 'prepaid_card' // card funding is prepaid — trials/subscriptions commonly decline; route to a real-card recapture page\n | 'authentication_required' // SCA/3DS needed — off-session can't challenge\n | 'requires_payment_method' // no usable saved method\n | 'processing_error'\n | 'canceled'\n | 'unknown'\n\nexport interface CheckoutError {\n category: CheckoutErrorCategory\n message: string\n /** Raw provider decline code, if any (e.g. Stripe `card_declined`/`insufficient_funds`). */\n declineCode?: string\n /** Whether re-attempting (e.g. an on-session re-collection) may succeed. */\n retryable: boolean\n}\n\n/** Build a {@link CheckoutError} (driver/test helper). */\nexport function checkoutError(\n category: CheckoutErrorCategory,\n message: string,\n extra: { declineCode?: string; retryable?: boolean } = {},\n): CheckoutError {\n const retryable = extra.retryable ?? (category === 'authentication_required' || category === 'requires_payment_method' || category === 'processing_error')\n return { category, message, declineCode: extra.declineCode, retryable }\n}\n"]}
|
|
@@ -37,7 +37,7 @@ function intervalLabel(interval, count) {
|
|
|
37
37
|
}
|
|
38
38
|
return { period: `${count} ${interval}s`, periodly: `every ${count} ${interval}s` };
|
|
39
39
|
}
|
|
40
|
-
function
|
|
40
|
+
function resolveOffering(input) {
|
|
41
41
|
const interval = input.interval ?? "one_time";
|
|
42
42
|
const count = input.intervalCount ?? 1;
|
|
43
43
|
const days = periodDays(interval, count);
|
|
@@ -65,7 +65,7 @@ function resolveProduct(input) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
function buildCatalog(inputs) {
|
|
68
|
-
return new Map(inputs.map((i) => [i.id,
|
|
68
|
+
return new Map(inputs.map((i) => [i.id, resolveOffering(i)]));
|
|
69
69
|
}
|
|
70
70
|
var EXPONENT_CACHE = /* @__PURE__ */ new Map();
|
|
71
71
|
function currencyExponent(currency) {
|
|
@@ -93,7 +93,7 @@ function formatMoney(minor, currency, locale) {
|
|
|
93
93
|
}
|
|
94
94
|
return { amount, minor, currency: code, formatted };
|
|
95
95
|
}
|
|
96
|
-
function
|
|
96
|
+
function formatOffering(r, locale) {
|
|
97
97
|
const m = (minor) => formatMoney(minor, r.currency, locale);
|
|
98
98
|
return {
|
|
99
99
|
id: r.id,
|
|
@@ -143,7 +143,8 @@ function evaluateCondition(cond, s) {
|
|
|
143
143
|
return typeof val === "number" && typeof value === "number" && val <= value;
|
|
144
144
|
case "contains":
|
|
145
145
|
if (typeof val === "string") return val.includes(String(value));
|
|
146
|
-
if (Array.isArray(val))
|
|
146
|
+
if (Array.isArray(val))
|
|
147
|
+
return val.includes(value);
|
|
147
148
|
return false;
|
|
148
149
|
case "exists":
|
|
149
150
|
return val !== void 0 && val !== null && val !== "";
|
|
@@ -207,13 +208,21 @@ function expectedPathLength(pages, startKey, s) {
|
|
|
207
208
|
function defineFunnel(def) {
|
|
208
209
|
return def;
|
|
209
210
|
}
|
|
210
|
-
function
|
|
211
|
-
if (!
|
|
212
|
-
if (Array.isArray(
|
|
213
|
-
|
|
211
|
+
function normalizeOfferings(offerings) {
|
|
212
|
+
if (!offerings) return [];
|
|
213
|
+
if (Array.isArray(offerings))
|
|
214
|
+
return offerings.map((k) => ({ slot: k, catalogKey: k }));
|
|
215
|
+
return Object.entries(offerings).map(([slot, catalogKey]) => ({
|
|
216
|
+
slot,
|
|
217
|
+
catalogKey: catalogKey ?? null
|
|
218
|
+
}));
|
|
214
219
|
}
|
|
215
|
-
function funnelCatalogKeys(
|
|
216
|
-
return [
|
|
220
|
+
function funnelCatalogKeys(offerings) {
|
|
221
|
+
return [
|
|
222
|
+
...new Set(
|
|
223
|
+
normalizeOfferings(offerings).map((s) => s.catalogKey).filter((k) => !!k)
|
|
224
|
+
)
|
|
225
|
+
];
|
|
217
226
|
}
|
|
218
227
|
|
|
219
228
|
// src/flow/experiments.ts
|
|
@@ -258,28 +267,21 @@ function weightsOf(variants) {
|
|
|
258
267
|
for (const [label, v] of Object.entries(variants)) out[label] = v.weight;
|
|
259
268
|
return out;
|
|
260
269
|
}
|
|
261
|
-
function statusOf(exp) {
|
|
262
|
-
return exp.status ?? "running";
|
|
263
|
-
}
|
|
264
270
|
function resolveExperiments(pages, experiments, seed) {
|
|
265
271
|
const assignments = {};
|
|
266
272
|
const render = {};
|
|
267
273
|
const experimentOf = {};
|
|
268
274
|
const versionOf = {};
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
assignments[exp.id] = label;
|
|
280
|
-
experimentOf[exp.slot] = exp.id;
|
|
281
|
-
if (exp.version !== void 0) versionOf[exp.id] = exp.version;
|
|
282
|
-
if (arm.page !== exp.slot) render[exp.slot] = arm.page;
|
|
275
|
+
if (seed) {
|
|
276
|
+
for (const exp of experiments) {
|
|
277
|
+
const label = assignVariant(exp.id, seed, weightsOf(exp.variants));
|
|
278
|
+
const arm = exp.variants[label];
|
|
279
|
+
if (!arm) continue;
|
|
280
|
+
assignments[exp.id] = label;
|
|
281
|
+
experimentOf[exp.slot] = exp.id;
|
|
282
|
+
if (exp.version !== void 0) versionOf[exp.id] = exp.version;
|
|
283
|
+
if (arm.page !== exp.slot) render[exp.slot] = arm.page;
|
|
284
|
+
}
|
|
283
285
|
}
|
|
284
286
|
const slotOf = {};
|
|
285
287
|
const activeKeys = [];
|
|
@@ -290,55 +292,6 @@ function resolveExperiments(pages, experiments, seed) {
|
|
|
290
292
|
}
|
|
291
293
|
return { assignments, activeKeys, render, slotOf, experimentOf, versionOf };
|
|
292
294
|
}
|
|
293
|
-
function validateExperiments(experiments, pages) {
|
|
294
|
-
const errors = [];
|
|
295
|
-
const warnings = [];
|
|
296
|
-
const pageKeys = new Set(pages.map((p) => p.key));
|
|
297
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
298
|
-
const slotOwner = /* @__PURE__ */ new Map();
|
|
299
|
-
for (const exp of experiments) {
|
|
300
|
-
const err = (code, message) => errors.push({ experimentId: exp.id, code, message });
|
|
301
|
-
const warn = (code, message) => warnings.push({ experimentId: exp.id, code, message });
|
|
302
|
-
if (seenIds.has(exp.id)) err("duplicate_id", `Experiment id "${exp.id}" is used more than once.`);
|
|
303
|
-
seenIds.add(exp.id);
|
|
304
|
-
if (!pageKeys.has(exp.slot)) err("slot_missing", `Slot "${exp.slot}" is not a page in the funnel.`);
|
|
305
|
-
if (isVariantKey(exp.slot)) err("slot_is_variant", `Slot "${exp.slot}" is a variant page; a slot must be a real flow page.`);
|
|
306
|
-
if (slotOwner.has(exp.slot)) {
|
|
307
|
-
err("slot_conflict", `Slot "${exp.slot}" is already targeted by experiment "${slotOwner.get(exp.slot)}".`);
|
|
308
|
-
} else {
|
|
309
|
-
slotOwner.set(exp.slot, exp.id);
|
|
310
|
-
}
|
|
311
|
-
const arms = Object.entries(exp.variants);
|
|
312
|
-
if (arms.length < 2) warn("single_arm", `Experiment "${exp.id}" has fewer than two variants \u2014 nothing to test.`);
|
|
313
|
-
let hasControl = false;
|
|
314
|
-
let totalWeight = 0;
|
|
315
|
-
for (const [label, arm] of arms) {
|
|
316
|
-
if (!pageKeys.has(arm.page)) {
|
|
317
|
-
err("variant_page_missing", `Variant "${label}" references page "${arm.page}", which doesn't exist.`);
|
|
318
|
-
}
|
|
319
|
-
if (arm.page === exp.slot) hasControl = true;
|
|
320
|
-
else if (isVariantKey(arm.page) && parseSlotKey(arm.page).slot !== exp.slot) {
|
|
321
|
-
err("variant_slot_mismatch", `Variant "${label}" page "${arm.page}" belongs to a different slot than "${exp.slot}".`);
|
|
322
|
-
}
|
|
323
|
-
if (arm.weight < 0) err("negative_weight", `Variant "${label}" has a negative weight (${arm.weight}).`);
|
|
324
|
-
totalWeight += arm.weight;
|
|
325
|
-
}
|
|
326
|
-
if (!hasControl) err("no_control", `No variant of "${exp.id}" renders the slot "${exp.slot}" itself (the control/baseline).`);
|
|
327
|
-
if (totalWeight <= 0) err("no_traffic", `Experiment "${exp.id}" has no positive weight \u2014 no traffic would enter it.`);
|
|
328
|
-
if (statusOf(exp) === "stopped" && exp.winner && !exp.variants[exp.winner]) {
|
|
329
|
-
err("bad_winner", `Stopped experiment "${exp.id}" names winner "${exp.winner}", which isn't one of its variants.`);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
const running = new Set(experiments.filter((e) => statusOf(e) === "running").flatMap(
|
|
333
|
-
(e) => Object.values(e.variants).map((v) => v.page)
|
|
334
|
-
));
|
|
335
|
-
for (const p of pages) {
|
|
336
|
-
if (isVariantKey(p.key) && !running.has(p.key)) {
|
|
337
|
-
warnings.push({ experimentId: "*", code: "orphan_variant", message: `Variant page "${p.key}" is rendered by no running experiment.` });
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return { ok: errors.length === 0, errors, warnings };
|
|
341
|
-
}
|
|
342
295
|
|
|
343
296
|
// src/manifest/manifest.ts
|
|
344
297
|
function isVariantNode(page) {
|
|
@@ -376,7 +329,16 @@ function compileManifest(input) {
|
|
|
376
329
|
return void 0;
|
|
377
330
|
};
|
|
378
331
|
pages.forEach((p, i) => {
|
|
379
|
-
if (isVariantNode(p))
|
|
332
|
+
if (isVariantNode(p)) {
|
|
333
|
+
for (const route of p.meta?.next ?? []) {
|
|
334
|
+
if (!keys.has(route.to)) {
|
|
335
|
+
badTargets.push({ from: p.key, to: route.to });
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
edges.push({ from: p.key, to: route.to, kind: "branch", variant: true, condition: serializeGate(route.when) });
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
380
342
|
const routes = p.meta?.next ?? [];
|
|
381
343
|
let hasUnconditional = false;
|
|
382
344
|
for (const route of routes) {
|
|
@@ -395,20 +357,25 @@ function compileManifest(input) {
|
|
|
395
357
|
const startPage = pages.find((p) => !isVariantNode(p));
|
|
396
358
|
const start = startPage?.key ?? null;
|
|
397
359
|
const variantKeys = new Set(pages.filter(isVariantNode).map((p) => p.key));
|
|
398
|
-
const
|
|
399
|
-
const validation = validate(manifestPages, edges, badTargets, start, variantKeys,
|
|
360
|
+
const offeringSlots = normalizeOfferings(funnel.offerings);
|
|
361
|
+
const validation = validate(manifestPages, edges, badTargets, start, variantKeys, offeringSlots, input.offeringRefs);
|
|
400
362
|
return {
|
|
401
363
|
id: funnel.id,
|
|
402
364
|
pages: manifestPages,
|
|
403
365
|
flow: { start, nodes: manifestPages.map((p) => p.id), edges },
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
366
|
+
offerings: funnelCatalogKeys(funnel.offerings),
|
|
367
|
+
offeringSlots,
|
|
368
|
+
offeringRefs: input.offeringRefs,
|
|
369
|
+
// Carry the funnel.ts `checkout:` bundle + the scanned checkout-surface usage for the publish
|
|
370
|
+
// capability gate. Both are provider-agnostic here (the SDK can't resolve offering→provider);
|
|
371
|
+
// the API gate combines them with the DB-resolved provider to run the (provider × surface) matrix.
|
|
372
|
+
checkout: funnel.checkout,
|
|
373
|
+
checkoutRefs: input.checkoutRefs,
|
|
407
374
|
locales: funnel.locales,
|
|
408
375
|
validation
|
|
409
376
|
};
|
|
410
377
|
}
|
|
411
|
-
function validate(pages, edges, badTargets, start, variantKeys, slots,
|
|
378
|
+
function validate(pages, edges, badTargets, start, variantKeys, slots, offeringRefs) {
|
|
412
379
|
const outgoing = /* @__PURE__ */ new Map();
|
|
413
380
|
for (const e of edges) outgoing.set(e.from, (outgoing.get(e.from) ?? 0) + 1);
|
|
414
381
|
const deadEnds = pages.filter((p) => p.type !== "finish" && !variantKeys.has(p.id) && !(outgoing.get(p.id) > 0)).map((p) => p.id);
|
|
@@ -420,6 +387,12 @@ function validate(pages, edges, badTargets, start, variantKeys, slots, productRe
|
|
|
420
387
|
if (list) list.push(e.to);
|
|
421
388
|
else adj.set(e.from, [e.to]);
|
|
422
389
|
}
|
|
390
|
+
for (const p of pages) {
|
|
391
|
+
if (p.variantOf === void 0) continue;
|
|
392
|
+
const list = adj.get(p.variantOf);
|
|
393
|
+
if (list) list.push(p.id);
|
|
394
|
+
else adj.set(p.variantOf, [p.id]);
|
|
395
|
+
}
|
|
423
396
|
const stack = [start];
|
|
424
397
|
while (stack.length) {
|
|
425
398
|
const id = stack.pop();
|
|
@@ -449,12 +422,12 @@ function validate(pages, edges, badTargets, start, variantKeys, slots, productRe
|
|
|
449
422
|
const noReachableFinish = start !== null && !pages.some((p) => p.type === "finish" && reachable.has(p.id));
|
|
450
423
|
const declaredSlots = new Set(slots.map((s) => s.slot));
|
|
451
424
|
const assignedSlots = new Set(slots.filter((s) => s.catalogKey).map((s) => s.slot));
|
|
452
|
-
const refs =
|
|
453
|
-
const
|
|
425
|
+
const refs = offeringRefs ?? [];
|
|
426
|
+
const unknownOfferingRefs = [...new Set(refs.filter((r) => !declaredSlots.has(r)))];
|
|
454
427
|
const unassignedSlots = [...new Set(refs.filter((r) => declaredSlots.has(r) && !assignedSlots.has(r)))];
|
|
455
428
|
return {
|
|
456
429
|
// Original hard errors AND the structural ones; slug collisions stay a warning.
|
|
457
|
-
ok: deadEnds.length === 0 && unreachable.length === 0 && badTargets.length === 0 && !missingEmailCapture && duplicateKeys.length === 0 && orphanVariants.length === 0 && !noReachableFinish &&
|
|
430
|
+
ok: deadEnds.length === 0 && unreachable.length === 0 && badTargets.length === 0 && !missingEmailCapture && duplicateKeys.length === 0 && orphanVariants.length === 0 && !noReachableFinish && unknownOfferingRefs.length === 0 && unassignedSlots.length === 0,
|
|
458
431
|
deadEnds,
|
|
459
432
|
unreachable,
|
|
460
433
|
badTargets,
|
|
@@ -463,11 +436,11 @@ function validate(pages, edges, badTargets, start, variantKeys, slots, productRe
|
|
|
463
436
|
slugCollisions,
|
|
464
437
|
orphanVariants,
|
|
465
438
|
noReachableFinish,
|
|
466
|
-
|
|
439
|
+
unknownOfferingRefs,
|
|
467
440
|
unassignedSlots
|
|
468
441
|
};
|
|
469
442
|
}
|
|
470
443
|
|
|
471
|
-
export { assignVariant, bucketingSeed, buildCatalog, compileManifest, currencyExponent, defineFunnel, definePage, entryGuard, evaluateCondition, evaluateGate, expectedPathLength, fnv1a, formatMoney,
|
|
472
|
-
//# sourceMappingURL=chunk-
|
|
473
|
-
//# sourceMappingURL=chunk-
|
|
444
|
+
export { assignVariant, bucketingSeed, buildCatalog, compileManifest, currencyExponent, defineFunnel, definePage, entryGuard, evaluateCondition, evaluateGate, expectedPathLength, fnv1a, formatMoney, formatOffering, funnelCatalogKeys, hashToUnit, isRtl, isVariantKey, nextPage, normalizeOfferings, outgoingKeys, pageMeta, parseSlotKey, pickByWeight, resolveExperiments, resolveLocale, resolveOffering, resolveRoute, weightsOf };
|
|
445
|
+
//# sourceMappingURL=chunk-VW2HVPR4.js.map
|
|
446
|
+
//# sourceMappingURL=chunk-VW2HVPR4.js.map
|