@appfunnel-dev/sdk 2.0.0-canary.0 → 2.0.0-canary.2

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 (43) hide show
  1. package/dist/capabilities-7_hy5f5G.d.cts +114 -0
  2. package/dist/capabilities-7_hy5f5G.d.ts +114 -0
  3. package/dist/checkout-CZmEvWfC.d.cts +317 -0
  4. package/dist/checkout-DiQvRT5q.d.ts +317 -0
  5. package/dist/chunk-7UC5VXOR.js +446 -0
  6. package/dist/chunk-7UC5VXOR.js.map +1 -0
  7. package/dist/chunk-LJYLGLFS.cjs +153 -0
  8. package/dist/chunk-LJYLGLFS.cjs.map +1 -0
  9. package/dist/chunk-UIR6TGEW.js +97 -0
  10. package/dist/chunk-UIR6TGEW.js.map +1 -0
  11. package/dist/chunk-VQOD2Z6Q.cjs +104 -0
  12. package/dist/chunk-VQOD2Z6Q.cjs.map +1 -0
  13. package/dist/chunk-YY375F2B.js +140 -0
  14. package/dist/chunk-YY375F2B.js.map +1 -0
  15. package/dist/chunk-Z3TWO2PW.cjs +475 -0
  16. package/dist/chunk-Z3TWO2PW.cjs.map +1 -0
  17. package/dist/driver-paddle.cjs +814 -0
  18. package/dist/driver-paddle.cjs.map +1 -0
  19. package/dist/driver-paddle.d.cts +10 -0
  20. package/dist/driver-paddle.d.ts +10 -0
  21. package/dist/driver-paddle.js +811 -0
  22. package/dist/driver-paddle.js.map +1 -0
  23. package/dist/driver-stripe.cjs +2253 -0
  24. package/dist/driver-stripe.cjs.map +1 -0
  25. package/dist/driver-stripe.d.cts +8 -0
  26. package/dist/driver-stripe.d.ts +8 -0
  27. package/dist/driver-stripe.js +2247 -0
  28. package/dist/driver-stripe.js.map +1 -0
  29. package/dist/index.cjs +1953 -813
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +183 -933
  32. package/dist/index.d.ts +183 -933
  33. package/dist/index.js +1674 -655
  34. package/dist/index.js.map +1 -1
  35. package/dist/manifest-DQThneiG.d.cts +777 -0
  36. package/dist/manifest-DQThneiG.d.ts +777 -0
  37. package/dist/manifest-entry.cjs +203 -0
  38. package/dist/manifest-entry.cjs.map +1 -0
  39. package/dist/manifest-entry.d.cts +184 -0
  40. package/dist/manifest-entry.d.ts +184 -0
  41. package/dist/manifest-entry.js +98 -0
  42. package/dist/manifest-entry.js.map +1 -0
  43. package/package.json +37 -4
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The provider capability matrix + checkout error taxonomy — the PURE half of the
3
+ * checkout seam, extracted from checkout.tsx so it ships through the **manifest
4
+ * entry** (no React): the API's publish validation, the editor, AI authoring, and
5
+ * local dev all run the SAME checks the runtime primitives use (phase-3 §3.1b —
6
+ * "the SDK ships this as data"). The React half (driver seam, `useCheckout`,
7
+ * `<Checkout>`/`<Upsell>`) stays in ./checkout, which re-exports everything here.
8
+ */
9
+ type CheckoutSurface = 'express' | 'element' | 'embedded' | 'sheet' | 'popup' | 'redirect';
10
+ type CheckoutProvider = 'stripe' | 'paddle' | 'whop' | 'primer' | 'solidgate';
11
+ type CheckoutIntent = 'purchase' | 'upsell';
12
+ /** Surfaces that render inline (vs. those that open on click). */
13
+ declare const INLINE_SURFACES: ReadonlySet<CheckoutSurface>;
14
+ declare function isInlineSurface(surface: CheckoutSurface): boolean;
15
+ /**
16
+ * The kind of off-session upsell (researched against Funnelfox's changelog, which
17
+ * ships these as distinct features). The discriminating real-world constraint is
18
+ * **stacking a 2nd concurrent subscription** — Paddle can't, SolidGate/Stripe can.
19
+ */
20
+ type UpsellKind = 'subscription' | 'one-time' | 'upgrade';
21
+ /** Reliability of an off-session upsell charge AFTER a purchase on a surface. */
22
+ type OffSessionReliability = 'reliable' | 'conditional' | 'none';
23
+ interface SurfaceCapability {
24
+ purchase: boolean;
25
+ /** Apple/Google Pay available on this surface. */
26
+ wallets: boolean;
27
+ /** Reliability of an off-session upsell after a purchase on this surface. */
28
+ offSession: OffSessionReliability;
29
+ }
30
+ interface ProviderProfile {
31
+ /** Merchant-of-Record (handles tax/compliance) vs. a PSP you settle through. */
32
+ isMoR: boolean;
33
+ /** Upsell kinds this provider can charge off-session (one-click, no re-entry). */
34
+ offSessionUpsells: UpsellKind[];
35
+ /** Who charges the saved method off-session: you (PSP) or the provider (MoR/managed). */
36
+ tokenModel: 'merchant' | 'provider';
37
+ /** Routes across multiple processors (Primer; SolidGate also offers this) vs. a single acquirer. */
38
+ orchestrator: boolean;
39
+ /**
40
+ * Driver tier (Wave-4 decision, Simon 2026-07-02): `full` = a real driver ships
41
+ * (Stripe, Paddle); `planned` = profile only — authoring/publish validate against
42
+ * it, but no charge path exists yet (Whop, Primer, SolidGate). A new provider
43
+ * becomes buildable by writing a driver; its profile already gates what authors
44
+ * can express, so funnels written today stay valid when the driver lands.
45
+ */
46
+ status: 'full' | 'planned';
47
+ /** Supported surfaces → per-surface capabilities. **Absent = not supported.** */
48
+ surfaces: Partial<Record<CheckoutSurface, SurfaceCapability>>;
49
+ }
50
+ /**
51
+ * Provider capabilities (researched 2026-06-23 against provider docs + Funnelfox;
52
+ * trial mechanics re-verified 2026-07-02, doc 09 §4.4).
53
+ *
54
+ * Three real-world corrections to the naive model:
55
+ * 1. **Off-session upsell support is per-provider by *kind*** (`offSessionUpsells`):
56
+ * Paddle can't stack a 2nd concurrent subscription (upgrade + one-time only);
57
+ * Stripe/SolidGate/Whop/Primer can do all kinds.
58
+ * 2. **Off-session *reliability* varies by the paywall surface** (`SurfaceCapability.
59
+ * offSession`): Stripe **managed** checkout (`embedded`/`redirect`) upsells as a
60
+ * direct charge against Stripe's vault → `conditional` (may decline); Elements
61
+ * surfaces are `reliable`. Primer is `conditional` everywhere (PSP-dependent).
62
+ * 3. **Surface availability is a hard constraint.** MoR/managed providers can't
63
+ * expose a raw card field to your own UI: Paddle has no `express`/`element`/your
64
+ * `sheet`; Whop has no raw `element`/`express`; Primer has no hosted `redirect`;
65
+ * Stripe/SolidGate have no `popup`. A missing entry means "not possible."
66
+ */
67
+ declare const PROVIDER_PROFILES: Record<CheckoutProvider, ProviderProfile>;
68
+ interface ValidationResult {
69
+ ok: boolean;
70
+ reason?: string;
71
+ /** Non-fatal caveat (e.g. Paddle upgrade-only). */
72
+ note?: string;
73
+ }
74
+ /** Surfaces a provider supports, for the editor/AI to steer authors. */
75
+ declare function surfacesFor(provider: CheckoutProvider): CheckoutSurface[];
76
+ /** Whether a provider is a Merchant of Record (it owns tax/compliance). */
77
+ declare function isMerchantOfRecord(provider: CheckoutProvider): boolean;
78
+ /** Whether a provider routes across multiple processors (Primer, SolidGate). */
79
+ declare function isOrchestrator(provider: CheckoutProvider): boolean;
80
+ /**
81
+ * Validate a (provider, surface) for a **purchase**: the surface must exist for
82
+ * the provider and accept a payment. Surface availability is the real constraint
83
+ * (e.g. Paddle has no `element`).
84
+ */
85
+ declare function validateCheckout(provider: CheckoutProvider, surface: CheckoutSurface): ValidationResult;
86
+ /**
87
+ * Validate an **off-session upsell** of `kind` after a purchase made on
88
+ * `paywallSurface`. Combines provider support for the kind (Paddle can't stack a
89
+ * 2nd subscription) with the paywall surface's off-session reliability (Stripe
90
+ * managed checkout is `conditional` — returned as a `note`, not a hard block).
91
+ */
92
+ declare function validateUpsell(provider: CheckoutProvider, paywallSurface: CheckoutSurface, kind?: UpsellKind): ValidationResult;
93
+ /**
94
+ * Normalized failure category — the driver/platform maps each provider's decline
95
+ * codes into one of these so funnels can route/recover by reason. `authentication_
96
+ * required` (3DS/SCA) and `requires_payment_method` are the cases an **off-session**
97
+ * upsell can't satisfy → recover on-session (re-collect a card).
98
+ */
99
+ type CheckoutErrorCategory = 'card_declined' | 'insufficient_funds' | 'expired_card' | 'prepaid_card' | 'authentication_required' | 'requires_payment_method' | 'processing_error' | 'canceled' | 'unknown';
100
+ interface CheckoutError {
101
+ category: CheckoutErrorCategory;
102
+ message: string;
103
+ /** Raw provider decline code, if any (e.g. Stripe `card_declined`/`insufficient_funds`). */
104
+ declineCode?: string;
105
+ /** Whether re-attempting (e.g. an on-session re-collection) may succeed. */
106
+ retryable: boolean;
107
+ }
108
+ /** Build a {@link CheckoutError} (driver/test helper). */
109
+ declare function checkoutError(category: CheckoutErrorCategory, message: string, extra?: {
110
+ declineCode?: string;
111
+ retryable?: boolean;
112
+ }): CheckoutError;
113
+
114
+ export { type CheckoutError as C, INLINE_SURFACES as I, type OffSessionReliability as O, PROVIDER_PROFILES as P, type SurfaceCapability as S, type UpsellKind as U, type ValidationResult as V, type CheckoutErrorCategory as a, type CheckoutIntent as b, type CheckoutProvider as c, type CheckoutSurface as d, type ProviderProfile as e, checkoutError as f, isMerchantOfRecord as g, isOrchestrator as h, isInlineSurface as i, validateUpsell as j, surfacesFor as s, validateCheckout as v };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The provider capability matrix + checkout error taxonomy — the PURE half of the
3
+ * checkout seam, extracted from checkout.tsx so it ships through the **manifest
4
+ * entry** (no React): the API's publish validation, the editor, AI authoring, and
5
+ * local dev all run the SAME checks the runtime primitives use (phase-3 §3.1b —
6
+ * "the SDK ships this as data"). The React half (driver seam, `useCheckout`,
7
+ * `<Checkout>`/`<Upsell>`) stays in ./checkout, which re-exports everything here.
8
+ */
9
+ type CheckoutSurface = 'express' | 'element' | 'embedded' | 'sheet' | 'popup' | 'redirect';
10
+ type CheckoutProvider = 'stripe' | 'paddle' | 'whop' | 'primer' | 'solidgate';
11
+ type CheckoutIntent = 'purchase' | 'upsell';
12
+ /** Surfaces that render inline (vs. those that open on click). */
13
+ declare const INLINE_SURFACES: ReadonlySet<CheckoutSurface>;
14
+ declare function isInlineSurface(surface: CheckoutSurface): boolean;
15
+ /**
16
+ * The kind of off-session upsell (researched against Funnelfox's changelog, which
17
+ * ships these as distinct features). The discriminating real-world constraint is
18
+ * **stacking a 2nd concurrent subscription** — Paddle can't, SolidGate/Stripe can.
19
+ */
20
+ type UpsellKind = 'subscription' | 'one-time' | 'upgrade';
21
+ /** Reliability of an off-session upsell charge AFTER a purchase on a surface. */
22
+ type OffSessionReliability = 'reliable' | 'conditional' | 'none';
23
+ interface SurfaceCapability {
24
+ purchase: boolean;
25
+ /** Apple/Google Pay available on this surface. */
26
+ wallets: boolean;
27
+ /** Reliability of an off-session upsell after a purchase on this surface. */
28
+ offSession: OffSessionReliability;
29
+ }
30
+ interface ProviderProfile {
31
+ /** Merchant-of-Record (handles tax/compliance) vs. a PSP you settle through. */
32
+ isMoR: boolean;
33
+ /** Upsell kinds this provider can charge off-session (one-click, no re-entry). */
34
+ offSessionUpsells: UpsellKind[];
35
+ /** Who charges the saved method off-session: you (PSP) or the provider (MoR/managed). */
36
+ tokenModel: 'merchant' | 'provider';
37
+ /** Routes across multiple processors (Primer; SolidGate also offers this) vs. a single acquirer. */
38
+ orchestrator: boolean;
39
+ /**
40
+ * Driver tier (Wave-4 decision, Simon 2026-07-02): `full` = a real driver ships
41
+ * (Stripe, Paddle); `planned` = profile only — authoring/publish validate against
42
+ * it, but no charge path exists yet (Whop, Primer, SolidGate). A new provider
43
+ * becomes buildable by writing a driver; its profile already gates what authors
44
+ * can express, so funnels written today stay valid when the driver lands.
45
+ */
46
+ status: 'full' | 'planned';
47
+ /** Supported surfaces → per-surface capabilities. **Absent = not supported.** */
48
+ surfaces: Partial<Record<CheckoutSurface, SurfaceCapability>>;
49
+ }
50
+ /**
51
+ * Provider capabilities (researched 2026-06-23 against provider docs + Funnelfox;
52
+ * trial mechanics re-verified 2026-07-02, doc 09 §4.4).
53
+ *
54
+ * Three real-world corrections to the naive model:
55
+ * 1. **Off-session upsell support is per-provider by *kind*** (`offSessionUpsells`):
56
+ * Paddle can't stack a 2nd concurrent subscription (upgrade + one-time only);
57
+ * Stripe/SolidGate/Whop/Primer can do all kinds.
58
+ * 2. **Off-session *reliability* varies by the paywall surface** (`SurfaceCapability.
59
+ * offSession`): Stripe **managed** checkout (`embedded`/`redirect`) upsells as a
60
+ * direct charge against Stripe's vault → `conditional` (may decline); Elements
61
+ * surfaces are `reliable`. Primer is `conditional` everywhere (PSP-dependent).
62
+ * 3. **Surface availability is a hard constraint.** MoR/managed providers can't
63
+ * expose a raw card field to your own UI: Paddle has no `express`/`element`/your
64
+ * `sheet`; Whop has no raw `element`/`express`; Primer has no hosted `redirect`;
65
+ * Stripe/SolidGate have no `popup`. A missing entry means "not possible."
66
+ */
67
+ declare const PROVIDER_PROFILES: Record<CheckoutProvider, ProviderProfile>;
68
+ interface ValidationResult {
69
+ ok: boolean;
70
+ reason?: string;
71
+ /** Non-fatal caveat (e.g. Paddle upgrade-only). */
72
+ note?: string;
73
+ }
74
+ /** Surfaces a provider supports, for the editor/AI to steer authors. */
75
+ declare function surfacesFor(provider: CheckoutProvider): CheckoutSurface[];
76
+ /** Whether a provider is a Merchant of Record (it owns tax/compliance). */
77
+ declare function isMerchantOfRecord(provider: CheckoutProvider): boolean;
78
+ /** Whether a provider routes across multiple processors (Primer, SolidGate). */
79
+ declare function isOrchestrator(provider: CheckoutProvider): boolean;
80
+ /**
81
+ * Validate a (provider, surface) for a **purchase**: the surface must exist for
82
+ * the provider and accept a payment. Surface availability is the real constraint
83
+ * (e.g. Paddle has no `element`).
84
+ */
85
+ declare function validateCheckout(provider: CheckoutProvider, surface: CheckoutSurface): ValidationResult;
86
+ /**
87
+ * Validate an **off-session upsell** of `kind` after a purchase made on
88
+ * `paywallSurface`. Combines provider support for the kind (Paddle can't stack a
89
+ * 2nd subscription) with the paywall surface's off-session reliability (Stripe
90
+ * managed checkout is `conditional` — returned as a `note`, not a hard block).
91
+ */
92
+ declare function validateUpsell(provider: CheckoutProvider, paywallSurface: CheckoutSurface, kind?: UpsellKind): ValidationResult;
93
+ /**
94
+ * Normalized failure category — the driver/platform maps each provider's decline
95
+ * codes into one of these so funnels can route/recover by reason. `authentication_
96
+ * required` (3DS/SCA) and `requires_payment_method` are the cases an **off-session**
97
+ * upsell can't satisfy → recover on-session (re-collect a card).
98
+ */
99
+ type CheckoutErrorCategory = 'card_declined' | 'insufficient_funds' | 'expired_card' | 'prepaid_card' | 'authentication_required' | 'requires_payment_method' | 'processing_error' | 'canceled' | 'unknown';
100
+ interface CheckoutError {
101
+ category: CheckoutErrorCategory;
102
+ message: string;
103
+ /** Raw provider decline code, if any (e.g. Stripe `card_declined`/`insufficient_funds`). */
104
+ declineCode?: string;
105
+ /** Whether re-attempting (e.g. an on-session re-collection) may succeed. */
106
+ retryable: boolean;
107
+ }
108
+ /** Build a {@link CheckoutError} (driver/test helper). */
109
+ declare function checkoutError(category: CheckoutErrorCategory, message: string, extra?: {
110
+ declineCode?: string;
111
+ retryable?: boolean;
112
+ }): CheckoutError;
113
+
114
+ export { type CheckoutError as C, INLINE_SURFACES as I, type OffSessionReliability as O, PROVIDER_PROFILES as P, type SurfaceCapability as S, type UpsellKind as U, type ValidationResult as V, type CheckoutErrorCategory as a, type CheckoutIntent as b, type CheckoutProvider as c, type CheckoutSurface as d, type ProviderProfile as e, checkoutError as f, isMerchantOfRecord as g, isOrchestrator as h, isInlineSurface as i, validateUpsell as j, surfacesFor as s, validateCheckout as v };
@@ -0,0 +1,317 @@
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 } from './capabilities-7_hy5f5G.cjs';
3
+
4
+ /**
5
+ * v2 commerce primitives — the two intents from phase-3, made trivial.
6
+ *
7
+ * - `<Checkout product 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 product>` — 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 product 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
+ product: string;
29
+ intent: CheckoutIntent;
30
+ surface?: CheckoutSurface;
31
+ /** For `upsell` intent — which kind of off-session charge (default `subscription`). */
32
+ kind?: UpsellKind;
33
+ /**
34
+ * Buyer email at attempt time, read from the funnel's `user.email` namespace value
35
+ * (injected by `FunnelProvider` via {@link driverWithEmail} — no author wiring).
36
+ * Drivers forward it into the wire `CheckoutSessionRequest.email` (Stripe Customer
37
+ * creation / receipts / Paddle prefill).
38
+ */
39
+ email?: string;
40
+ }
41
+ interface CheckoutResult {
42
+ ok: boolean;
43
+ error?: CheckoutError;
44
+ /** Charged amount in minor units (for the purchase event). */
45
+ amountMinor?: number;
46
+ currency?: string;
47
+ /**
48
+ * The **server-minted** dedup id for the purchase event — the platform creates it
49
+ * when the charge settles and fires CAPI with it; the SDK emits `purchase.complete`
50
+ * with the same id so the browser pixel dedupes against the server. (Server is the
51
+ * source of truth for payment events; the SDK doesn't mint these.)
52
+ */
53
+ eventId?: string;
54
+ }
55
+ interface CheckoutCallbacks {
56
+ onSuccess: (result: CheckoutResult) => void;
57
+ onError: (error: CheckoutError) => void;
58
+ /**
59
+ * The buyer SUBMITTED an inline surface (Stripe confirm click / wallet
60
+ * authorization; Paddle inline checkout opening). This is when `checkout.start`
61
+ * fires and the status machine enters `loading` for inline attempts — trigger
62
+ * surfaces fire it at click time instead. It also marks fresh user intent, so
63
+ * the once-per-chain recovery guard resets. Optional: drivers that can't
64
+ * observe submit simply never call it, and the SHEET host deliberately doesn't
65
+ * forward it (the sheet's own open already began the attempt — re-firing would
66
+ * double-count starts and un-arm the recovery guard mid-chain).
67
+ */
68
+ onStart?: () => void;
69
+ /**
70
+ * The inline surface reports a valid payment method was entered (e.g. Stripe's
71
+ * Payment Element became `complete`), before the charge. Drives the
72
+ * `checkout.payment_added` analytics step; optional — drivers that can't observe
73
+ * it (or off-session charges) simply never call it.
74
+ */
75
+ onPaymentAdded?: () => void;
76
+ }
77
+ /**
78
+ * What a driver factory (`stripeCheckoutDriver` et al. from the
79
+ * `@appfunnel-dev/sdk/driver-*` entries) receives to construct a
80
+ * {@link CheckoutDriver}. Derived by `createFunnelTree` from the SAME
81
+ * renderer-injected mount data the platform tracker uses (tracking config +
82
+ * identity), so checkout wire calls land on the exact campaign/funnel/session
83
+ * the analytics do — there is no second config channel to drift.
84
+ *
85
+ * Serializable on purpose, and factories must be **construction-safe on the
86
+ * server too**: a factory only closes over this ctx (no provider SDK, no
87
+ * `window`) — all provider work belongs in effects/event handlers, so SSR can
88
+ * render the driver's stable inline placeholder and hydration matches.
89
+ */
90
+ interface CheckoutDriverContext {
91
+ /** Public API origin, e.g. `https://api.appfunnel.net` (no trailing slash). */
92
+ apiBase: string;
93
+ /** Campaign DB id (the checkout route key, same as the ingestion route key). */
94
+ campaignId: string;
95
+ /** WebFunnel DB id (NOT the funnel.ts config id). */
96
+ funnelId: string;
97
+ /** Render-time mode — the server resolves the product binding + store credentials by it. */
98
+ mode: 'test' | 'live';
99
+ /** Funnel session id when one exists (analytics + customer continuity). */
100
+ sessionId?: string | null;
101
+ /** Signed-cookie-verified bare visitor id, when known. */
102
+ visitorId?: string | null;
103
+ }
104
+ /**
105
+ * The provider seam. The platform injects the real one (Stripe/Paddle/Whop/…);
106
+ * tests use {@link createMockDriver}.
107
+ *
108
+ * Most providers ship their **own React SDK** (`@stripe/react-stripe-js`,
109
+ * `@paddle/paddle-js`, Primer's React components, …). A driver's `renderInline`
110
+ * returns exactly that: the provider's React element mounted in our chrome (inline,
111
+ * or inside the `<Sheet>`). Those provider SDKs live in the **driver/platform**
112
+ * package, not our core funnel SDK — which is why the driver is injected: the core
113
+ * stays lean and provider-agnostic, and you only pull the SDK for the provider you use.
114
+ */
115
+ interface CheckoutDriver {
116
+ provider: CheckoutProvider;
117
+ /** Trigger a charge (trigger surfaces + programmatic). Resolves when settled. */
118
+ start(req: CheckoutRequest): Promise<CheckoutResult>;
119
+ /** Render an inline surface (e.g. the provider's React payment element); report via callbacks. */
120
+ renderInline?(req: CheckoutRequest, cb: CheckoutCallbacks): ReactNode;
121
+ /**
122
+ * Verify an EXISTING attempt with the server (`POST /complete`, bounded re-poll on
123
+ * `pending`) — the redirect-return resume leg: hosted-checkout success URLs carry
124
+ * `?af_checkout={correlationId}` back into the funnel, and {@link CheckoutResume}
125
+ * hands the id here so the settlement (server-verified) resolves into the normal
126
+ * success/failure paths. Never charges; only verifies.
127
+ */
128
+ resume?(correlationId: string): Promise<CheckoutResult>;
129
+ }
130
+ interface MockDriverOptions {
131
+ /** The result a successful charge resolves with. */
132
+ result?: CheckoutResult;
133
+ /** Make **off-session** charges (no surface) fail — to exercise the upsell fallback. */
134
+ failOffSession?: CheckoutError | boolean;
135
+ }
136
+ /** A console/no-op driver — tests, preview, local dev. Succeeds unless configured to fail. */
137
+ declare function createMockDriver(provider?: CheckoutProvider, options?: MockDriverOptions): CheckoutDriver;
138
+ /**
139
+ * Wrap a driver so every attempt carries the buyer email the funnel has collected
140
+ * (`user.email`), read at ATTEMPT time — `start()` for trigger surfaces, the
141
+ * `renderInline` request for mounted surfaces. Applied by `FunnelProvider` around
142
+ * the injected driver, so `useCheckout`, `<Checkout>` inline branches and the
143
+ * checkout sheet all forward it without wiring; drivers pass it into the wire's
144
+ * `CheckoutSessionRequest.email`. An explicit `req.email` always wins.
145
+ */
146
+ declare function driverWithEmail(driver: CheckoutDriver, getEmail: () => string | undefined): CheckoutDriver;
147
+ /** One declarative recovery route for a failure category. */
148
+ type FailureRoute = {
149
+ go: string;
150
+ } | {
151
+ modal: string | ComponentType<any>;
152
+ } | {
153
+ fallback: CheckoutSurface;
154
+ } | ((error: CheckoutError) => void);
155
+ /**
156
+ * What a `{ modal }` failure route hands the modal — the error AND the actions,
157
+ * because registry modals mount outside the flow's nav context and a recovery
158
+ * modal that can't act is just an error toast. Type your modal with this:
159
+ *
160
+ * ```tsx
161
+ * const Declined = defineModal<FailureModalProps>(({ error, go, retry }) => (
162
+ * <Sheet>
163
+ * <p>{error.category === 'prepaid_card' ? 'Prepaid cards can't start a trial.' : 'Payment didn't go through.'}</p>
164
+ * <button onClick={() => retry('sheet')}>Try another card</button>
165
+ * <button onClick={() => go('downsell')}>See the lite plan</button>
166
+ * </Sheet>
167
+ * ))
168
+ * ```
169
+ *
170
+ * A modal can also host a whole `<Checkout product="…" surface="element" />` for
171
+ * an inline downsell — the driver context is app-level, so checkout works inside
172
+ * modals as-is.
173
+ */
174
+ interface FailureModalProps {
175
+ error: CheckoutError;
176
+ /** The product whose charge failed (absent only for product-less failures). */
177
+ product?: string;
178
+ /** Navigate the flow to a page key (modals can't call useNavigation themselves). */
179
+ go: (pageKey: string) => void;
180
+ /**
181
+ * Re-attempt the SAME product. `retry()` re-runs an upsell off-session; for a
182
+ * purchase (which needs UI to re-collect) it opens the payment sheet.
183
+ * `retry('sheet')` re-collects on the SDK sheet; an inline surface
184
+ * (`element`/`express`/`embedded`) is COERCED to the sheet when the provider
185
+ * supports it — a retry has no page slot to mount into, so the sheet hosts the
186
+ * element. On sheet-less MoR providers (Paddle) inline retries run through the
187
+ * driver, whose provider checkout opens its own overlay. `retry('popup')` /
188
+ * `retry('redirect')` hand off to the provider. A failed retry routes through
189
+ * `onFailed` again (the modal reopens) — the loop is user-driven, never
190
+ * automatic. Hide the modal first (`useModal().hide()`) when retrying into a
191
+ * surface that opens its own overlay.
192
+ */
193
+ retry: (surface?: CheckoutSurface) => void;
194
+ }
195
+ /**
196
+ * Declarative failure recovery — route each failure **category** to what happens
197
+ * next, instead of hand-wiring `onError`:
198
+ *
199
+ * ```tsx
200
+ * <Checkout product="monthly" surface="element" onFailed={{
201
+ * prepaid_card: { go: 'real-card' },
202
+ * insufficient_funds: { modal: DownsellModal },
203
+ * default: { modal: TryAgainModal },
204
+ * }} />
205
+ * ```
206
+ *
207
+ * Resolution: exact category → `default` → nothing (the error state + `checkout.
208
+ * failed` event stand, as today). Two built-in judgments:
209
+ * - `canceled` only routes when mapped EXPLICITLY — a deliberately dismissed
210
+ * sheet/popup is not a failure to recover from, so `default` never catches it;
211
+ * - a `{ fallback }` recovery runs at most once per attempt chain (its own
212
+ * failure routes through the non-fallback routes), so declines can't loop.
213
+ *
214
+ * `checkout.failed` fires with category + declineCode on every failure regardless
215
+ * of routing — recovery pages are measurable, and being ordinary flow pages they
216
+ * are A/B-able with page experiments like anything else.
217
+ */
218
+ type OnFailedMap = Partial<Record<CheckoutErrorCategory | 'default', FailureRoute>>;
219
+ interface UseCheckoutOptions {
220
+ intent?: CheckoutIntent;
221
+ /** For `upsell` intent — which kind of off-session charge (default `subscription`). */
222
+ kind?: UpsellKind;
223
+ /** Advance to the next page after a successful charge (default: true). */
224
+ advanceOnSuccess?: boolean;
225
+ onSuccess?: (result: CheckoutResult) => void;
226
+ onError?: (error: CheckoutError) => void;
227
+ /** Declarative failure recovery — see {@link OnFailedMap}. Runs AFTER `onError`. */
228
+ onFailed?: OnFailedMap;
229
+ /**
230
+ * Try this surface ON-SESSION once on any first failure, before `onFailed`
231
+ * routing — the generalized `<Upsell fallback>`: the off-session (or first)
232
+ * charge fails, the surface re-collects a card, the retry's failure then goes
233
+ * through `onFailed`. Never re-triggered by its own failure; never triggered
234
+ * by a user-`canceled` checkout.
235
+ */
236
+ recoverySurface?: CheckoutSurface;
237
+ }
238
+ interface CheckoutHandle {
239
+ /** Start the charge for a product; resolves with the result (no surface = off-session). */
240
+ open: (product: string, surface?: CheckoutSurface) => Promise<CheckoutResult>;
241
+ status: CheckoutStatus;
242
+ /** The last failure, structured so you can route/recover by `category`. */
243
+ error: CheckoutError | null;
244
+ isLoading: boolean;
245
+ reset: () => void;
246
+ /** Build the callbacks an inline surface reports through (used by `<Checkout>`). */
247
+ callbacksFor: (product?: string, surface?: CheckoutSurface) => CheckoutCallbacks & {
248
+ onStart: () => void;
249
+ };
250
+ }
251
+ /**
252
+ * The headless checkout hook. Owns the state machine + analytics + post-charge
253
+ * navigation; delegates the charge to the injected {@link CheckoutDriver}.
254
+ */
255
+ declare function useCheckout(opts?: UseCheckoutOptions): CheckoutHandle;
256
+ interface CheckoutProps extends UseCheckoutOptions {
257
+ /** Logical product id (the author's bound selection). */
258
+ product: string;
259
+ surface: CheckoutSurface;
260
+ /** Render your own trigger element instead of the default button (trigger surfaces). */
261
+ asChild?: boolean;
262
+ /** Trigger label / your trigger element. */
263
+ children?: ReactNode;
264
+ className?: string;
265
+ }
266
+ /**
267
+ * The purchase primitive. Three presentation modes by `surface`:
268
+ * - **inline** (`express`/`element`/`embedded`) — the driver renders the
269
+ * provider UI in place (keyed by product so a selection change remounts it);
270
+ * - **SDK sheet** (`sheet`) — a trigger opens the AppFunnel-owned {@link Sheet}
271
+ * and the driver renders the provider element inside it;
272
+ * - **provider-hosted** (`popup`/`redirect`) — a trigger hands off to the
273
+ * provider's own overlay/page.
274
+ */
275
+ declare function Checkout({ product, surface, asChild, children, className, ...options }: CheckoutProps): ReactNode;
276
+ interface UpsellProps extends Omit<UseCheckoutOptions, 'intent'> {
277
+ product: string;
278
+ /**
279
+ * If the off-session charge fails, present this surface **on-session** to
280
+ * re-collect a card and charge again — e.g. `fallback="sheet"` opens the
281
+ * payment sheet, `fallback="popup"` the provider overlay. Omit to just report
282
+ * the failure (via `onError` / the `checkout.failed` event).
283
+ */
284
+ fallback?: CheckoutSurface;
285
+ asChild?: boolean;
286
+ children?: ReactNode;
287
+ className?: string;
288
+ }
289
+ /**
290
+ * The upsell primitive — an off-session, one-tap charge to the saved method (no
291
+ * surface, no card re-entry). On failure, optionally `fallback` to an on-session
292
+ * surface to re-collect and retry — `fallback` IS the hook's `recoverySurface`,
293
+ * so the retry's own failure routes through `onFailed` (e.g. prepaid_card → a
294
+ * recapture page) instead of dead-ending.
295
+ */
296
+ declare function Upsell({ product, fallback, asChild, children, className, ...options }: UpsellProps): ReactNode;
297
+ /**
298
+ * The dangling settle leg of the `redirect` surface. The server appends
299
+ * `af_checkout={correlationId}` to hosted-checkout success URLs; when the buyer
300
+ * lands back in the funnel this component detects the param, has the server
301
+ * VERIFY the attempt (`driver.resume` → `POST /complete`, bounded re-poll while
302
+ * `pending`), and resolves it into the normal machinery:
303
+ *
304
+ * - `settled` → `purchase.complete` fires with the settlement's server-minted
305
+ * `eventId` (pixel↔CAPI dedup), the flow advances, and the param is stripped
306
+ * (`history.replaceState`) so a reload can't re-run the leg;
307
+ * - `failed` → the normal failure path (`checkout.failed` + status `error`;
308
+ * any `onFailed` maps passed as props apply — product-less, so only
309
+ * `go`/handler routes can act).
310
+ *
311
+ * SSR-safe (effect-only) and runs once per correlationId per page load. Mounted
312
+ * automatically by `createFunnelTree` inside the nav context; needs a driver
313
+ * with `resume` (both platform drivers ship it; the mock resolves its result).
314
+ */
315
+ declare function CheckoutResume(options?: UseCheckoutOptions): ReactNode;
316
+
317
+ 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 };