@appfunnel-dev/sdk 2.0.0-canary.1 → 2.0.0-canary.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/dist/capabilities-Dq22RCr_.d.cts +180 -0
  3. package/dist/capabilities-Dq22RCr_.d.ts +180 -0
  4. package/dist/checkout-ClaO5IYV.d.ts +333 -0
  5. package/dist/checkout-DBp4bCpC.d.cts +333 -0
  6. package/dist/chunk-7JLOF6CJ.cjs +172 -0
  7. package/dist/chunk-7JLOF6CJ.cjs.map +1 -0
  8. package/dist/chunk-EMMSS5I5.cjs +37 -0
  9. package/dist/chunk-EMMSS5I5.cjs.map +1 -0
  10. package/dist/chunk-G3PMV62Z.js +33 -0
  11. package/dist/chunk-G3PMV62Z.js.map +1 -0
  12. package/dist/chunk-JAO6AA4R.js +99 -0
  13. package/dist/chunk-JAO6AA4R.js.map +1 -0
  14. package/dist/chunk-OXQBEKZ5.js +157 -0
  15. package/dist/chunk-OXQBEKZ5.js.map +1 -0
  16. package/dist/chunk-RKPRMTSU.cjs +517 -0
  17. package/dist/chunk-RKPRMTSU.cjs.map +1 -0
  18. package/dist/chunk-SXENKZ4U.js +486 -0
  19. package/dist/chunk-SXENKZ4U.js.map +1 -0
  20. package/dist/chunk-VV7TFC64.cjs +106 -0
  21. package/dist/chunk-VV7TFC64.cjs.map +1 -0
  22. package/dist/chunk-WYUDL4FI.cjs +8 -0
  23. package/dist/chunk-WYUDL4FI.cjs.map +1 -0
  24. package/dist/chunk-ZZJG4EYL.js +6 -0
  25. package/dist/chunk-ZZJG4EYL.js.map +1 -0
  26. package/dist/driver-paddle.cjs +817 -0
  27. package/dist/driver-paddle.cjs.map +1 -0
  28. package/dist/driver-paddle.d.cts +10 -0
  29. package/dist/driver-paddle.d.ts +10 -0
  30. package/dist/driver-paddle.js +814 -0
  31. package/dist/driver-paddle.js.map +1 -0
  32. package/dist/driver-stripe.cjs +2312 -0
  33. package/dist/driver-stripe.cjs.map +1 -0
  34. package/dist/driver-stripe.d.cts +24 -0
  35. package/dist/driver-stripe.d.ts +24 -0
  36. package/dist/driver-stripe.js +2305 -0
  37. package/dist/driver-stripe.js.map +1 -0
  38. package/dist/index.cjs +2677 -846
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +358 -1005
  41. package/dist/index.d.ts +358 -1005
  42. package/dist/index.js +2395 -698
  43. package/dist/index.js.map +1 -1
  44. package/dist/manifest-C2mDXWNU.d.cts +996 -0
  45. package/dist/manifest-C2mDXWNU.d.ts +996 -0
  46. package/dist/manifest-entry.cjs +408 -0
  47. package/dist/manifest-entry.cjs.map +1 -0
  48. package/dist/manifest-entry.d.cts +192 -0
  49. package/dist/manifest-entry.d.ts +192 -0
  50. package/dist/manifest-entry.js +270 -0
  51. package/dist/manifest-entry.js.map +1 -0
  52. package/dist/protocol.cjs +13 -0
  53. package/dist/protocol.cjs.map +1 -0
  54. package/dist/protocol.d.cts +182 -0
  55. package/dist/protocol.d.ts +182 -0
  56. package/dist/protocol.js +4 -0
  57. package/dist/protocol.js.map +1 -0
  58. package/package.json +49 -4
