@appfunnel-dev/sdk 2.0.0 → 2.0.1

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 (38) hide show
  1. package/CHANGELOG.md +41 -3
  2. package/dist/{checkout-BYcOfyg_.d.ts → checkout-B1K6ozwk.d.ts} +6 -0
  3. package/dist/{checkout-BM1T-u6_.d.cts → checkout-C4tFUEdF.d.cts} +6 -0
  4. package/dist/{chunk-MBTHQP4Z.cjs → chunk-DTSLJ24T.cjs} +11 -3
  5. package/dist/chunk-DTSLJ24T.cjs.map +1 -0
  6. package/dist/{chunk-NA7DRFOA.cjs → chunk-E7JXWUNV.cjs} +55 -17
  7. package/dist/chunk-E7JXWUNV.cjs.map +1 -0
  8. package/dist/{chunk-KVMNVX6J.js → chunk-USAECFWO.js} +11 -3
  9. package/dist/chunk-USAECFWO.js.map +1 -0
  10. package/dist/{chunk-PN7CRJAF.js → chunk-WCBVSZHJ.js} +55 -17
  11. package/dist/chunk-WCBVSZHJ.js.map +1 -0
  12. package/dist/driver-paddle.cjs +14 -14
  13. package/dist/driver-paddle.d.cts +1 -1
  14. package/dist/driver-paddle.d.ts +1 -1
  15. package/dist/driver-paddle.js +1 -1
  16. package/dist/driver-stripe.cjs +15 -15
  17. package/dist/driver-stripe.d.cts +1 -1
  18. package/dist/driver-stripe.d.ts +1 -1
  19. package/dist/driver-stripe.js +2 -2
  20. package/dist/index.cjs +78 -67
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +25 -6
  23. package/dist/index.d.ts +25 -6
  24. package/dist/index.js +13 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/{manifest-BajvtPWs.d.cts → manifest-Dp1oRvqp.d.cts} +23 -0
  27. package/dist/{manifest-BajvtPWs.d.ts → manifest-Dp1oRvqp.d.ts} +23 -0
  28. package/dist/manifest-entry.cjs +9 -1
  29. package/dist/manifest-entry.cjs.map +1 -1
  30. package/dist/manifest-entry.d.cts +15 -2
  31. package/dist/manifest-entry.d.ts +15 -2
  32. package/dist/manifest-entry.js +9 -1
  33. package/dist/manifest-entry.js.map +1 -1
  34. package/package.json +1 -1
  35. package/dist/chunk-KVMNVX6J.js.map +0 -1
  36. package/dist/chunk-MBTHQP4Z.cjs.map +0 -1
  37. package/dist/chunk-NA7DRFOA.cjs.map +0 -1
  38. package/dist/chunk-PN7CRJAF.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.1
4
+
5
+ A checkout that names no offering is now caught, in both places it can be caught.
6
+
7
+ ### A missing `offering` fails the build
8
+
9
+ `<Checkout>`, `<CheckoutButton>` and `<UpsellButton>` must name the offering slot they charge.
10
+ A tag that doesn't now fails the build, with the file and line:
11
+
12
+ ```
13
+ checkout with no `offering`:
14
+ pages/paywall.tsx:27 <Checkout>
15
+ ```
16
+
17
+ This closes a gap where such a tag was invisible end to end. It contributes no offering
18
+ reference, so the manifest's unknown/unassigned-slot validation never saw the slot and the
19
+ publish gate passed with nothing to check — while at runtime the charge resolved to nothing and
20
+ the page shipped with a dead paywall. A funnel build is a bundler transform, so types are
21
+ stripped rather than checked; nothing else in the deploy path was looking.
22
+
23
+ The shape that hits this most often is the pre-2.0 spelling `<Checkout product="plan">`, where
24
+ the offering arrives under the old prop name and `offering` is simply absent.
25
+
26
+ Tags whose offering cannot be read statically are unaffected: `offering={expr}` and a tag
27
+ carrying a spread (`<Checkout {...props} />`) both pass — the same relaxation the rest of the
28
+ page scan applies to anything it cannot enumerate.
29
+
30
+ ### A missing `offering` is refused at runtime
31
+
32
+ For the cases no static scan can see — an `offering={expr}` that evaluated to nothing, a spread
33
+ that did not carry one — the components now refuse instead of resolving nothing. The refusal
34
+ reports through the normal failure path (`checkout.failed`, a non-retryable `processing_error`,
35
+ any `onFailed` routes) and renders nothing chargeable. On `<UpsellButton>` the trigger itself
36
+ never renders: an upsell charges off-session on the first tap, so there must be nothing to tap.
37
+
3
38
  ## 2.0.0
4
39
 
5
40
  First stable release of the v2 SDK, and the first v2 release published to the `latest`
@@ -7,11 +42,14 @@ dist-tag. Everything from `2.0.0-canary.1` through `2.0.0-canary.36` is folded i
7
42
  release; the canary line is closed.
8
43
 
9
44
  **`npm install @appfunnel-dev/sdk` now resolves to 2.0.0, not 0.17.0.** v2 is a rewrite,
10
- not an upgrade — a 0.17 funnel will not build against it. Stay on 0.17 by pinning the
11
- exact version:
45
+ not an upgrade — a 0.17 funnel will not build against it.
46
+
47
+ **The 2.0.x line is the only thing on the registry.** Every 0.x release and the whole
48
+ `2.0.0-canary.*` line have been unpublished, so there is no version to stay on: a funnel
49
+ still pinned to one of them fails its next build at install (`ETARGET`). Move the pin:
12
50
 
13
51
  ```json
14
- { "dependencies": { "@appfunnel-dev/sdk": "0.17.0" } }
52
+ { "dependencies": { "@appfunnel-dev/sdk": "2.0.0" } }
15
53
  ```
16
54
 
17
55
  ### The v2 surface
@@ -222,6 +222,12 @@ interface CheckoutResult {
222
222
  * source of truth for payment events; the SDK doesn't mint these.)
223
223
  */
224
224
  eventId?: string;
225
+ /**
226
+ * Server-decided acquisition shape. A trial start reports `subscription.trial_started`
227
+ * (→ Meta/TikTok StartTrial), never `purchase.complete`/`subscription.created` — see
228
+ * {@link CheckoutSettlement.acquisition}.
229
+ */
230
+ acquisition?: 'direct' | 'free_trial' | 'paid_trial';
225
231
  }