@@ -0,0 +1,333 @@
1
+ import { ReactNode, ComponentType } from 'react';
2
+ import { c as CheckoutProvider, b as CheckoutIntent, d as CheckoutSurface, U as UpsellKind, C as CheckoutError, a as CheckoutErrorCategory, f as SurfacePreference } from './capabilities-Dq22RCr_.js';
3
+
4
+ /**
5
+ * v2 commerce primitives — the two intents from phase-3, made trivial.
6
+ *
7
+ * - `<Checkout offering surface>` — the **purchase** primitive. The `surface`
8
+ * prop picks presentation; some surfaces render **inline** (express wallets,
9
+ * inline card, provider iframe), some **open on click** (sheet/popup/redirect).
10
+ * `<Checkout>` covers both: it renders inline surfaces and renders a trigger
11
+ * button for the rest (`asChild` to use your own button).
12
+ * - `<Upsell offering>` — the **upsell** primitive. Off-session charge to the
13
+ * saved method, no card re-entry, one tap.
14
+ * - `useCheckout()` — the headless hook under both, always available for a
15
+ * fully-custom CTA.
16
+ *
17
+ * The SDK never talks to a provider. It owns the state machine + the analytics
18
+ * (`checkout.start` / `purchase.complete`) + post-charge navigation, and
19
+ * delegates the actual charge/render to an injected {@link CheckoutDriver}
20
+ * (the real one is platform-side; tests use {@link createMockDriver}).
21
+ *
22
+ * Which offering is charged is the author's bound variable, captured at render
23
+ * (inline surfaces) or at `open()` time (trigger surfaces).
24
+ */
25
+
26
+ type CheckoutStatus = 'idle' | 'loading' | 'requires_action' | 'success' | 'error';
27
+ interface CheckoutRequest {
28
+ /** The offering SLOT the page named (`<Checkout offering="primary">`) — the display/analytics identity. Kept as the wire field `product` at the provider boundary. */
29
+ product: string;
30
+ /** The catalog charge KEY charged — the CHARGE identity, resolved from `product` by {@link driverWithCatalog}. */
31
+ catalogKey?: string;
32
+ intent: CheckoutIntent;
33
+ surface?: CheckoutSurface;
34
+ /** For `upsell` intent — which kind of off-session charge (default `subscription`). */
35
+ kind?: UpsellKind;
36
+ /**
37
+ * Buyer email at attempt time, read from the funnel's `user.email` namespace value
38
+ * (injected by `FunnelProvider` via {@link driverWithEmail} — no author wiring).
39
+ * Drivers forward it into the wire `CheckoutSessionRequest.email` (Stripe Customer
40
+ * creation / receipts / Paddle prefill).
41
+ */
42
+ email?: string;
43
+ }
44
+ interface CheckoutResult {
45
+ ok: boolean;
46
+ error?: CheckoutError;
47
+ /** Charged amount in minor units (for the purchase event). */
48
+ amountMinor?: number;
49
+ currency?: string;
50
+ /**
51
+ * The **server-minted** dedup id for the purchase event — the platform creates it
52
+ * when the charge settles and fires CAPI with it; the SDK emits `purchase.complete`
53
+ * with the same id so the browser pixel dedupes against the server. (Server is the
54
+ * source of truth for payment events; the SDK doesn't mint these.)
55
+ */
56
+ eventId?: string;
57
+ }
58
+ interface CheckoutCallbacks {
59
+ onSuccess: (result: CheckoutResult) => void;
60
+ onError: (error: CheckoutError) => void;
61
+ /**
62
+ * The buyer SUBMITTED an inline surface (Stripe confirm click / wallet
63
+ * authorization; Paddle inline checkout opening). This is when `checkout.start`
64
+ * fires and the status machine enters `loading` for inline attempts — trigger
65
+ * surfaces fire it at click time instead. It also marks fresh user intent, so
66
+ * the once-per-chain recovery guard resets. Optional: drivers that can't
67
+ * observe submit simply never call it, and the SHEET host deliberately doesn't
68
+ * forward it (the sheet's own open already began the attempt — re-firing would
69
+ * double-count starts and un-arm the recovery guard mid-chain).
70
+ */
71
+ onStart?: () => void;
72
+ /**
73
+ * The inline surface reports a valid payment method was entered (e.g. Stripe's
74
+ * Payment Element became `complete`), before the charge. Drives the
75
+ * `checkout.payment_added` analytics step; optional — drivers that can't observe
76
+ * it (or off-session charges) simply never call it.
77
+ */
78
+ onPaymentAdded?: () => void;
79
+ }
80
+ /**
81
+ * What a driver factory (`stripeCheckoutDriver` et al. from the
82
+ * `@appfunnel-dev/sdk/driver-*` entries) receives to construct a
83
+ * {@link CheckoutDriver}. Derived by `createFunnelTree` from the SAME
84
+ * renderer-injected mount data the platform tracker uses (tracking config +
85
+ * identity), so checkout wire calls land on the exact campaign/funnel/session
86
+ * the analytics do — there is no second config channel to drift.
87
+ *
88
+ * Serializable on purpose, and factories must be **construction-safe on the
89
+ * server too**: a factory only closes over this ctx (no provider SDK, no
90
+ * `window`) — all provider work belongs in effects/event handlers, so SSR can
91
+ * render the driver's stable inline placeholder and hydration matches.
92
+ */
93
+ interface CheckoutDriverContext {
94
+ /** Public API origin, e.g. `https://api.appfunnel.net` (no trailing slash). */
95
+ apiBase: string;
96
+ /** Campaign DB id (the checkout route key, same as the ingestion route key). */
97
+ campaignId: string;
98
+ /** WebFunnel DB id (NOT the funnel.ts config id). */
99
+ funnelId: string;
100
+ /** Render-time mode — the server resolves the product binding + store credentials by it. */
101
+ mode: 'test' | 'live';
102
+ /** Funnel session id when one exists (analytics + customer continuity). */
103
+ sessionId?: string | null;
104
+ /** Signed-cookie-verified bare visitor id, when known. */
105
+ visitorId?: string | null;
106
+ }
107
+ /**
108
+ * The provider seam. The platform injects the real one (Stripe/Paddle/Whop/…);
109
+ * tests use {@link createMockDriver}.
110
+ *
111
+ * Most providers ship their **own React SDK** (`@stripe/react-stripe-js`,
112
+ * `@paddle/paddle-js`, Primer's React components, …). A driver's `renderInline`
113
+ * returns exactly that: the provider's React element mounted in our chrome (inline,
114
+ * or inside the `<Sheet>`). Those provider SDKs live in the **driver/platform**
115
+ * package, not our core funnel SDK — which is why the driver is injected: the core
116
+ * stays lean and provider-agnostic, and you only pull the SDK for the provider you use.
117
+ */
118
+ interface CheckoutDriver {
119
+ provider: CheckoutProvider;
120
+ /** Trigger a charge (trigger surfaces + programmatic). Resolves when settled. */
121
+ start(req: CheckoutRequest): Promise<CheckoutResult>;
122
+ /** Render an inline surface (e.g. the provider's React payment element); report via callbacks. */
123
+ renderInline?(req: CheckoutRequest, cb: CheckoutCallbacks): ReactNode;
124
+ /**
125
+ * Verify an EXISTING attempt with the server (`POST /complete`, bounded re-poll on
126
+ * `pending`) — the redirect-return resume leg: hosted-checkout success URLs carry
127
+ * `?af_checkout={correlationId}` back into the funnel, and {@link CheckoutResume}
128
+ * hands the id here so the settlement (server-verified) resolves into the normal
129
+ * success/failure paths. Never charges; only verifies.
130
+ */
131
+ resume?(correlationId: string): Promise<CheckoutResult>;
132
+ }
133
+ interface MockDriverOptions {
134
+ /** The result a successful charge resolves with. */
135
+ result?: CheckoutResult;
136
+ /** Make **off-session** charges (no surface) fail — to exercise the upsell fallback. */
137
+ failOffSession?: CheckoutError | boolean;
138
+ }
139
+ /** A console/no-op driver — tests, preview, local dev. Succeeds unless configured to fail. */
140
+ declare function createMockDriver(provider?: CheckoutProvider, options?: MockDriverOptions): CheckoutDriver;
141
+ /**
142
+ * Wrap a driver so every attempt carries the buyer email the funnel has collected
143
+ * (`user.email`), read at ATTEMPT time — `start()` for trigger surfaces, the
144
+ * `renderInline` request for mounted surfaces. Applied by `FunnelProvider` around
145
+ * the injected driver, so `useCheckout`, `<Checkout>` inline branches and the
146
+ * checkout sheet all forward it without wiring; drivers pass it into the wire's
147
+ * `CheckoutSessionRequest.email`. An explicit `req.email` always wins.
148
+ */
149
+ declare function driverWithEmail(driver: CheckoutDriver, getEmail: () => string | undefined): CheckoutDriver;
150
+ /** One declarative recovery route for a failure category. */
151
+ type FailureRoute = {
152
+ go: string;
153
+ } | {
154
+ modal: string | ComponentType<any>;
155
+ } | {
156
+ fallback: CheckoutSurface;
157
+ } | ((error: CheckoutError) => void);
158
+ /**
159
+ * What a `{ modal }` failure route hands the modal — the error AND the actions,
160
+ * because registry modals mount outside the flow's nav context and a recovery
161
+ * modal that can't act is just an error toast. Type your modal with this:
162
+ *
163
+ * ```tsx
164
+ * const Declined = defineModal<FailureModalProps>(({ error, go, retry }) => (
165
+ * <Sheet>
166
+ * <p>{error.category === 'prepaid_card' ? 'Prepaid cards can't start a trial.' : 'Payment didn't go through.'}</p>
167
+ * <button onClick={() => retry('sheet')}>Try another card</button>
168
+ * <button onClick={() => go('downsell')}>See the lite plan</button>
169
+ * </Sheet>
170
+ * ))
171
+ * ```
172
+ *
173
+ * A modal can also host a whole `<Checkout offering="…" surface="element" />` for
174
+ * an inline downsell — the driver context is app-level, so checkout works inside
175
+ * modals as-is.
176
+ */
177
+ interface FailureModalProps {
178
+ error: CheckoutError;
179
+ /** The product whose charge failed (absent only for product-less failures). */
180
+ product?: string;
181
+ /** Navigate the flow to a page key (modals can't call useNavigation themselves). */
182
+ go: (pageKey: string) => void;
183
+ /**
184
+ * Re-attempt the SAME product. `retry()` re-runs an upsell off-session; for a
185
+ * purchase (which needs UI to re-collect) it opens the payment sheet.
186
+ * `retry('sheet')` re-collects on the SDK sheet; an inline surface
187
+ * (`element`/`express`/`embedded`) is COERCED to the sheet when the provider
188
+ * supports it — a retry has no page slot to mount into, so the sheet hosts the
189
+ * element. On sheet-less MoR providers (Paddle) inline retries run through the
190
+ * driver, whose provider checkout opens its own overlay. `retry('popup')` /
191
+ * `retry('redirect')` hand off to the provider. A failed retry routes through
192
+ * `onFailed` again (the modal reopens) — the loop is user-driven, never
193
+ * automatic. Hide the modal first (`useModal().hide()`) when retrying into a
194
+ * surface that opens its own overlay.
195
+ */
196
+ retry: (surface?: CheckoutSurface) => void;
197
+ }
198
+ /**
199
+ * Declarative failure recovery — route each failure **category** to what happens
200
+ * next, instead of hand-wiring `onError`:
201
+ *
202
+ * ```tsx
203
+ * <Checkout offering="monthly" surface="element" onFailed={{
204
+ * prepaid_card: { go: 'real-card' },
205
+ * insufficient_funds: { modal: DownsellModal },
206
+ * default: { modal: TryAgainModal },
207
+ * }} />
208
+ * ```
209
+ *
210
+ * Resolution: exact category → `default` → nothing (the error state + `checkout.
211
+ * failed` event stand, as today). Two built-in judgments:
212
+ * - `canceled` only routes when mapped EXPLICITLY — a deliberately dismissed
213
+ * sheet/popup is not a failure to recover from, so `default` never catches it;
214
+ * - a `{ fallback }` recovery runs at most once per attempt chain (its own
215
+ * failure routes through the non-fallback routes), so declines can't loop.
216
+ *
217
+ * `checkout.failed` fires with category + declineCode on every failure regardless
218
+ * of routing — recovery pages are measurable, and being ordinary flow pages they
219
+ * are A/B-able with page experiments like anything else.
220
+ */
221
+ type OnFailedMap = Partial<Record<CheckoutErrorCategory | 'default', FailureRoute>>;
222
+ interface UseCheckoutOptions {
223
+ intent?: CheckoutIntent;
224
+ /** For `upsell` intent — which kind of off-session charge (default `subscription`). */
225
+ kind?: UpsellKind;
226
+ /** Advance to the next page after a successful charge (default: true). */
227
+ advanceOnSuccess?: boolean;
228
+ onSuccess?: (result: CheckoutResult) => void;
229
+ onError?: (error: CheckoutError) => void;
230
+ /** Declarative failure recovery — see {@link OnFailedMap}. Runs AFTER `onError`. */
231
+ onFailed?: OnFailedMap;
232
+ /**
233
+ * Try this surface ON-SESSION once on any first failure, before `onFailed`
234
+ * routing — the generalized `<Upsell fallback>`: the off-session (or first)
235
+ * charge fails, the surface re-collects a card, the retry's failure then goes
236
+ * through `onFailed`. Never re-triggered by its own failure; never triggered
237
+ * by a user-`canceled` checkout.
238
+ */
239
+ recoverySurface?: CheckoutSurface;
240
+ }
241
+ interface CheckoutHandle {
242
+ /** Start the charge for a product; resolves with the result (no surface = off-session). */
243
+ open: (product: string, surface?: CheckoutSurface) => Promise<CheckoutResult>;
244
+ status: CheckoutStatus;
245
+ /** The last failure, structured so you can route/recover by `category`. */
246
+ error: CheckoutError | null;
247
+ isLoading: boolean;
248
+ reset: () => void;
249
+ /** Build the callbacks an inline surface reports through (used by `<Checkout>`). */
250
+ callbacksFor: (product?: string, surface?: CheckoutSurface) => CheckoutCallbacks & {
251
+ onStart: () => void;
252
+ };
253
+ }
254
+ /**
255
+ * The headless checkout hook. Owns the state machine + analytics + post-charge
256
+ * navigation; delegates the charge to the injected {@link CheckoutDriver}.
257
+ */
258
+ declare function useCheckout(opts?: UseCheckoutOptions): CheckoutHandle;
259
+ interface CheckoutProps extends UseCheckoutOptions {
260
+ /** Logical offering slot (the author's bound selection). */
261
+ offering: string;
262
+ /**
263
+ * Presentation preference. A concrete {@link CheckoutSurface} pins it; `'auto'` lets the platform
264
+ * pick the best surface for the store's provider and the buyer's device — wallet-first on mobile
265
+ * (Stripe → ExpressCheckoutElement, Paddle → its Express Checkout variant), inline card/embedded
266
+ * on desktop (see {@link resolveSurface}). NOTE: `surface` has no default (kept required) so pinned
267
+ * funnels don't change behavior this canary; `'auto'` is opt-in. New authoring (editor + AI) should
268
+ * emit `surface="auto"` going forward so wallet-first is a funnel intent, not a provider prop.
269
+ */
270
+ surface: SurfacePreference;
271
+ /** Render your own trigger element instead of the default button (trigger surfaces). */
272
+ asChild?: boolean;
273
+ /** Trigger label / your trigger element. */
274
+ children?: ReactNode;
275
+ className?: string;
276
+ }
277
+ /**
278
+ * The purchase primitive. Three presentation modes by the RESOLVED `surface`:
279
+ * - **inline** (`express`/`element`/`embedded`) — the driver renders the
280
+ * provider UI in place (keyed by product so a selection change remounts it);
281
+ * - **SDK sheet** (`sheet`) — a trigger opens the Appfunnel-owned {@link Sheet}
282
+ * and the driver renders the provider element inside it;
283
+ * - **provider-hosted** (`popup`/`redirect`) — a trigger hands off to the
284
+ * provider's own overlay/page.
285
+ *
286
+ * The `surface` prop is a {@link SurfacePreference}: a concrete surface (pinned) or `'auto'`, which
287
+ * {@link resolveSurface} maps to the best surface for the driver's provider + this device — ONCE per
288
+ * mount (frozen so the surface can't change mid-attempt and remount the provider element into a new
289
+ * charge). The resolved literal is what flows onto the wire; the server re-validates it.
290
+ */
291
+ declare function Checkout({ offering, surface: surfacePreference, asChild, children, className, ...options }: CheckoutProps): ReactNode;
292
+ interface UpsellProps extends Omit<UseCheckoutOptions, 'intent'> {
293
+ offering: string;
294
+ /**
295
+ * If the off-session charge fails, present this surface **on-session** to
296
+ * re-collect a card and charge again — e.g. `fallback="sheet"` opens the
297
+ * payment sheet, `fallback="popup"` the provider overlay. Omit to just report
298
+ * the failure (via `onError` / the `checkout.failed` event).
299
+ */
300
+ fallback?: CheckoutSurface;
301
+ asChild?: boolean;
302
+ children?: ReactNode;
303
+ className?: string;
304
+ }
305
+ /**
306
+ * The upsell primitive — an off-session, one-tap charge to the saved method (no
307
+ * surface, no card re-entry). On failure, optionally `fallback` to an on-session
308
+ * surface to re-collect and retry — `fallback` IS the hook's `recoverySurface`,
309
+ * so the retry's own failure routes through `onFailed` (e.g. prepaid_card → a
310
+ * recapture page) instead of dead-ending.
311
+ */
312
+ declare function Upsell({ offering, fallback, asChild, children, className, ...options }: UpsellProps): ReactNode;
313
+ /**
314
+ * The dangling settle leg of the `redirect` surface. The server appends
315
+ * `af_checkout={correlationId}` to hosted-checkout success URLs; when the buyer
316
+ * lands back in the funnel this component detects the param, has the server
317
+ * VERIFY the attempt (`driver.resume` → `POST /complete`, bounded re-poll while
318
+ * `pending`), and resolves it into the normal machinery:
319
+ *
320
+ * - `settled` → `purchase.complete` fires with the settlement's server-minted
321
+ * `eventId` (pixel↔CAPI dedup), the flow advances, and the param is stripped
322
+ * (`history.replaceState`) so a reload can't re-run the leg;
323
+ * - `failed` → the normal failure path (`checkout.failed` + status `error`;
324
+ * any `onFailed` maps passed as props apply — product-less, so only
325
+ * `go`/handler routes can act).
326
+ *
327
+ * SSR-safe (effect-only) and runs once per correlationId per page load. Mounted
328
+ * automatically by `createFunnelTree` inside the nav context; needs a driver
329
+ * with `resume` (both platform drivers ship it; the mock resolves its result).
330
+ */
331
+ declare function CheckoutResume(options?: UseCheckoutOptions): ReactNode;
332
+
333
+ export { type CheckoutDriverContext as C, type FailureModalProps as F, type MockDriverOptions as M, type OnFailedMap as O, Upsell as U, type CheckoutDriver as a, Checkout as b, type CheckoutCallbacks as c, type CheckoutHandle as d, type CheckoutProps as e, type CheckoutRequest as f, type CheckoutResult as g, CheckoutResume as h, type CheckoutStatus as i, type FailureRoute as j, type UpsellProps as k, type UseCheckoutOptions as l, createMockDriver as m, driverWithEmail as n, useCheckout as u };
@@ -0,0 +1,333 @@
1
+ import { ReactNode, ComponentType } from 'react';
2
+ import { c as CheckoutProvider, b as CheckoutIntent, d as CheckoutSurface, U as UpsellKind, C as CheckoutError, a as CheckoutErrorCategory, f as SurfacePreference } from './capabilities-Dq22RCr_.cjs';
3
+
4
+ /**
5
+ * v2 commerce primitives — the two intents from phase-3, made trivial.
6
+ *
7
+ * - `<Checkout offering surface>` — the **purchase** primitive. The `surface`
8
+ * prop picks presentation; some surfaces render **inline** (express wallets,
9
+ * inline card, provider iframe), some **open on click** (sheet/popup/redirect).
10
+ * `<Checkout>` covers both: it renders inline surfaces and renders a trigger
11
+ * button for the rest (`asChild` to use your own button).
12
+ * - `<Upsell offering>` — the **upsell** primitive. Off-session charge to the
13
+ * saved method, no card re-entry, one tap.
14
+ * - `useCheckout()` — the headless hook under both, always available for a
15
+ * fully-custom CTA.
16
+ *
17
+ * The SDK never talks to a provider. It owns the state machine + the analytics
18
+ * (`checkout.start` / `purchase.complete`) + post-charge navigation, and
19
+ * delegates the actual charge/render to an injected {@link CheckoutDriver}
20
+ * (the real one is platform-side; tests use {@link createMockDriver}).
21
+ *
22
+ * Which offering is charged is the author's bound variable, captured at render
23
+ * (inline surfaces) or at `open()` time (trigger surfaces).
24
+ */
25
+
26
+ type CheckoutStatus = 'idle' | 'loading' | 'requires_action' | 'success' | 'error';
27
+ interface CheckoutRequest {
28
+ /** The offering SLOT the page named (`<Checkout offering="primary">`) — the display/analytics identity. Kept as the wire field `product` at the provider boundary. */
29
+ product: string;
30
+ /** The catalog charge KEY charged — the CHARGE identity, resolved from `product` by {@link driverWithCatalog}. */
31
+ catalogKey?: string;
32
+ intent: CheckoutIntent;
33
+ surface?: CheckoutSurface;
34
+ /** For `upsell` intent — which kind of off-session charge (default `subscription`). */
35
+ kind?: UpsellKind;
36
+ /**
37
+ * Buyer email at attempt time, read from the funnel's `user.email` namespace value
38
+ * (injected by `FunnelProvider` via {@link driverWithEmail} — no author wiring).
39
+ * Drivers forward it into the wire `CheckoutSessionRequest.email` (Stripe Customer
40
+ * creation / receipts / Paddle prefill).
41
+ */
42
+ email?: string;
43
+ }
44
+ interface CheckoutResult {
45
+ ok: boolean;
46
+ error?: CheckoutError;
47
+ /** Charged amount in minor units (for the purchase event). */
48
+ amountMinor?: number;
49
+ currency?: string;
50
+ /**
51
+ * The **server-minted** dedup id for the purchase event — the platform creates it
52
+ * when the charge settles and fires CAPI with it; the SDK emits `purchase.complete`
53
+ * with the same id so the browser pixel dedupes against the server. (Server is the
54
+ * source of truth for payment events; the SDK doesn't mint these.)
55
+ */
56
+ eventId?: string;
57
+ }
58
+ interface CheckoutCallbacks {
59
+ onSuccess: (result: CheckoutResult) => void;
60
+ onError: (error: CheckoutError) => void;
61
+ /**
62
+ * The buyer SUBMITTED an inline surface (Stripe confirm click / wallet
63
+ * authorization; Paddle inline checkout opening). This is when `checkout.start`
64
+ * fires and the status machine enters `loading` for inline attempts — trigger
65
+ * surfaces fire it at click time instead. It also marks fresh user intent, so
66
+ * the once-per-chain recovery guard resets. Optional: drivers that can't
67
+ * observe submit simply never call it, and the SHEET host deliberately doesn't
68
+ * forward it (the sheet's own open already began the attempt — re-firing would
69
+ * double-count starts and un-arm the recovery guard mid-chain).
70
+ */
71
+ onStart?: () => void;
72
+ /**
73
+ * The inline surface reports a valid payment method was entered (e.g. Stripe's
74
+ * Payment Element became `complete`), before the charge. Drives the
75
+ * `checkout.payment_added` analytics step; optional — drivers that can't observe
76
+ * it (or off-session charges) simply never call it.
77
+ */
78
+ onPaymentAdded?: () => void;
79
+ }
80
+ /**
81
+ * What a driver factory (`stripeCheckoutDriver` et al. from the
82
+ * `@appfunnel-dev/sdk/driver-*` entries) receives to construct a
83
+ * {@link CheckoutDriver}. Derived by `createFunnelTree` from the SAME
84
+ * renderer-injected mount data the platform tracker uses (tracking config +
85
+ * identity), so checkout wire calls land on the exact campaign/funnel/session
86
+ * the analytics do — there is no second config channel to drift.
87
+ *
88
+ * Serializable on purpose, and factories must be **construction-safe on the
89
+ * server too**: a factory only closes over this ctx (no provider SDK, no
90
+ * `window`) — all provider work belongs in effects/event handlers, so SSR can
91
+ * render the driver's stable inline placeholder and hydration matches.
92
+ */
93
+ interface CheckoutDriverContext {
94
+ /** Public API origin, e.g. `https://api.appfunnel.net` (no trailing slash). */
95
+ apiBase: string;
96
+ /** Campaign DB id (the checkout route key, same as the ingestion route key). */
97
+ campaignId: string;
98
+ /** WebFunnel DB id (NOT the funnel.ts config id). */
99
+ funnelId: string;
100
+ /** Render-time mode — the server resolves the product binding + store credentials by it. */
101
+ mode: 'test' | 'live';
102
+ /** Funnel session id when one exists (analytics + customer continuity). */
103
+ sessionId?: string | null;
104
+ /** Signed-cookie-verified bare visitor id, when known. */
105
+ visitorId?: string | null;
106
+ }
107
+ /**
108
+ * The provider seam. The platform injects the real one (Stripe/Paddle/Whop/…);
109
+ * tests use {@link createMockDriver}.
110
+ *
111
+ * Most providers ship their **own React SDK** (`@stripe/react-stripe-js`,
112
+ * `@paddle/paddle-js`, Primer's React components, …). A driver's `renderInline`
113
+ * returns exactly that: the provider's React element mounted in our chrome (inline,
114
+ * or inside the `<Sheet>`). Those provider SDKs live in the **driver/platform**
115
+ * package, not our core funnel SDK — which is why the driver is injected: the core
116
+ * stays lean and provider-agnostic, and you only pull the SDK for the provider you use.
117
+ */
118
+ interface CheckoutDriver {
119
+ provider: CheckoutProvider;
120
+ /** Trigger a charge (trigger surfaces + programmatic). Resolves when settled. */
121
+ start(req: CheckoutRequest): Promise<CheckoutResult>;
122
+ /** Render an inline surface (e.g. the provider's React payment element); report via callbacks. */
123
+ renderInline?(req: CheckoutRequest, cb: CheckoutCallbacks): ReactNode;
124
+ /**
125
+ * Verify an EXISTING attempt with the server (`POST /complete`, bounded re-poll on
126
+ * `pending`) — the redirect-return resume leg: hosted-checkout success URLs carry
127
+ * `?af_checkout={correlationId}` back into the funnel, and {@link CheckoutResume}
128
+ * hands the id here so the settlement (server-verified) resolves into the normal
129
+ * success/failure paths. Never charges; only verifies.
130
+ */
131
+ resume?(correlationId: string): Promise<CheckoutResult>;
132
+ }
133
+ interface MockDriverOptions {
134
+ /** The result a successful charge resolves with. */
135
+ result?: CheckoutResult;
136
+ /** Make **off-session** charges (no surface) fail — to exercise the upsell fallback. */
137
+ failOffSession?: CheckoutError | boolean;
138
+ }
139
+ /** A console/no-op driver — tests, preview, local dev. Succeeds unless configured to fail. */
140
+ declare function createMockDriver(provider?: CheckoutProvider, options?: MockDriverOptions): CheckoutDriver;
141
+ /**
142
+ * Wrap a driver so every attempt carries the buyer email the funnel has collected
143
+ * (`user.email`), read at ATTEMPT time — `start()` for trigger surfaces, the
144
+ * `renderInline` request for mounted surfaces. Applied by `FunnelProvider` around
145
+ * the injected driver, so `useCheckout`, `<Checkout>` inline branches and the
146
+ * checkout sheet all forward it without wiring; drivers pass it into the wire's
147
+ * `CheckoutSessionRequest.email`. An explicit `req.email` always wins.
148
+ */
149
+ declare function driverWithEmail(driver: CheckoutDriver, getEmail: () => string | undefined): CheckoutDriver;
150
+ /** One declarative recovery route for a failure category. */
151
+ type FailureRoute = {
152
+ go: string;
153
+ } | {
154
+ modal: string | ComponentType<any>;
155
+ } | {
156
+ fallback: CheckoutSurface;
157
+ } | ((error: CheckoutError) => void);
158
+ /**
159
+ * What a `{ modal }` failure route hands the modal — the error AND the actions,
160
+ * because registry modals mount outside the flow's nav context and a recovery
161
+ * modal that can't act is just an error toast. Type your modal with this:
162
+ *
163
+ * ```tsx
164
+ * const Declined = defineModal<FailureModalProps>(({ error, go, retry }) => (
165
+ * <Sheet>
166
+ * <p>{error.category === 'prepaid_card' ? 'Prepaid cards can't start a trial.' : 'Payment didn't go through.'}</p>
167
+ * <button onClick={() => retry('sheet')}>Try another card</button>
168
+ * <button onClick={() => go('downsell')}>See the lite plan</button>
169
+ * </Sheet>
170
+ * ))
171
+ * ```
172
+ *
173
+ * A modal can also host a whole `<Checkout offering="…" surface="element" />` for
174
+ * an inline downsell — the driver context is app-level, so checkout works inside
175
+ * modals as-is.
176
+ */
177
+ interface FailureModalProps {
178
+ error: CheckoutError;
179
+ /** The product whose charge failed (absent only for product-less failures). */
180
+ product?: string;
181
+ /** Navigate the flow to a page key (modals can't call useNavigation themselves). */
182
+ go: (pageKey: string) => void;
183
+ /**
184
+ * Re-attempt the SAME product. `retry()` re-runs an upsell off-session; for a
185
+ * purchase (which needs UI to re-collect) it opens the payment sheet.
186
+ * `retry('sheet')` re-collects on the SDK sheet; an inline surface
187
+ * (`element`/`express`/`embedded`) is COERCED to the sheet when the provider
188
+ * supports it — a retry has no page slot to mount into, so the sheet hosts the
189
+ * element. On sheet-less MoR providers (Paddle) inline retries run through the
190
+ * driver, whose provider checkout opens its own overlay. `retry('popup')` /
191
+ * `retry('redirect')` hand off to the provider. A failed retry routes through
192
+ * `onFailed` again (the modal reopens) — the loop is user-driven, never
193
+ * automatic. Hide the modal first (`useModal().hide()`) when retrying into a
194
+ * surface that opens its own overlay.
195
+ */
196
+ retry: (surface?: CheckoutSurface) => void;
197
+ }
198
+ /**
199
+ * Declarative failure recovery — route each failure **category** to what happens
200
+ * next, instead of hand-wiring `onError`:
201
+ *
202
+ * ```tsx
203
+ * <Checkout offering="monthly" surface="element" onFailed={{
204
+ * prepaid_card: { go: 'real-card' },
205
+ * insufficient_funds: { modal: DownsellModal },
206
+ * default: { modal: TryAgainModal },
207
+ * }} />
208
+ * ```
209
+ *
210
+ * Resolution: exact category → `default` → nothing (the error state + `checkout.
211
+ * failed` event stand, as today). Two built-in judgments:
212
+ * - `canceled` only routes when mapped EXPLICITLY — a deliberately dismissed
213
+ * sheet/popup is not a failure to recover from, so `default` never catches it;
214
+ * - a `{ fallback }` recovery runs at most once per attempt chain (its own
215
+ * failure routes through the non-fallback routes), so declines can't loop.
216
+ *
217
+ * `checkout.failed` fires with category + declineCode on every failure regardless
218
+ * of routing — recovery pages are measurable, and being ordinary flow pages they
219
+ * are A/B-able with page experiments like anything else.
220
+ */
221
+ type OnFailedMap = Partial<Record<CheckoutErrorCategory | 'default', FailureRoute>>;
222
+ interface UseCheckoutOptions {
223
+ intent?: CheckoutIntent;
224
+ /** For `upsell` intent — which kind of off-session charge (default `subscription`). */
225
+ kind?: UpsellKind;
226
+ /** Advance to the next page after a successful charge (default: true). */
227
+ advanceOnSuccess?: boolean;
228
+ onSuccess?: (result: CheckoutResult) => void;
229
+ onError?: (error: CheckoutError) => void;
230
+ /** Declarative failure recovery — see {@link OnFailedMap}. Runs AFTER `onError`. */
231
+ onFailed?: OnFailedMap;
232
+ /**
233
+ * Try this surface ON-SESSION once on any first failure, before `onFailed`
234
+ * routing — the generalized `<Upsell fallback>`: the off-session (or first)
235
+ * charge fails, the surface re-collects a card, the retry's failure then goes
236
+ * through `onFailed`. Never re-triggered by its own failure; never triggered
237
+ * by a user-`canceled` checkout.
238
+ */
239
+ recoverySurface?: CheckoutSurface;
240
+ }
241
+ interface CheckoutHandle {
242
+ /** Start the charge for a product; resolves with the result (no surface = off-session). */
243
+ open: (product: string, surface?: CheckoutSurface) => Promise<CheckoutResult>;
244
+ status: CheckoutStatus;
245
+ /** The last failure, structured so you can route/recover by `category`. */
246
+ error: CheckoutError | null;
247
+ isLoading: boolean;
248
+ reset: () => void;
249
+ /** Build the callbacks an inline surface reports through (used by `<Checkout>`). */
250
+ callbacksFor: (product?: string, surface?: CheckoutSurface) => CheckoutCallbacks & {
251
+ onStart: () => void;
252
+ };
253
+ }
254
+ /**
255
+ * The headless checkout hook. Owns the state machine + analytics + post-charge
256
+ * navigation; delegates the charge to the injected {@link CheckoutDriver}.
257
+ */
258
+ declare function useCheckout(opts?: UseCheckoutOptions): CheckoutHandle;
259
+ interface CheckoutProps extends UseCheckoutOptions {
260
+ /** Logical offering slot (the author's bound selection). */
261
+ offering: string;
262
+ /**
263
+ * Presentation preference. A concrete {@link CheckoutSurface} pins it; `'auto'` lets the platform
264
+ * pick the best surface for the store's provider and the buyer's device — wallet-first on mobile
265
+ * (Stripe → ExpressCheckoutElement, Paddle → its Express Checkout variant), inline card/embedded
266
+ * on desktop (see {@link resolveSurface}). NOTE: `surface` has no default (kept required) so pinned
267
+ * funnels don't change behavior this canary; `'auto'` is opt-in. New authoring (editor + AI) should
268
+ * emit `surface="auto"` going forward so wallet-first is a funnel intent, not a provider prop.
269
+ */
270
+ surface: SurfacePreference;
271
+ /** Render your own trigger element instead of the default button (trigger surfaces). */
272
+ asChild?: boolean;
273
+ /** Trigger label / your trigger element. */
274
+ children?: ReactNode;
275
+ className?: string;
276
+ }
277
+ /**
278
+ * The purchase primitive. Three presentation modes by the RESOLVED `surface`:
279
+ * - **inline** (`express`/`element`/`embedded`) — the driver renders the
280
+ * provider UI in place (keyed by product so a selection change remounts it);
281
+ * - **SDK sheet** (`sheet`) — a trigger opens the Appfunnel-owned {@link Sheet}
282
+ * and the driver renders the provider element inside it;
283
+ * - **provider-hosted** (`popup`/`redirect`) — a trigger hands off to the
284
+ * provider's own overlay/page.
285
+ *
286
+ * The `surface` prop is a {@link SurfacePreference}: a concrete surface (pinned) or `'auto'`, which
287
+ * {@link resolveSurface} maps to the best surface for the driver's provider + this device — ONCE per
288
+ * mount (frozen so the surface can't change mid-attempt and remount the provider element into a new
289
+ * charge). The resolved literal is what flows onto the wire; the server re-validates it.
290
+ */
291
+ declare function Checkout({ offering, surface: surfacePreference, asChild, children, className, ...options }: CheckoutProps): ReactNode;
292
+ interface UpsellProps extends Omit<UseCheckoutOptions, 'intent'> {
293
+ offering: string;
294
+ /**
295
+ * If the off-session charge fails, present this surface **on-session** to
296
+ * re-collect a card and charge again — e.g. `fallback="sheet"` opens the
297
+ * payment sheet, `fallback="popup"` the provider overlay. Omit to just report
298
+ * the failure (via `onError` / the `checkout.failed` event).
299
+ */
300
+ fallback?: CheckoutSurface;
301
+ asChild?: boolean;
302
+ children?: ReactNode;
303
+ className?: string;
304
+ }
305
+ /**
306
+ * The upsell primitive — an off-session, one-tap charge to the saved method (no
307
+ * surface, no card re-entry). On failure, optionally `fallback` to an on-session
308
+ * surface to re-collect and retry — `fallback` IS the hook's `recoverySurface`,
309
+ * so the retry's own failure routes through `onFailed` (e.g. prepaid_card → a
310
+ * recapture page) instead of dead-ending.
311
+ */
312
+ declare function Upsell({ offering, fallback, asChild, children, className, ...options }: UpsellProps): ReactNode;
313
+ /**
314
+ * The dangling settle leg of the `redirect` surface. The server appends
315
+ * `af_checkout={correlationId}` to hosted-checkout success URLs; when the buyer
316
+ * lands back in the funnel this component detects the param, has the server
317
+ * VERIFY the attempt (`driver.resume` → `POST /complete`, bounded re-poll while
318
+ * `pending`), and resolves it into the normal machinery:
319
+ *
320
+ * - `settled` → `purchase.complete` fires with the settlement's server-minted
321
+ * `eventId` (pixel↔CAPI dedup), the flow advances, and the param is stripped
322
+ * (`history.replaceState`) so a reload can't re-run the leg;
323
+ * - `failed` → the normal failure path (`checkout.failed` + status `error`;
324
+ * any `onFailed` maps passed as props apply — product-less, so only
325
+ * `go`/handler routes can act).
326
+ *
327
+ * SSR-safe (effect-only) and runs once per correlationId per page load. Mounted
328
+ * automatically by `createFunnelTree` inside the nav context; needs a driver
329
+ * with `resume` (both platform drivers ship it; the mock resolves its result).
330
+ */
331
+ declare function CheckoutResume(options?: UseCheckoutOptions): ReactNode;
332
+
333
+ export { type CheckoutDriverContext as C, type FailureModalProps as F, type MockDriverOptions as M, type OnFailedMap as O, Upsell as U, type CheckoutDriver as a, Checkout as b, type CheckoutCallbacks as c, type CheckoutHandle as d, type CheckoutProps as e, type CheckoutRequest as f, type CheckoutResult as g, CheckoutResume as h, type CheckoutStatus as i, type FailureRoute as j, type UpsellProps as k, type UseCheckoutOptions as l, createMockDriver as m, driverWithEmail as n, useCheckout as u };