226
232
  interface CheckoutCallbacks {
227
233
  onSuccess: (result: CheckoutResult) => void;
@@ -222,6 +222,12 @@ interface CheckoutResult {
222
222
  * source of truth for payment events; the SDK doesn't mint these.)
223
223
  */
224
224
  eventId?: string;
225
+ /**
226
+ * Server-decided acquisition shape. A trial start reports `subscription.trial_started`
227
+ * (→ Meta/TikTok StartTrial), never `purchase.complete`/`subscription.created` — see
228
+ * {@link CheckoutSettlement.acquisition}.
229
+ */
230
+ acquisition?: 'direct' | 'free_trial' | 'paid_trial';
225
231
  }
226
232
  interface CheckoutCallbacks {
227
233
  onSuccess: (result: CheckoutResult) => void;
@@ -74,7 +74,15 @@ function postComplete(ctx, correlationId, providerRef) {
74
74
  );
75
75
  }
76
76
  function settlementToResult(s) {
77
- return { ok: true, amountMinor: s.amountMinor, currency: s.currency, eventId: s.eventId };
77
+ return {
78
+ ok: true,
79
+ amountMinor: s.amountMinor,
80
+ currency: s.currency,
81
+ eventId: s.eventId,
82
+ // Carried through so the checkout hook can tell a trial start from a purchase — it decides
83
+ // which conversion event fires, and the browser has no other honest source for it.
84
+ acquisition: s.acquisition
85
+ };
78
86
  }
79
87
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
80
88
  async function completeWithPoll(ctx, correlationId, opts = {}) {
@@ -125,5 +133,5 @@ exports.postComplete = postComplete;
125
133
  exports.postSession = postSession;
126
134
  exports.settlementToResult = settlementToResult;
127
135
  exports.toCheckoutError = toCheckoutError;
128
- //# sourceMappingURL=chunk-MBTHQP4Z.cjs.map
129
- //# sourceMappingURL=chunk-MBTHQP4Z.cjs.map
136
+ //# sourceMappingURL=chunk-DTSLJ24T.cjs.map
137
+ //# sourceMappingURL=chunk-DTSLJ24T.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/drivers/wire.ts"],"names":["checkoutError"],"mappings":"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","file":"chunk-DTSLJ24T.cjs","sourcesContent":["/**\n * Shared wire plumbing for the checkout drivers — the CLIENT half of the v2\n * checkout contract (commerce/checkout-api.ts). Lives under drivers/ (not\n * commerce/) on purpose: it ships only inside the `driver-*` subpath entries,\n * so the core funnel entry never bundles a checkout fetch — the core SDK talks\n * to a {@link CheckoutDriver}, never to the wire.\n *\n * Only `import type` from commerce/checkout(.tsx) here — a runtime import would\n * drag the React checkout primitives into every driver chunk. The one runtime\n * dependency is the pure error taxonomy (capabilities.ts).\n */\nimport type {\n CheckoutCompleteRequest,\n CheckoutCompleteResponse,\n CheckoutSessionRequest,\n CheckoutSessionResponse,\n CheckoutSettlement,\n} from '../commerce/checkout-api'\nimport type { CheckoutDriverContext, CheckoutRequest, CheckoutResult } from '../commerce/checkout'\nimport { checkoutError, type CheckoutError } from '../commerce/capabilities'\n\n/**\n * The ONE place a driver's URL is decided — which is why adding the preview surface needed no\n * change in driver-stripe / driver-paddle, and needs none in whatever driver ships next. Two\n * surfaces, one wire contract:\n *\n * - campaign → `{apiBase}/campaign/{campaignId}/v2/checkout{path}`\n * - preview → `{apiBase}/v2/funnels/{funnelId}/preview-checkout{path}?buildId=…&t=…`\n *\n * The preview surface carries its auth in the query string (the build id + the HMAC token the\n * renderer already served the page with); the request BODY is byte-identical between the two, so\n * the server parses one schema either way.\n */\nfunction endpoint(ctx: CheckoutDriverContext, path = ''): string {\n const base = ctx.apiBase.replace(/\\/+$/, '')\n if (ctx.preview) {\n const auth = `?buildId=${encodeURIComponent(ctx.preview.buildId)}&t=${encodeURIComponent(ctx.preview.token)}`\n return `${base}/v2/funnels/${encodeURIComponent(ctx.funnelId)}/preview-checkout${path}${auth}`\n }\n return `${base}/campaign/${encodeURIComponent(ctx.campaignId ?? '')}/v2/checkout${path}`\n}\n\nasync function postJson<T>(url: string, body: unknown): Promise<T> {\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n // The contract speaks in `step` responses, not HTTP errors — a non-2xx means the\n // request never reached the checkout service properly (proxy, deploy, outage).\n if (!res.ok) throw checkoutError('processing_error', `checkout request failed (HTTP ${res.status})`)\n return (await res.json()) as T\n}\n\n/** Normalize any thrown value into the taxonomy (network drops → `processing_error`). */\nexport function toCheckoutError(e: unknown): CheckoutError {\n if (e && typeof e === 'object' && 'category' in e && 'message' in e && 'retryable' in e) {\n return e as CheckoutError\n }\n return checkoutError('processing_error', e instanceof Error ? e.message : 'Checkout request failed')\n}\n\n/**\n * Open a checkout attempt: `POST {apiBase}/campaign/{campaignId}/v2/checkout`.\n * The correlationId is client-minted per ATTEMPT (idempotency key + the id the\n * server stamps into provider metadata) — mint it once, reuse it on retries.\n */\n/**\n * The current page as a hosted checkout's return target — with `af_checkout` REMOVED.\n *\n * That param is the settle leg's correlation id, and it is not always cleaned up: a resume that\n * ends in `completion_timeout` (a slow webhook, an async/MoR settlement) deliberately leaves it in\n * the address bar so a reload can retry, and the URL sync then carries it onto every later page.\n * Sending it back would make the server append a SECOND `af_checkout`, and `searchParams.get()`\n * returns the FIRST — so the buyer's new, paid attempt would never settle client-side while a\n * stale one replayed. Same reason a seeded `?af_checkout=` in an ad URL must not survive to here.\n *\n * Everything else (utm, click ids) is kept: the return load re-reads it for attribution.\n */\nfunction currentReturnUrl(): string | undefined {\n if (typeof window === 'undefined') return undefined\n try {\n const url = new URL(window.location.href)\n url.searchParams.delete('af_checkout')\n return url.toString()\n } catch {\n return undefined\n }\n}\n\nexport function postSession(\n ctx: CheckoutDriverContext,\n req: CheckoutRequest,\n correlationId: string,\n): Promise<CheckoutSessionResponse> {\n const body: CheckoutSessionRequest = {\n funnelId: ctx.funnelId,\n mode: ctx.mode,\n // The CHARGE identity is the catalog key (resolved from the slot by driverWithCatalog); fall back\n // to `product` for legacy builds where the page named the catalog key directly.\n productKey: req.catalogKey ?? req.product,\n intent: req.intent,\n kind: req.kind,\n // null (not absent) = explicit off-session — the server validates surfaces\n // against the capability matrix, so never let one default in transit.\n surface: req.surface ?? null,\n // The provider VARIANT the author chose for this checkout (Stripe Managed Payments today).\n // Forwarded verbatim, never defaulted: `undefined` drops out of the JSON entirely, so a\n // non-managed attempt puts the exact same bytes on the wire it always has. The server owns\n // the other half of the gate — it re-validates the claim against the store's eligibility.\n managed: req.managed,\n sessionId: ctx.sessionId ?? undefined,\n visitorId: ctx.visitorId ?? undefined,\n // Buyer email collected by the funnel (user.email) — Stripe Customer creation,\n // receipts, Paddle prefill. Injected at attempt time by the FunnelProvider seam.\n email: req.email,\n // The page the buyer is leaving, so a hosted checkout returns them TO IT (with `?af_checkout=`)\n // instead of to the site root. The server keeps only path+query and re-derives the origin from\n // the request headers, so this can't redirect anyone off-domain. Absent during SSR.\n returnUrl: currentReturnUrl(),\n correlationId,\n }\n return postJson<CheckoutSessionResponse>(endpoint(ctx), body)\n}\n\n/** Ask the server to VERIFY with the provider and write the money facts (idempotent). */\nexport function postComplete(\n ctx: CheckoutDriverContext,\n correlationId: string,\n providerRef?: string,\n): Promise<CheckoutCompleteResponse> {\n const body: CheckoutCompleteRequest = { providerRef }\n return postJson<CheckoutCompleteResponse>(\n endpoint(ctx, `/${encodeURIComponent(correlationId)}/complete`),\n body,\n )\n}\n\n/** Server settlement → the SDK's CheckoutResult (eventId passes through for pixel dedup). */\nexport function settlementToResult(s: CheckoutSettlement): CheckoutResult {\n return {\n ok: true,\n amountMinor: s.amountMinor,\n currency: s.currency,\n eventId: s.eventId,\n // Carried through so the checkout hook can tell a trial start from a purchase — it decides\n // which conversion event fires, and the browser has no other honest source for it.\n acquisition: s.acquisition,\n }\n}\n\nexport interface CompletePollOptions {\n providerRef?: string\n /** Total complete attempts before giving up (default 5). */\n attempts?: number\n /** Delay between attempts (default 2000ms). */\n intervalMs?: number\n /** Error message when the provider hasn't confirmed inside the window (MoR/async). */\n timeoutMessage?: string\n}\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\n\n/**\n * Complete with a bounded poll. Sync providers (Stripe) settle on the first\n * attempt — the server verifies against the provider inline. MoR/async providers\n * (Paddle) settle via webhook, so `pending` is normal: re-poll a few times, then\n * stop holding the UI hostage — the charge may still land server-side, which is\n * why the timeout error is `retryable` and the message should say the receipt\n * can still arrive (the server's money facts don't depend on this browser).\n */\nexport async function completeWithPoll(\n ctx: CheckoutDriverContext,\n correlationId: string,\n opts: CompletePollOptions = {},\n): Promise<CheckoutResult> {\n const attempts = opts.attempts ?? 5\n const intervalMs = opts.intervalMs ?? 2000\n try {\n for (let i = 0; i < attempts; i++) {\n const res = await postComplete(ctx, correlationId, opts.providerRef)\n if (res.step === 'settled') return settlementToResult(res.result)\n if (res.step === 'failed') return { ok: false, error: res.error }\n if (i < attempts - 1) await sleep(intervalMs)\n }\n } catch (e) {\n return { ok: false, error: toCheckoutError(e) }\n }\n return {\n ok: false,\n error: checkoutError(\n 'processing_error',\n opts.timeoutMessage ?? 'The payment has not been confirmed yet — please check back shortly.',\n // `completion_timeout` marks \"the poll ran out while the attempt was still\n // pending\" — drivers key double-charge guards on it (the attempt may still\n // settle server-side; it was never refused).\n { retryable: true, declineCode: 'completion_timeout' },\n ),\n }\n}\n\n/**\n * The `redirect` step hand-off: navigate to the provider-hosted page and return a\n * promise that settles ONLY if the buyer comes back to this very page. The page\n * is leaving — resolving early would fire `purchase.complete`/navigation for a\n * charge that hasn't happened, and rejecting would flash an error during the\n * (successful) hand-off — so while the navigation proceeds the promise stays\n * pending and `useCheckout` stays in `loading`, the truthful UI for a page\n * mid-navigation. The hosted page's SUCCESS return URL re-enters the funnel with\n * `af_checkout={correlationId}` (a fresh load — this promise is gone; the SDK's\n * resume leg settles the attempt).\n *\n * The one way THIS page comes back to life is a bfcache restore (browser Back\n * from the hosted page): `pageshow` with `persisted: true`. That is a buyer who\n * abandoned the hosted checkout — resolve `canceled` so the trigger re-enables\n * and inline redirect placeholders recover instead of spinning forever.\n */\nexport function leaveForRedirect(url: string): Promise<CheckoutResult> {\n return new Promise<CheckoutResult>((resolve) => {\n if (typeof window !== 'undefined') {\n const onPageshow = (event: PageTransitionEvent) => {\n if (!event.persisted) return\n window.removeEventListener('pageshow', onPageshow)\n resolve({\n ok: false,\n error: checkoutError('canceled', 'Returned from the hosted checkout without completing'),\n })\n }\n window.addEventListener('pageshow', onPageshow)\n }\n window.location.assign(url)\n })\n}\n"]}
@@ -515,10 +515,23 @@ function interpolate(str, params) {
515
515
  if (!params) return str;
516
516
  return str.replace(/\{\{(\w+)\}\}/g, (_, k) => k in params ? String(params[k]) : `{{${k}}}`);
517
517
  }
518
- function lookup(catalog, chain, key) {
518
+ var baseLang = (locale) => locale.split("-")[0];
519
+ function lookupMessage(catalog, chain, key) {
519
520
  for (const loc of chain) {
520
- const v = catalog[loc]?.[key];
521
- if (v !== void 0) return v;
521
+ const lang = baseLang(loc);
522
+ const exact = catalog[loc]?.[key];
523
+ if (exact !== void 0) return exact;
524
+ if (lang !== loc) {
525
+ const wide = catalog[lang]?.[key];
526
+ if (wide !== void 0) return wide;
527
+ }
528
+ const siblings = Object.keys(catalog).filter(
529
+ (k) => k !== loc && k !== lang && baseLang(k) === lang
530
+ );
531
+ if (siblings.length === 1) {
532
+ const only = catalog[siblings[0]]?.[key];
533
+ if (only !== void 0) return only;
534
+ }
522
535
  }
523
536
  return void 0;
524
537
  }
@@ -562,9 +575,11 @@ function useTranslation() {
562
575
  }, [locale, config]);
563
576
  const t = React.useMemo(() => {
564
577
  const fn = ((key, params) => {
565
- const raw = lookup(catalog, chain, key);
578
+ const raw = lookupMessage(catalog, chain, key);
566
579
  if (raw === void 0) {
567
- devWarn(`[appfunnel] missing translation: "${key}" (${locale})`);
580
+ devWarn(
581
+ `[appfunnel] missing translation: "${key}" (${locale}; tried ${chain.join(" \u2192 ")})`
582
+ );
568
583
  return key;
569
584
  }
570
585
  return interpolate(raw, params);
@@ -7002,6 +7017,10 @@ function Modal(props) {
7002
7017
  }
7003
7018
 
7004
7019
  // src/commerce/checkout.tsx
7020
+ function refuseOffering(offering, as) {
7021
+ if (typeof offering === "string" && offering.trim() !== "") return void 0;
7022
+ return `<${as}> has no \`offering\` \u2014 there is nothing to charge. Name the offering slot (offering="plan"). If you are migrating from an older SDK, the prop used to be \`product\`.`;
7023
+ }
7005
7024
  function refuseSurface(provider, surface, managed) {
7006
7025
  if (!managed) return void 0;
7007
7026
  const check = chunkRUYT27DT_cjs.validateCheckout(provider, surface, { managed });
@@ -7182,20 +7201,31 @@ function useCheckout(opts = {}) {
7182
7201
  funnel.data.set({ purchased: true });
7183
7202
  const { productId, isSubscription } = productFacts(product);
7184
7203
  const amount = result.amountMinor != null ? result.amountMinor / 100 : void 0;
7185
- tracker.track("purchase.complete", {
7186
- amount,
7187
- currency: result.currency,
7188
- productId,
7189
- eventId: result.eventId
7190
- // server-minted (browser↔CAPI dedup); undefined if none
7191
- });
7192
- if (isSubscription) {
7193
- tracker.track("subscription.created", {
7204
+ const isTrialStart = result.acquisition === "free_trial" || result.acquisition === "paid_trial";
7205
+ if (isTrialStart) {
7206
+ tracker.track("subscription.trial_started", {
7194
7207
  amount,
7195
7208
  currency: result.currency,
7196
7209
  productId,
7197
7210
  eventId: result.eventId
7211
+ // server-minted (browser↔CAPI dedup)
7198
7212
  });
7213
+ } else {
7214
+ tracker.track("purchase.complete", {
7215
+ amount,
7216
+ currency: result.currency,
7217
+ productId,
7218
+ eventId: result.eventId
7219
+ // server-minted (browser↔CAPI dedup); undefined if none
7220
+ });
7221
+ if (isSubscription) {
7222
+ tracker.track("subscription.created", {
7223
+ amount,
7224
+ currency: result.currency,
7225
+ productId,
7226
+ eventId: result.eventId
7227
+ });
7228
+ }
7199
7229
  }
7200
7230
  opts.onSuccess?.(result);
7201
7231
  if (advance) nav.next();
@@ -7566,7 +7596,7 @@ function CheckoutImpl({
7566
7596
  notePreviewCheckout(as, product);
7567
7597
  checkout.callbacksFor(product, surface).onSuccess({ ok: true });
7568
7598
  };
7569
- const rejection = refuseSurface(driver.provider, surface, managed);
7599
+ const rejection = refuseOffering(offering, as) ?? refuseSurface(driver.provider, surface, managed);
7570
7600
  const callbacksRef = React.useRef(checkout.callbacksFor);
7571
7601
  callbacksRef.current = checkout.callbacksFor;
7572
7602
  React.useEffect(() => {
@@ -7721,6 +7751,14 @@ function UpsellButton(props) {
7721
7751
  );
7722
7752
  const checkout = useCheckout({ ...options, intent: "upsell", recoverySurface });
7723
7753
  const isSample = useOffering(offering)?.sample === true;
7754
+ const rejection = refuseOffering(offering, "UpsellButton");
7755
+ const callbacksRef = React.useRef(checkout.callbacksFor);
7756
+ callbacksRef.current = checkout.callbacksFor;
7757
+ React.useEffect(() => {
7758
+ if (!rejection) return;
7759
+ callbacksRef.current(product).onError(chunkRUYT27DT_cjs.checkoutError("processing_error", rejection, { retryable: false }));
7760
+ }, []);
7761
+ if (rejection) return null;
7724
7762
  return trigger(
7725
7763
  () => {
7726
7764
  if (isSample) {
@@ -9573,5 +9611,5 @@ exports.useTranslation = useTranslation;
9573
9611
  exports.useUserAttribute = useUserAttribute;
9574
9612
  exports.useUtm = useUtm;
9575
9613
  exports.withBus = withBus;
9576
- //# sourceMappingURL=chunk-NA7DRFOA.cjs.map
9577
- //# sourceMappingURL=chunk-NA7DRFOA.cjs.map
9614
+ //# sourceMappingURL=chunk-E7JXWUNV.cjs.map
9615
+ //# sourceMappingURL=chunk-E7JXWUNV.cjs.map