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

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-VV7TFC64.cjs +106 -0
  17. package/dist/chunk-VV7TFC64.cjs.map +1 -0
  18. package/dist/chunk-VW2HVPR4.js +446 -0
  19. package/dist/chunk-VW2HVPR4.js.map +1 -0
  20. package/dist/chunk-WYUDL4FI.cjs +8 -0
  21. package/dist/chunk-WYUDL4FI.cjs.map +1 -0
  22. package/dist/chunk-Y4YNJ2EX.cjs +476 -0
  23. package/dist/chunk-Y4YNJ2EX.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 +2654 -843
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +351 -1005
  41. package/dist/index.d.ts +351 -1005
  42. package/dist/index.js +2386 -704
  43. package/dist/index.js.map +1 -1
  44. package/dist/manifest-B3Tdab0M.d.cts +920 -0
  45. package/dist/manifest-B3Tdab0M.d.ts +920 -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
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
- import { createContext, createElement, useCallback, useContext, useMemo, lazy, useState, useRef, useEffect, Suspense, Fragment, isValidElement, cloneElement, useReducer, useSyncExternalStore } from 'react';
2
- import { createPortal } from 'react-dom';
3
- import { Toaster } from 'sonner';
4
- export { toast } from 'sonner';
5
-
6
- // src/state/namespaces.ts
1
+ import { isRtl, formatOffering, bucketingSeed, resolveExperiments, entryGuard, evaluateGate, expectedPathLength, nextPage, outgoingKeys, buildCatalog, resolveLocale } from './chunk-VW2HVPR4.js';
2
+ export { assignVariant, bucketingSeed, buildCatalog, compileManifest, currencyExponent, defineFunnel, definePage, entryGuard, evaluateCondition, evaluateGate, expectedPathLength, fnv1a, formatMoney, formatOffering, hashToUnit, isRtl, isVariantKey, nextPage, outgoingKeys, pageMeta, parseSlotKey, pickByWeight, resolveExperiments, resolveLocale, resolveOffering, resolveRoute } from './chunk-VW2HVPR4.js';
3
+ import { AF_PROTOCOL } from './chunk-ZZJG4EYL.js';
4
+ import { checkoutError, resolveSurface, isInlineSurface, PROVIDER_PROFILES } from './chunk-OXQBEKZ5.js';
5
+ export { INLINE_SURFACES, PROVIDER_PROFILES, checkoutError, isInlineSurface, isMerchantOfRecord, isOrchestrator, isRenderableSurface, renderableSurfaces, resolveSurface, surfacesFor, validateCheckout, validateUpsell } from './chunk-OXQBEKZ5.js';
6
+ import './chunk-G3PMV62Z.js';
7
+ import React, { createContext, createElement, useCallback, useContext, useMemo, lazy, useRef, useState, useEffect, useTransition, Suspense, Fragment, isValidElement, cloneElement, useReducer, useSyncExternalStore } from 'react';
8
+ import ReactDOM, { createPortal } from 'react-dom';
9
+ import { jsx } from 'react/jsx-runtime';
7
10
 
8
11
  // src/state/variableStore.ts
9
12
  var VariableStore = class {
@@ -257,8 +260,12 @@ function collectClickIds() {
257
260
  }
258
261
  function readCookie(name) {
259
262
  if (typeof document === "undefined") return void 0;
260
- const m = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
261
- return m ? decodeURIComponent(m[1]) : void 0;
263
+ try {
264
+ const m = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
265
+ return m ? decodeURIComponent(m[1]) : void 0;
266
+ } catch {
267
+ return void 0;
268
+ }
262
269
  }
263
270
  function buildAcquisition(context) {
264
271
  return {
@@ -271,41 +278,44 @@ function buildAcquisition(context) {
271
278
  }
272
279
  function buildContext(opts = {}) {
273
280
  const raw = detect();
274
- const now = opts.now ?? 0;
281
+ const now2 = opts.now ?? 0;
275
282
  const referrer = typeof document !== "undefined" ? document.referrer : "";
276
- const base2 = {
283
+ const base = {
277
284
  device: raw.device,
278
285
  browser: raw.browser,
279
286
  os: raw.os,
280
287
  locale: raw.locale,
281
288
  utm: collectUtm(),
282
289
  clickIds: collectClickIds(),
283
- page: { key: "", slug: "", index: 0, total: 0, progressPercentage: 0, startedAt: now },
284
- session: { id: opts.sessionId ?? null, startedAt: now, referrer },
290
+ page: { key: "", slug: "", index: 0, total: 0, progressPercentage: 0, startedAt: now2 },
291
+ session: { id: opts.sessionId ?? null, startedAt: now2, referrer },
285
292
  identity: { customerId: opts.customerId ?? null, visitorId: opts.visitorId ?? null },
286
293
  system: {
287
294
  funnelId: opts.funnelId ?? "",
288
295
  campaignId: opts.campaignId ?? "",
289
296
  mode: opts.mode ?? "live",
290
- now
297
+ now: now2
291
298
  }
292
299
  };
293
- if (!opts.overrides) return base2;
300
+ if (!opts.overrides) return base;
294
301
  return {
295
- ...base2,
302
+ ...base,
296
303
  ...opts.overrides,
297
- device: { ...base2.device, ...opts.overrides.device },
298
- browser: { ...base2.browser, ...opts.overrides.browser },
299
- os: { ...base2.os, ...opts.overrides.os },
300
- locale: { ...base2.locale, ...opts.overrides.locale },
301
- utm: { ...base2.utm, ...opts.overrides.utm },
302
- clickIds: { ...base2.clickIds, ...opts.overrides.clickIds },
303
- page: { ...base2.page, ...opts.overrides.page },
304
- session: { ...base2.session, ...opts.overrides.session },
305
- identity: { ...base2.identity, ...opts.overrides.identity },
306
- system: { ...base2.system, ...opts.overrides.system }
304
+ device: { ...base.device, ...opts.overrides.device },
305
+ browser: { ...base.browser, ...opts.overrides.browser },
306
+ os: { ...base.os, ...opts.overrides.os },
307
+ locale: { ...base.locale, ...opts.overrides.locale },
308
+ utm: { ...base.utm, ...opts.overrides.utm },
309
+ clickIds: { ...base.clickIds, ...opts.overrides.clickIds },
310
+ page: { ...base.page, ...opts.overrides.page },
311
+ session: { ...base.session, ...opts.overrides.session },
312
+ identity: { ...base.identity, ...opts.overrides.identity },
313
+ system: { ...base.system, ...opts.overrides.system }
307
314
  };
308
315
  }
316
+ function exposurePayload(experimentId, variant, version) {
317
+ return { experimentId, variant, ...version !== void 0 ? { version } : {} };
318
+ }
309
319
  var _eventIdCounter = 0;
310
320
  function newEventId() {
311
321
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
@@ -344,7 +354,7 @@ function useTracker() {
344
354
 
345
355
  // src/tracking/bus.ts
346
356
  var MAX_HISTORY = 250;
347
- function createBus(accessors, now = () => Date.now()) {
357
+ function createBus(accessors, now2 = () => Date.now()) {
348
358
  const listeners = /* @__PURE__ */ new Map();
349
359
  const events = [];
350
360
  const on = (event, cb) => {
@@ -357,7 +367,7 @@ function createBus(accessors, now = () => Date.now()) {
357
367
  listeners.get(event)?.delete(cb);
358
368
  };
359
369
  const emit = (event, data) => {
360
- events.push({ event, data, ts: now() });
370
+ events.push({ event, data, ts: now2() });
361
371
  if (events.length > MAX_HISTORY) events.shift();
362
372
  for (const cb of listeners.get(event) ?? []) {
363
373
  try {
@@ -372,7 +382,40 @@ function createBus(accessors, now = () => Date.now()) {
372
382
  }
373
383
  }
374
384
  };
375
- return { on, off, events, emit, ...accessors };
385
+ let nav = null;
386
+ let warnedNoNav = false;
387
+ const currentNav = () => {
388
+ if (!nav && !warnedNoNav) {
389
+ warnedNoNav = true;
390
+ console.warn(
391
+ "[appfunnel] window.appfunnel navigation called before the funnel flow mounted \u2014 ignored"
392
+ );
393
+ }
394
+ return nav;
395
+ };
396
+ const registerNavigation = (n) => {
397
+ nav = n;
398
+ warnedNoNav = false;
399
+ return () => {
400
+ if (nav === n) nav = null;
401
+ };
402
+ };
403
+ const goTo = (pageKey) => currentNav()?.goTo(pageKey) ?? false;
404
+ const goNext = () => currentNav()?.goNext();
405
+ const goPrevious = () => currentNav()?.goPrevious();
406
+ const navigation = () => nav?.snapshot() ?? null;
407
+ return {
408
+ on,
409
+ off,
410
+ events,
411
+ emit,
412
+ registerNavigation,
413
+ goTo,
414
+ goNext,
415
+ goPrevious,
416
+ navigation,
417
+ ...accessors
418
+ };
376
419
  }
377
420
  function withBus(tracker, bus) {
378
421
  return {
@@ -394,7 +437,19 @@ function withBus(tracker, bus) {
394
437
  function attachBus(bus) {
395
438
  if (typeof window === "undefined") return () => {
396
439
  };
397
- const { on, off, getVariable, getVariables, getCurrentPageId, getCustomerId, getVisitorId } = bus;
440
+ const {
441
+ on,
442
+ off,
443
+ getVariable,
444
+ getVariables,
445
+ getCurrentPageId,
446
+ getCustomerId,
447
+ getVisitorId,
448
+ goTo,
449
+ goNext,
450
+ goPrevious,
451
+ navigation
452
+ } = bus;
398
453
  const api = {
399
454
  on,
400
455
  off,
@@ -403,6 +458,11 @@ function attachBus(bus) {
403
458
  getCurrentPageId,
404
459
  getCustomerId,
405
460
  getVisitorId,
461
+ // Flow control (bound closures over the bus's live nav registration).
462
+ goTo,
463
+ goNext,
464
+ goPrevious,
465
+ navigation,
406
466
  get events() {
407
467
  return bus.events;
408
468
  }
@@ -412,21 +472,6 @@ function attachBus(bus) {
412
472
  if (window.appfunnel === api) delete window.appfunnel;
413
473
  };
414
474
  }
415
- var RTL_LANGS = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur", "ps", "sd", "dv", "yi"]);
416
- function isRtl(locale) {
417
- return RTL_LANGS.has(locale.split("-")[0]);
418
- }
419
- var base = (l) => l?.split("-")[0];
420
- function resolveLocale(config, detected, override) {
421
- const supported = config?.supported ?? (config?.default ? [config.default] : ["en"]);
422
- const def = config?.default ?? supported[0] ?? "en";
423
- const pick = (l) => {
424
- if (!l) return void 0;
425
- if (supported.includes(l)) return l;
426
- return supported.find((s) => base(s) === base(l));
427
- };
428
- return pick(override) ?? pick(detected) ?? def;
429
- }
430
475
  function interpolate(str, params) {
431
476
  if (!params) return str;
432
477
  return str.replace(/\{\{(\w+)\}\}/g, (_, k) => k in params ? String(params[k]) : `{{${k}}}`);
@@ -455,6 +500,9 @@ function LocaleProvider({
455
500
  children
456
501
  }) {
457
502
  const [locale, setLocale] = useState(() => resolveLocale(config, detected, override));
503
+ useEffect(() => {
504
+ setLocale(resolveLocale(config, detected, override));
505
+ }, [config, detected, override]);
458
506
  const value = useMemo(
459
507
  () => ({ locale, setLocale, config, catalog }),
460
508
  [locale, config, catalog]
@@ -483,7 +531,12 @@ function useTranslation() {
483
531
  return interpolate(raw, params);
484
532
  });
485
533
  fn.plural = (n, forms, params) => {
486
- const rule = new Intl.PluralRules(locale).select(n);
534
+ let rule;
535
+ try {
536
+ rule = new Intl.PluralRules(locale).select(n);
537
+ } catch {
538
+ rule = "other";
539
+ }
487
540
  const form = forms[rule] ?? forms.other ?? "";
488
541
  return interpolate(form.replace(/#/g, String(n)), params);
489
542
  };
@@ -491,9 +544,27 @@ function useTranslation() {
491
544
  }, [catalog, chain, locale]);
492
545
  const fmt = useMemo(
493
546
  () => ({
494
- number: (n, opts) => new Intl.NumberFormat(locale, opts).format(n),
495
- date: (d, opts) => new Intl.DateTimeFormat(locale, opts).format(d),
496
- percent: (n, opts) => new Intl.NumberFormat(locale, { style: "percent", ...opts }).format(n)
547
+ number: (n, opts) => {
548
+ try {
549
+ return new Intl.NumberFormat(locale, opts).format(n);
550
+ } catch {
551
+ return String(n);
552
+ }
553
+ },
554
+ date: (d, opts) => {
555
+ try {
556
+ return new Intl.DateTimeFormat(locale, opts).format(d);
557
+ } catch {
558
+ return String(d instanceof Date ? d.toISOString() : d);
559
+ }
560
+ },
561
+ percent: (n, opts) => {
562
+ try {
563
+ return new Intl.NumberFormat(locale, { style: "percent", ...opts }).format(n);
564
+ } catch {
565
+ return `${(n * 100).toFixed(0)}%`;
566
+ }
567
+ }
497
568
  }),
498
569
  [locale]
499
570
  );
@@ -508,84 +579,6 @@ function useTranslation() {
508
579
  }
509
580
 
510
581
  // src/commerce/catalog.ts
511
- var PERIOD_DAYS = {
512
- day: 1,
513
- week: 7,
514
- month: 30,
515
- year: 365,
516
- one_time: 0
517
- };
518
- function periodDays(interval, count) {
519
- return PERIOD_DAYS[interval] * count;
520
- }
521
- function intervalLabel(interval, count) {
522
- if (interval === "one_time") return { period: "one-time", periodly: "one-time" };
523
- if (interval === "month" && count === 3) return { period: "quarter", periodly: "quarterly" };
524
- if (interval === "month" && count === 6) return { period: "6 months", periodly: "semiannually" };
525
- if (interval === "week" && count === 2) return { period: "2 weeks", periodly: "biweekly" };
526
- if (count === 1) {
527
- const periodly = { day: "daily", week: "weekly", month: "monthly", year: "yearly" }[interval];
528
- return { period: interval, periodly };
529
- }
530
- return { period: `${count} ${interval}s`, periodly: `every ${count} ${interval}s` };
531
- }
532
- function resolveProduct(input) {
533
- const interval = input.interval ?? "one_time";
534
- const count = input.intervalCount ?? 1;
535
- const days = periodDays(interval, count);
536
- const perDayMinor = days > 0 ? input.amount / days : input.amount;
537
- const label = intervalLabel(interval, count);
538
- return {
539
- id: input.id,
540
- name: input.name ?? input.id,
541
- displayName: input.displayName ?? input.name ?? input.id,
542
- provider: input.provider ?? "stripe",
543
- currency: input.currency,
544
- priceMinor: input.amount,
545
- perDayMinor,
546
- perWeekMinor: perDayMinor * 7,
547
- perMonthMinor: perDayMinor * 30,
548
- perYearMinor: perDayMinor * 365,
549
- period: label.period,
550
- periodly: label.periodly,
551
- hasTrial: (input.trialDays ?? 0) > 0,
552
- trialDays: input.trialDays ?? 0,
553
- trialMinor: input.trialAmount ?? 0
554
- };
555
- }
556
- function buildCatalog(inputs) {
557
- return new Map(inputs.map((i) => [i.id, resolveProduct(i)]));
558
- }
559
- function formatMoney(minor, currency, locale) {
560
- const amount = minor / 100;
561
- const code = currency.toUpperCase();
562
- let formatted;
563
- try {
564
- formatted = new Intl.NumberFormat(locale, { style: "currency", currency: code }).format(amount);
565
- } catch {
566
- formatted = `${code} ${amount.toFixed(2)}`;
567
- }
568
- return { amount, minor, currency: code, formatted };
569
- }
570
- function formatProduct(r, locale) {
571
- const m = (minor) => formatMoney(minor, r.currency, locale);
572
- return {
573
- id: r.id,
574
- name: r.name,
575
- displayName: r.displayName,
576
- provider: r.provider,
577
- price: m(r.priceMinor),
578
- period: r.period,
579
- periodly: r.periodly,
580
- perDay: m(r.perDayMinor),
581
- perWeek: m(r.perWeekMinor),
582
- perMonth: m(r.perMonthMinor),
583
- perYear: m(r.perYearMinor),
584
- hasTrial: r.hasTrial,
585
- trialDays: r.trialDays,
586
- trialPrice: r.hasTrial ? m(r.trialMinor) : null
587
- };
588
- }
589
582
  var CatalogContext = createContext(null);
590
583
  function CatalogProvider({
591
584
  catalog,
@@ -597,234 +590,116 @@ function useCatalog() {
597
590
  return useContext(CatalogContext) ?? EMPTY;
598
591
  }
599
592
  var EMPTY = /* @__PURE__ */ new Map();
600
- function useProduct(id) {
593
+ function useOffering(id) {
601
594
  const catalog = useCatalog();
602
595
  const locale = useActiveLocale();
603
596
  const resolved = id ? catalog.get(id) : void 0;
604
- return useMemo(() => resolved ? formatProduct(resolved, locale) : void 0, [resolved, locale]);
597
+ return useMemo(
598
+ () => resolved ? formatOffering(resolved, locale) : void 0,
599
+ [resolved, locale]
600
+ );
605
601
  }
606
- function useProducts() {
602
+ function useOfferings() {
607
603
  const catalog = useCatalog();
608
604
  const locale = useActiveLocale();
609
605
  return useMemo(
610
- () => Array.from(catalog.values()).map((r) => formatProduct(r, locale)),
606
+ () => Array.from(catalog.values()).map((r) => formatOffering(r, locale)),
611
607
  [catalog, locale]
612
608
  );
613
609
  }
614
610
 
615
- // src/flow/spine.ts
616
- function readField(s, field) {
617
- let cur = s;
618
- for (const part of field.split(".")) {
619
- if (cur == null) return void 0;
620
- cur = cur[part];
621
- }
622
- return cur;
623
- }
624
- function evaluateCondition(cond, s) {
625
- const val = readField(s, cond.field);
626
- const { op, value } = cond;
627
- switch (op) {
628
- case "eq":
629
- return val === value;
630
- case "neq":
631
- return val !== value;
632
- case "in":
633
- return Array.isArray(value) && value.includes(val);
634
- case "gt":
635
- return typeof val === "number" && typeof value === "number" && val > value;
636
- case "gte":
637
- return typeof val === "number" && typeof value === "number" && val >= value;
638
- case "lt":
639
- return typeof val === "number" && typeof value === "number" && val < value;
640
- case "lte":
641
- return typeof val === "number" && typeof value === "number" && val <= value;
642
- case "contains":
643
- if (typeof val === "string") return val.includes(String(value));
644
- if (Array.isArray(val)) return val.includes(value);
645
- return false;
646
- case "exists":
647
- return val !== void 0 && val !== null && val !== "";
648
- case "empty":
649
- return val === void 0 || val === null || val === "" || Array.isArray(val) && val.length === 0;
650
- default:
651
- return false;
652
- }
653
- }
654
- function evaluateGate(gate, s) {
655
- return typeof gate === "function" ? gate(s) : evaluateCondition(gate, s);
656
- }
657
- function resolveRoute(routes, s) {
658
- if (!routes) return void 0;
659
- for (const route of routes) {
660
- if (!route.when || evaluateGate(route.when, s)) return route.to;
611
+ // src/flow/screenshot.ts
612
+ var MIME = { png: "image/png", jpeg: "image/jpeg", webp: "image/webp" };
613
+ var ScreenshotError = class extends Error {
614
+ constructor(code, message) {
615
+ super(message);
616
+ this.code = code;
661
617
  }
662
- return void 0;
663
- }
664
- function pageMeta(meta) {
665
- return meta;
666
- }
667
- var TYPE_GUARDS = {
668
- paywall: { field: "user.email", op: "exists" },
669
- upsell: { field: "user.email", op: "exists" }
670
618
  };
671
- function entryGuard(meta) {
672
- return meta?.guard ?? (meta?.type ? TYPE_GUARDS[meta.type] : void 0);
673
- }
674
- function definePage(component) {
675
- return component;
676
- }
677
- function nextPage(pages, currentKey, s) {
678
- const idx = pages.findIndex((p) => p.key === currentKey);
679
- if (idx === -1) return void 0;
680
- const routed = resolveRoute(pages[idx].meta?.next, s);
681
- if (routed) return routed;
682
- return pages[idx + 1]?.key;
683
- }
684
- function outgoingKeys(pages, currentKey) {
685
- const idx = pages.findIndex((p) => p.key === currentKey);
686
- if (idx === -1) return [];
687
- const out = /* @__PURE__ */ new Set();
688
- for (const route of pages[idx].meta?.next ?? []) out.add(route.to);
689
- const linear = pages[idx + 1]?.key;
690
- if (linear) out.add(linear);
691
- return [...out];
692
- }
693
- function expectedPathLength(pages, startKey, s) {
694
- const seen = /* @__PURE__ */ new Set();
695
- let key = startKey;
696
- let count = 0;
697
- while (key && !seen.has(key)) {
698
- seen.add(key);
699
- count++;
700
- if (pages.find((p) => p.key === key)?.meta?.type === "finish") break;
701
- key = nextPage(pages, key, s);
702
- }
703
- return count;
704
- }
705
- function defineFunnel(def) {
706
- return def;
707
- }
708
-
709
- // src/flow/experiments.ts
710
- function fnv1a(input) {
711
- let h = 2166136261;
712
- for (let i = 0; i < input.length; i++) {
713
- h ^= input.charCodeAt(i);
714
- h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
619
+ function resolveTarget(t) {
620
+ if (!t || t.kind === "page") return document.getElementById("root") ?? document.body;
621
+ if (t.kind === "viewport") return document.body;
622
+ return document.querySelector(t.selector);
623
+ }
624
+ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
625
+ function hideAll(selectors) {
626
+ const touched = [];
627
+ for (const sel of selectors) {
628
+ document.querySelectorAll(sel).forEach((el) => {
629
+ touched.push([el, el.style.visibility]);
630
+ el.style.visibility = "hidden";
631
+ });
715
632
  }
716
- return h >>> 0;
717
- }
718
- function hashToUnit(seed) {
719
- return fnv1a(seed) / 4294967296;
720
- }
721
- function pickByWeight(weights, u) {
722
- const entries = Object.entries(weights).filter(([, w]) => w > 0);
723
- if (entries.length === 0) return Object.keys(weights)[0] ?? "";
724
- const total = entries.reduce((sum, [, w]) => sum + w, 0);
725
- const target = u * total;
726
- let acc = 0;
727
- for (const [key, w] of entries) {
728
- acc += w;
729
- if (target < acc) return key;
633
+ return () => touched.forEach(([el, v]) => el.style.visibility = v);
634
+ }
635
+ async function scrollThrough(el) {
636
+ const prev = el.scrollTop;
637
+ const step = el.clientHeight || window.innerHeight;
638
+ for (let y = 0; y < el.scrollHeight; y += step) {
639
+ el.scrollTop = y;
640
+ await delay(16);
730
641
  }
731
- return entries[entries.length - 1][0];
732
- }
733
- function assignVariant(experimentId, seed, weights) {
734
- return pickByWeight(weights, hashToUnit(`${experimentId}:${seed}`));
735
- }
736
- function bucketingSeed(context) {
737
- return context.identity.customerId ?? context.identity.visitorId ?? null;
738
- }
739
- function parseSlotKey(key) {
740
- const at = key.indexOf("@");
741
- return at === -1 ? { slot: key } : { slot: key.slice(0, at), variant: key.slice(at + 1) };
742
- }
743
- function isVariantKey(key) {
744
- return parseSlotKey(key).variant !== void 0;
745
- }
746
- function weightsOf(exp) {
747
- const out = {};
748
- for (const [label, v] of Object.entries(exp.variants)) out[label] = v.weight;
749
- return out;
642
+ el.scrollTop = prev;
643
+ await delay(16);
644
+ }
645
+ function blobToDataURL(blob) {
646
+ return new Promise((resolve, reject) => {
647
+ const fr = new FileReader();
648
+ fr.onload = () => resolve(String(fr.result));
649
+ fr.onerror = () => reject(fr.error);
650
+ fr.readAsDataURL(blob);
651
+ });
750
652
  }
751
- function resolveExperiments(pages, experiments, seed) {
752
- const assignments = {};
753
- const render = {};
754
- const experimentOf = {};
755
- for (const exp of experiments) {
756
- const status = exp.status ?? "running";
757
- let label;
758
- if (status === "stopped" && exp.winner) label = exp.winner;
759
- else if (status === "running" && seed) label = assignVariant(exp.id, seed, weightsOf(exp));
760
- else continue;
761
- const arm = exp.variants[label];
762
- if (!arm) continue;
763
- assignments[exp.id] = label;
764
- experimentOf[exp.slot] = exp.id;
765
- if (arm.page !== exp.slot) render[exp.slot] = arm.page;
766
- }
767
- const slotOf = {};
768
- const activeKeys = [];
769
- for (const p of pages) {
770
- const { slot, variant } = parseSlotKey(p.key);
771
- if (variant !== void 0) slotOf[p.key] = slot;
772
- else activeKeys.push(p.key);
773
- }
774
- return { assignments, activeKeys, render, slotOf, experimentOf };
775
- }
776
- function validateExperiments(experiments, pages) {
777
- const errors = [];
778
- const warnings = [];
779
- const pageKeys = new Set(pages.map((p) => p.key));
780
- const seenIds = /* @__PURE__ */ new Set();
781
- const slotOwner = /* @__PURE__ */ new Map();
782
- const renderedVariantPages = /* @__PURE__ */ new Set();
783
- for (const exp of experiments) {
784
- const err = (code, message) => errors.push({ experimentId: exp.id, code, message });
785
- const warn = (code, message) => warnings.push({ experimentId: exp.id, code, message });
786
- if (seenIds.has(exp.id)) err("duplicate_id", `Experiment id "${exp.id}" is used more than once.`);
787
- seenIds.add(exp.id);
788
- if (!pageKeys.has(exp.slot)) err("slot_missing", `Slot "${exp.slot}" is not a page in the funnel.`);
789
- if (isVariantKey(exp.slot)) err("slot_is_variant", `Slot "${exp.slot}" is a variant page; a slot must be a real flow page.`);
790
- if (slotOwner.has(exp.slot)) {
791
- err("slot_conflict", `Slot "${exp.slot}" is already targeted by experiment "${slotOwner.get(exp.slot)}".`);
792
- } else {
793
- slotOwner.set(exp.slot, exp.id);
794
- }
795
- const arms = Object.entries(exp.variants);
796
- if (arms.length < 2) warn("single_arm", `Experiment "${exp.id}" has fewer than two variants \u2014 nothing to test.`);
797
- let hasControl = false;
798
- let totalWeight = 0;
799
- for (const [label, arm] of arms) {
800
- if (!pageKeys.has(arm.page)) {
801
- err("variant_page_missing", `Variant "${label}" references page "${arm.page}", which doesn't exist.`);
802
- }
803
- if (arm.page === exp.slot) hasControl = true;
804
- else if (isVariantKey(arm.page)) {
805
- renderedVariantPages.add(arm.page);
806
- if (parseSlotKey(arm.page).slot !== exp.slot) {
807
- err("variant_slot_mismatch", `Variant "${label}" page "${arm.page}" belongs to a different slot than "${exp.slot}".`);
808
- }
809
- }
810
- if (arm.weight < 0) err("negative_weight", `Variant "${label}" has a negative weight (${arm.weight}).`);
811
- totalWeight += arm.weight;
653
+ async function captureScreenshot(params = {}) {
654
+ const el = resolveTarget(params.target);
655
+ if (!el) throw new ScreenshotError("bad_params", "screenshot target not found");
656
+ const restore = params.hideSelectors?.length ? hideAll(params.hideSelectors) : null;
657
+ const scale = Math.min(params.scale ?? 2, params.maxScale ?? 3);
658
+ const format = params.format ?? "png";
659
+ const timeout = params.timeout ?? 1e4;
660
+ try {
661
+ if (params.waitForFonts !== false && document.fonts?.ready) {
662
+ await Promise.race([document.fonts.ready, delay(timeout)]);
812
663
  }
813
- if (!hasControl) err("no_control", `No variant of "${exp.id}" renders the slot "${exp.slot}" itself (the control/baseline).`);
814
- if (totalWeight <= 0) err("no_traffic", `Experiment "${exp.id}" has no positive weight \u2014 no traffic would enter it.`);
815
- if (exp.status === "stopped" && exp.winner && !exp.variants[exp.winner]) {
816
- err("bad_winner", `Stopped experiment "${exp.id}" names winner "${exp.winner}", which isn't one of its variants.`);
664
+ if (params.triggerLazyLoad) await scrollThrough(el);
665
+ if (params.waitForImages !== false) {
666
+ await Promise.all(
667
+ Array.from(el.querySelectorAll("img")).map(
668
+ (img) => typeof img.decode === "function" ? img.decode().catch(() => {
669
+ }) : Promise.resolve()
670
+ )
671
+ );
817
672
  }
818
- }
819
- const running = new Set(experiments.filter((e) => (e.status ?? "running") === "running").flatMap(
820
- (e) => Object.values(e.variants).map((v) => v.page)
821
- ));
822
- for (const p of pages) {
823
- if (isVariantKey(p.key) && !running.has(p.key)) {
824
- warnings.push({ experimentId: "*", code: "orphan_variant", message: `Variant page "${p.key}" is rendered by no running experiment.` });
673
+ const { domToBlob } = await import('modern-screenshot');
674
+ let blob;
675
+ try {
676
+ blob = await domToBlob(el, {
677
+ type: MIME[format],
678
+ quality: params.quality ?? 0.92,
679
+ scale,
680
+ width: params.width,
681
+ height: params.height,
682
+ // `null` = transparent (png/webp); only fall back to white when the caller omitted it. `??`
683
+ // would wrongly collapse an explicit `null` to white.
684
+ backgroundColor: params.backgroundColor === void 0 ? "#ffffff" : params.backgroundColor,
685
+ timeout
686
+ });
687
+ } catch (e) {
688
+ throw new ScreenshotError("failed", `capture failed (an image/font may be served without CORS '*'): ${String(e?.message || e)}`);
825
689
  }
690
+ const result = {
691
+ mime: MIME[format],
692
+ width: Math.round((params.width ?? el.clientWidth) * scale),
693
+ height: Math.round((params.height ?? el.scrollHeight) * scale),
694
+ scale,
695
+ bytesApprox: blob.size
696
+ };
697
+ if ((params.encoding ?? "blob") === "dataURL") result.dataURL = await blobToDataURL(blob);
698
+ else result.blob = blob;
699
+ return result;
700
+ } finally {
701
+ restore?.();
826
702
  }
827
- return { ok: errors.length === 0, errors, warnings };
828
703
  }
829
704
 
830
705
  // src/flow/flow.tsx
@@ -844,9 +719,43 @@ function pageContextFor(currentKey, slug, index, total, startedAt) {
844
719
  startedAt
845
720
  };
846
721
  }
722
+ var now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
723
+ function usePageExitTimer(tracker, currentKey) {
724
+ const timer = useRef(null);
725
+ const emitExit = useCallback(() => {
726
+ const t = timer.current;
727
+ if (!t) return;
728
+ const at = now();
729
+ const hidden = t.hidden + (t.hiddenSince != null ? at - t.hiddenSince : 0);
730
+ const durationMs = Math.round(at - t.enteredAt);
731
+ tracker.track("page.exit", { pageId: t.key, durationMs, activeMs: Math.max(0, Math.round(durationMs - hidden)) });
732
+ }, [tracker]);
733
+ useEffect(() => {
734
+ if (typeof document === "undefined") return;
735
+ const onVis = () => {
736
+ const t = timer.current;
737
+ if (!t) return;
738
+ if (document.visibilityState === "hidden") t.hiddenSince = now();
739
+ else if (t.hiddenSince != null) {
740
+ t.hidden += now() - t.hiddenSince;
741
+ t.hiddenSince = null;
742
+ }
743
+ };
744
+ document.addEventListener("visibilitychange", onVis);
745
+ return () => document.removeEventListener("visibilitychange", onVis);
746
+ }, []);
747
+ useEffect(() => {
748
+ if (!currentKey) return;
749
+ if (timer.current && timer.current.key !== currentKey) emitExit();
750
+ const hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
751
+ timer.current = { key: currentKey, enteredAt: now(), hidden: 0, hiddenSince: hidden ? now() : null };
752
+ }, [currentKey, emitExit]);
753
+ useEffect(() => () => emitExit(), [emitExit]);
754
+ }
847
755
  function FunnelView({
848
756
  pages,
849
757
  initialKey,
758
+ trustInitialKey = false,
850
759
  layout,
851
760
  fallback,
852
761
  prefetch: prefetchMode = "auto",
@@ -871,28 +780,38 @@ function FunnelView({
871
780
  () => new Map(pages.filter((p) => p.load).map((p) => [p.key, p.load])),
872
781
  [pages]
873
782
  );
874
- const seed = bucketingSeed(funnel.context);
783
+ const seedRef = useRef(null);
784
+ if (seedRef.current === null) seedRef.current = bucketingSeed(funnel.context);
785
+ const seed = seedRef.current;
875
786
  const experiments = useMemo(
876
787
  () => resolveExperiments(pages, funnel.experiments, seed),
877
788
  [pages, funnel.experiments, seed]
878
789
  );
790
+ const toSlot = useCallback((k) => experiments.slotOf[k] ?? k, [experiments]);
791
+ const toRenderKey = useCallback((k) => experiments.render[k] ?? k, [experiments]);
879
792
  const pageByKey = useMemo(() => new Map(pages.map((p) => [p.key, p])), [pages]);
880
793
  const flowPages = useMemo(
881
- () => experiments.activeKeys.map((k) => pageByKey.get(k)).filter(Boolean),
794
+ () => experiments.activeKeys.map((k) => pageByKey.get(k)).filter((p) => p !== void 0),
882
795
  [experiments, pageByKey]
883
796
  );
884
797
  const flow = useMemo(
885
- () => flowPages.map((p) => ({ key: p.key, meta: p.meta })),
886
- [flowPages]
798
+ () => flowPages.map((p) => {
799
+ const servedVariant = experiments.render[p.key];
800
+ const variantNext = servedVariant ? pageByKey.get(servedVariant)?.meta?.next : void 0;
801
+ return variantNext ? { key: p.key, meta: { ...p.meta ?? {}, next: variantNext } } : { key: p.key, meta: p.meta };
802
+ }),
803
+ [flowPages, experiments, pageByKey]
887
804
  );
888
805
  const [history, setHistory] = useState(() => {
889
806
  const start = flowPages[0]?.key;
890
- if (!initialKey || initialKey === start) return [start].filter(Boolean);
891
- const target = experiments.slotOf[initialKey] ?? initialKey;
807
+ if (!initialKey || initialKey === start) return start ? [start] : [];
808
+ const target = toSlot(initialKey);
892
809
  const page2 = flowPages.find((p) => p.key === target);
810
+ if (trustInitialKey) return page2 ? [target] : start ? [start] : [];
893
811
  const guard = entryGuard(page2?.meta);
894
812
  const ok = page2 && (!guard || evaluateGate(guard, funnel.snapshot()));
895
- return [ok ? target : start].filter(Boolean);
813
+ const first = ok ? target : start;
814
+ return first ? [first] : [];
896
815
  });
897
816
  const currentKey = history[history.length - 1];
898
817
  const index = history.length - 1;
@@ -907,7 +826,9 @@ function FunnelView({
907
826
  () => pageContextFor(currentKey, slug, index, total, funnel.context.system.now),
908
827
  [currentKey, slug, index, total, funnel.context.system.now]
909
828
  );
910
- funnel.context.page = page;
829
+ useEffect(() => {
830
+ funnel.context.page = page;
831
+ }, [funnel, page]);
911
832
  const started = useRef(false);
912
833
  useEffect(() => {
913
834
  if (started.current) return;
@@ -926,66 +847,37 @@ function FunnelView({
926
847
  const expId = experiments.experimentOf[currentKey];
927
848
  if (!expId || exposed.current.has(expId)) return;
928
849
  exposed.current.add(expId);
929
- tracker.track("experiment.exposure", { experimentId: expId, variant: experiments.assignments[expId] });
850
+ tracker.track("experiment.exposure", exposurePayload(expId, experiments.assignments[expId], experiments.versionOf[expId]));
930
851
  }, [tracker, currentKey, experiments]);
931
- const timer = useRef(null);
932
- const now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
933
- const emitExit = useCallback(() => {
934
- const t = timer.current;
935
- if (!t) return;
936
- const at = now();
937
- const hidden = t.hidden + (t.hiddenSince != null ? at - t.hiddenSince : 0);
938
- const durationMs = Math.round(at - t.enteredAt);
939
- tracker.track("page.exit", { pageId: t.key, durationMs, activeMs: Math.max(0, Math.round(durationMs - hidden)) });
940
- }, [tracker]);
941
- useEffect(() => {
942
- if (typeof document === "undefined") return;
943
- const onVis = () => {
944
- const t = timer.current;
945
- if (!t) return;
946
- if (document.visibilityState === "hidden") t.hiddenSince = now();
947
- else if (t.hiddenSince != null) {
948
- t.hidden += now() - t.hiddenSince;
949
- t.hiddenSince = null;
950
- }
951
- };
952
- document.addEventListener("visibilitychange", onVis);
953
- return () => document.removeEventListener("visibilitychange", onVis);
954
- }, []);
955
- useEffect(() => {
956
- if (!currentKey) return;
957
- if (timer.current && timer.current.key !== currentKey) emitExit();
958
- const hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
959
- timer.current = { key: currentKey, enteredAt: now(), hidden: 0, hiddenSince: hidden ? now() : null };
960
- }, [currentKey, emitExit]);
961
- useEffect(() => () => emitExit(), [emitExit]);
852
+ usePageExitTimer(tracker, currentKey);
853
+ const [isNavigating, startNav] = useTransition();
962
854
  const next = useCallback(() => {
963
855
  const target = nextPage(flow, currentKey, funnel.snapshot());
964
- const resolved = target ? experiments.slotOf[target] ?? target : target;
965
- if (resolved) setHistory((h) => [...h, resolved]);
966
- }, [flow, currentKey, funnel, experiments]);
856
+ const resolved = target ? toSlot(target) : target;
857
+ if (resolved) startNav(() => setHistory((h) => [...h, resolved]));
858
+ }, [flow, currentKey, funnel, toSlot]);
967
859
  const back = useCallback(
968
- () => setHistory((h) => h.length > 1 ? h.slice(0, -1) : h),
860
+ () => startNav(() => setHistory((h) => h.length > 1 ? h.slice(0, -1) : h)),
969
861
  []
970
862
  );
971
863
  const go = useCallback(
972
- (key) => setHistory((h) => [...h, experiments.slotOf[key] ?? key]),
973
- [experiments]
864
+ (key) => startNav(() => setHistory((h) => [...h, toSlot(key)])),
865
+ [toSlot]
974
866
  );
975
867
  const prefetched = useRef(/* @__PURE__ */ new Set());
976
868
  const prefetch = useCallback(
977
869
  (key) => {
978
- const target = experiments.render[key] ?? key;
870
+ const target = toRenderKey(key);
979
871
  const load = loaders.get(target);
980
872
  if (!load || prefetched.current.has(target)) return;
981
873
  prefetched.current.add(target);
982
874
  load().catch(() => prefetched.current.delete(target));
983
875
  },
984
- [loaders, experiments]
876
+ [loaders, toRenderKey]
985
877
  );
986
878
  const nextCandidates = useMemo(
987
- () => [...new Set(outgoingKeys(flow, currentKey).map((k) => experiments.render[k] ?? k))],
988
- [flow, currentKey, experiments]
879
+ () => [...new Set(outgoingKeys(flow, currentKey).map(toRenderKey))],
880
+ [flow, currentKey, toRenderKey]
989
881
  );
990
882
  useEffect(() => {
991
883
  if (prefetchMode !== "auto") return;
@@ -998,9 +890,48 @@ function FunnelView({
998
890
  go,
999
891
  prefetch,
1000
892
  nextCandidates,
1001
- canGoBack: history.length > 1
893
+ canGoBack: history.length > 1,
894
+ isNavigating
1002
895
  };
1003
- const renderKey = experiments.render[currentKey] ?? currentKey;
896
+ const bus = useBusInternal();
897
+ const navRef = useRef(value);
898
+ navRef.current = value;
899
+ useEffect(() => {
900
+ if (!bus) return;
901
+ const control = {
902
+ goTo: (key) => {
903
+ if (!components.has(toRenderKey(key))) return false;
904
+ navRef.current.go(key);
905
+ return true;
906
+ },
907
+ goNext: () => navRef.current.next(),
908
+ goPrevious: () => navRef.current.back(),
909
+ snapshot: () => {
910
+ const v = navRef.current;
911
+ return {
912
+ key: v.page.key,
913
+ index: v.page.index,
914
+ total: v.page.total,
915
+ progressPercentage: v.page.progressPercentage,
916
+ canGoBack: v.canGoBack,
917
+ nextCandidates: v.nextCandidates
918
+ };
919
+ }
920
+ };
921
+ return bus.registerNavigation(control);
922
+ }, [bus]);
923
+ useEffect(() => {
924
+ const v = navRef.current;
925
+ bus?.emit("navigation", {
926
+ key: v.page.key,
927
+ index: v.page.index,
928
+ total: v.page.total,
929
+ progressPercentage: v.page.progressPercentage,
930
+ canGoBack: v.canGoBack,
931
+ nextCandidates: v.nextCandidates
932
+ });
933
+ }, [bus, currentKey]);
934
+ const renderKey = toRenderKey(currentKey);
1004
935
  const Current = components.get(renderKey);
1005
936
  const pageEl = createElement(Suspense, { fallback: fallback ?? null }, Current ? createElement(Current) : null);
1006
937
  const content = layout ? createElement(layout, null, pageEl) : pageEl;
@@ -1015,21 +946,104 @@ function useNavigation() {
1015
946
  if (!ctx) throw new Error("useNavigation must be used inside <FunnelView>");
1016
947
  return ctx;
1017
948
  }
949
+ var EMBED_CAPABILITIES = {
950
+ methods: ["nav.snapshot", "nav.goTo", "nav.goNext", "nav.goPrevious", "screenshot"],
951
+ events: ["navigation"]
952
+ };
953
+ function EmbedBridge() {
954
+ const bus = useBusInternal();
955
+ useEffect(() => {
956
+ if (!bus || typeof window === "undefined" || window.parent === window) return;
957
+ const parent = window.parent;
958
+ let disposed = false;
959
+ const send = (msg) => {
960
+ try {
961
+ parent.postMessage({ af: AF_PROTOCOL, source: "af-preview", ...msg }, "*");
962
+ } catch {
963
+ }
964
+ };
965
+ const hello = () => send({ kind: "hello", role: "funnel", protocol: AF_PROTOCOL, capabilities: EMBED_CAPABILITIES });
966
+ const sendSnapshot = () => {
967
+ const s = bus.navigation();
968
+ if (s) send({ kind: "event", event: "navigation", data: s });
969
+ };
970
+ async function invoke(method, params) {
971
+ switch (method) {
972
+ case "nav.snapshot":
973
+ return bus.navigation();
974
+ case "nav.goTo": {
975
+ const key = params?.key;
976
+ if (typeof key !== "string" || !bus.goTo(key)) throw { code: "bad_params", message: `unknown page key: ${String(key)}` };
977
+ return void 0;
978
+ }
979
+ case "nav.goNext":
980
+ bus.goNext();
981
+ return void 0;
982
+ case "nav.goPrevious":
983
+ bus.goPrevious();
984
+ return void 0;
985
+ case "screenshot":
986
+ return await captureScreenshot(params ?? {});
987
+ default:
988
+ throw { code: "unknown_method", message: `unknown method: ${method}` };
989
+ }
990
+ }
991
+ async function onMsg(e) {
992
+ if (e.source !== parent) return;
993
+ const d = e.data;
994
+ if (!d || typeof d !== "object" || d.af !== AF_PROTOCOL || d.source !== "af-host") return;
995
+ if (d.kind === "hello") {
996
+ hello();
997
+ sendSnapshot();
998
+ return;
999
+ }
1000
+ if (d.kind !== "request" || typeof d.id !== "string") return;
1001
+ try {
1002
+ const result = await invoke(d.method, d.params);
1003
+ if (!disposed) send({ kind: "response", id: d.id, ok: true, result });
1004
+ } catch (err) {
1005
+ const e2 = err;
1006
+ const error = e2 && typeof e2.code === "string" ? { code: e2.code, message: String(e2.message ?? e2.code) } : { code: "failed", message: String(e2?.message ?? err) };
1007
+ if (!disposed) send({ kind: "response", id: d.id, ok: false, error });
1008
+ }
1009
+ }
1010
+ window.addEventListener("message", onMsg);
1011
+ const offNav = bus.on("navigation", (data) => {
1012
+ if (!disposed) send({ kind: "event", event: "navigation", data });
1013
+ });
1014
+ hello();
1015
+ sendSnapshot();
1016
+ return () => {
1017
+ disposed = true;
1018
+ offNav();
1019
+ window.removeEventListener("message", onMsg);
1020
+ };
1021
+ }, [bus]);
1022
+ return null;
1023
+ }
1018
1024
  function usePage() {
1019
1025
  return useNavigation().page;
1020
1026
  }
1021
- var symModalId = /* @__PURE__ */ Symbol("AppFunnelModalId");
1027
+ var symModalId = /* @__PURE__ */ Symbol("AppfunnelModalId");
1022
1028
  var initialState = {};
1023
1029
  var ModalStateContext = createContext(initialState);
1024
1030
  var ModalIdContext = createContext(null);
1031
+ var ModalDispatchContext = createContext(null);
1025
1032
  var REGISTRY = {};
1026
1033
  var ALREADY_MOUNTED = {};
1027
1034
  var modalCallbacks = {};
1028
1035
  var hideCallbacks = {};
1029
1036
  var uid = 0;
1030
1037
  var getUid = () => `_af_modal_${uid++}`;
1031
- var dispatch = () => {
1032
- throw new Error("No modal dispatch \u2014 render inside <FunnelProvider> (which mounts the modal runtime).");
1038
+ var runtimeStack = [];
1039
+ var dispatch = (action) => {
1040
+ const top = runtimeStack[runtimeStack.length - 1];
1041
+ if (!top) {
1042
+ throw new Error(
1043
+ "No modal dispatch \u2014 render inside <FunnelProvider> (which mounts the modal runtime)."
1044
+ );
1045
+ }
1046
+ top(action);
1033
1047
  };
1034
1048
  var modalReducer = (state = initialState, action) => {
1035
1049
  const { modalId, args, flags } = action.payload;
@@ -1072,10 +1086,10 @@ function registerModal(id, comp, props) {
1072
1086
  function unregisterModal(id) {
1073
1087
  delete REGISTRY[id];
1074
1088
  }
1075
- function showModal(modal, args) {
1089
+ function showModalVia(d, modal, args) {
1076
1090
  const id = idOf(modal);
1077
1091
  if (typeof modal !== "string" && !REGISTRY[id]) registerModal(id, modal);
1078
- dispatch({ type: "show", payload: { modalId: id, args } });
1092
+ d({ type: "show", payload: { modalId: id, args } });
1079
1093
  if (!modalCallbacks[id]) {
1080
1094
  let res;
1081
1095
  let rej;
@@ -1087,9 +1101,12 @@ function showModal(modal, args) {
1087
1101
  }
1088
1102
  return modalCallbacks[id].promise;
1089
1103
  }
1090
- function hideModal(modal) {
1104
+ function showModal(modal, args) {
1105
+ return showModalVia(dispatch, modal, args);
1106
+ }
1107
+ function hideModalVia(d, modal) {
1091
1108
  const id = idOf(modal);
1092
- dispatch({ type: "hide", payload: { modalId: id } });
1109
+ d({ type: "hide", payload: { modalId: id } });
1093
1110
  delete modalCallbacks[id];
1094
1111
  if (!hideCallbacks[id]) {
1095
1112
  let res;
@@ -1102,40 +1119,63 @@ function hideModal(modal) {
1102
1119
  }
1103
1120
  return hideCallbacks[id].promise;
1104
1121
  }
1105
- function removeModal(modal) {
1122
+ function hideModal(modal) {
1123
+ return hideModalVia(dispatch, modal);
1124
+ }
1125
+ function removeModalVia(d, modal) {
1106
1126
  const id = idOf(modal);
1107
- dispatch({ type: "remove", payload: { modalId: id } });
1127
+ d({ type: "remove", payload: { modalId: id } });
1108
1128
  delete modalCallbacks[id];
1109
1129
  delete hideCallbacks[id];
1110
1130
  }
1111
- function setFlags(id, flags) {
1112
- dispatch({ type: "set-flags", payload: { modalId: id, flags } });
1131
+ function removeModal(modal) {
1132
+ removeModalVia(dispatch, modal);
1133
+ }
1134
+ function setFlags(d, id, flags) {
1135
+ d({ type: "set-flags", payload: { modalId: id, flags } });
1113
1136
  }
1114
1137
  function useModal(modal, args) {
1115
1138
  const modals = useContext(ModalStateContext);
1116
1139
  const contextId = useContext(ModalIdContext);
1140
+ const d = useContext(ModalDispatchContext) ?? dispatch;
1117
1141
  const isComponent = !!modal && typeof modal !== "string";
1118
1142
  const id = modal ? idOf(modal) : contextId;
1119
- if (!id) throw new Error("useModal: no modal id (call inside a defineModal component, or pass an id/component).");
1143
+ if (!id)
1144
+ throw new Error(
1145
+ "useModal: no modal id (call inside a defineModal component, or pass an id/component)."
1146
+ );
1120
1147
  useEffect(() => {
1121
- if (isComponent && !REGISTRY[id]) registerModal(id, modal, args);
1148
+ if (isComponent && !REGISTRY[id])
1149
+ registerModal(id, modal, args);
1122
1150
  }, [isComponent, id, modal, args]);
1123
1151
  const info = modals[id];
1124
- const show = useCallback((a) => showModal(id, a), [id]);
1125
- const hide = useCallback(() => hideModal(id), [id]);
1126
- const remove = useCallback(() => removeModal(id), [id]);
1127
- const resolve = useCallback((v) => {
1128
- modalCallbacks[id]?.resolve(v);
1129
- delete modalCallbacks[id];
1130
- }, [id]);
1131
- const reject = useCallback((v) => {
1132
- modalCallbacks[id]?.reject(v);
1133
- delete modalCallbacks[id];
1134
- }, [id]);
1135
- const resolveHide = useCallback((v) => {
1136
- hideCallbacks[id]?.resolve(v);
1137
- delete hideCallbacks[id];
1138
- }, [id]);
1152
+ const show = useCallback(
1153
+ (a) => showModalVia(d, id, a),
1154
+ [d, id]
1155
+ );
1156
+ const hide = useCallback(() => hideModalVia(d, id), [d, id]);
1157
+ const remove = useCallback(() => removeModalVia(d, id), [d, id]);
1158
+ const resolve = useCallback(
1159
+ (v) => {
1160
+ modalCallbacks[id]?.resolve(v);
1161
+ delete modalCallbacks[id];
1162
+ },
1163
+ [id]
1164
+ );
1165
+ const reject = useCallback(
1166
+ (v) => {
1167
+ modalCallbacks[id]?.reject(v);
1168
+ delete modalCallbacks[id];
1169
+ },
1170
+ [id]
1171
+ );
1172
+ const resolveHide = useCallback(
1173
+ (v) => {
1174
+ hideCallbacks[id]?.resolve(v);
1175
+ delete hideCallbacks[id];
1176
+ },
1177
+ [id]
1178
+ );
1139
1179
  return useMemo(
1140
1180
  () => ({
1141
1181
  id,
@@ -1149,13 +1189,30 @@ function useModal(modal, args) {
1149
1189
  reject,
1150
1190
  resolveHide
1151
1191
  }),
1152
- [id, info?.args, info?.visible, info?.keepMounted, show, hide, remove, resolve, reject, resolveHide]
1192
+ [
1193
+ id,
1194
+ info?.args,
1195
+ info?.visible,
1196
+ info?.keepMounted,
1197
+ show,
1198
+ hide,
1199
+ remove,
1200
+ resolve,
1201
+ reject,
1202
+ resolveHide
1203
+ ]
1153
1204
  );
1154
1205
  }
1155
1206
  function defineModal(Comp) {
1156
- return function Wrapped({ id, defaultVisible, keepMounted, ...props }) {
1207
+ return function Wrapped({
1208
+ id,
1209
+ defaultVisible,
1210
+ keepMounted,
1211
+ ...props
1212
+ }) {
1157
1213
  const { args, show } = useModal(id);
1158
1214
  const modals = useContext(ModalStateContext);
1215
+ const d = useContext(ModalDispatchContext) ?? dispatch;
1159
1216
  const shouldMount = !!modals[id];
1160
1217
  useEffect(() => {
1161
1218
  if (defaultVisible) show();
@@ -1165,8 +1222,8 @@ function defineModal(Comp) {
1165
1222
  };
1166
1223
  }, [id, show, defaultVisible]);
1167
1224
  useEffect(() => {
1168
- if (keepMounted) setFlags(id, { keepMounted: true });
1169
- }, [id, keepMounted]);
1225
+ if (keepMounted) setFlags(d, id, { keepMounted: true });
1226
+ }, [d, id, keepMounted]);
1170
1227
  const delayVisible = modals[id]?.delayVisible;
1171
1228
  useEffect(() => {
1172
1229
  if (delayVisible) show(args);
@@ -1185,17 +1242,37 @@ function ModalRoot() {
1185
1242
  return createElement(
1186
1243
  "div",
1187
1244
  { "data-appfunnel-modal-root": "" },
1188
- ...toRender.map((t) => createElement(t.comp, { key: t.id, id: t.id, ...t.props }))
1245
+ ...toRender.map(
1246
+ (t) => createElement(t.comp, { key: t.id, id: t.id, ...t.props })
1247
+ )
1189
1248
  );
1190
1249
  }
1191
1250
  function ModalRuntime({ children }) {
1192
1251
  const [state, localDispatch] = useReducer(modalReducer, initialState);
1193
- dispatch = localDispatch;
1252
+ useEffect(() => {
1253
+ runtimeStack.push(localDispatch);
1254
+ if (runtimeStack.length > 1) {
1255
+ try {
1256
+ console.warn(
1257
+ `[appfunnel] ${runtimeStack.length} modal runtimes are mounted \u2014 the imperative showModal()/hideModal() API binds to the most recent one. Use the useModal() hook inside each tree instead.`
1258
+ );
1259
+ } catch {
1260
+ }
1261
+ }
1262
+ return () => {
1263
+ const i = runtimeStack.indexOf(localDispatch);
1264
+ if (i !== -1) runtimeStack.splice(i, 1);
1265
+ };
1266
+ }, []);
1194
1267
  return createElement(
1195
- ModalStateContext.Provider,
1196
- { value: state },
1197
- children,
1198
- createElement(ModalRoot, null)
1268
+ ModalDispatchContext.Provider,
1269
+ { value: localDispatch },
1270
+ createElement(
1271
+ ModalStateContext.Provider,
1272
+ { value: state },
1273
+ children,
1274
+ createElement(ModalRoot, null)
1275
+ )
1199
1276
  );
1200
1277
  }
1201
1278
 
@@ -1294,110 +1371,21 @@ function Modal(props) {
1294
1371
  }
1295
1372
 
1296
1373
  // src/commerce/checkout.tsx
1297
- var INLINE_SURFACES = /* @__PURE__ */ new Set([
1298
- "express",
1299
- "element",
1300
- "embedded"
1301
- ]);
1302
- function isInlineSurface(surface) {
1303
- return INLINE_SURFACES.has(surface);
1304
- }
1305
- var ON = { purchase: true, wallets: true, offSession: "reliable" };
1306
- var MANAGED = { purchase: true, wallets: true, offSession: "conditional" };
1307
- var ALL_UPSELLS = ["subscription", "one-time", "upgrade"];
1308
- var PROVIDER_PROFILES = {
1309
- // PSP; you hold the PaymentMethod and fire your own off-session PaymentIntent on
1310
- // the Elements surfaces (reliable). **Managed** Checkout (`embedded`/`redirect`)
1311
- // upsells too, but as a direct charge against Stripe's vaulted card → conditional.
1312
- // No provider "popup" surface.
1313
- stripe: {
1314
- isMoR: false,
1315
- offSessionUpsells: ALL_UPSELLS,
1316
- tokenModel: "merchant",
1317
- orchestrator: false,
1318
- surfaces: { express: ON, element: ON, sheet: ON, embedded: MANAGED, redirect: MANAGED }
1319
- },
1320
- // Merchant-of-Record; card entry is always Paddle's frame → no own element/sheet/
1321
- // express. Off-session via subscription one-time charge / collection_mode, but
1322
- // **no 2nd concurrent subscription** (upgrade + one-time only), provider-initiated.
1323
- paddle: {
1324
- isMoR: true,
1325
- offSessionUpsells: ["one-time", "upgrade"],
1326
- tokenModel: "provider",
1327
- orchestrator: false,
1328
- surfaces: { embedded: ON, popup: ON, redirect: ON }
1329
- },
1330
- // Merchant-of-Record; iframe-only (no raw card field). Off-session works
1331
- // (provider-initiated, async via webhooks). `sheet` = our chrome hosting their embed.
1332
- whop: {
1333
- isMoR: true,
1334
- offSessionUpsells: ALL_UPSELLS,
1335
- tokenModel: "provider",
1336
- orchestrator: false,
1337
- surfaces: { embedded: ON, redirect: ON, sheet: ON }
1338
- },
1339
- // Pure orchestrator (PSP-side); Headless renders express/element/sheet/embedded.
1340
- // No hosted redirect page, no popup checkout. Off-session is PSP-conditional.
1341
- primer: {
1342
- isMoR: false,
1343
- offSessionUpsells: ALL_UPSELLS,
1344
- tokenModel: "merchant",
1345
- orchestrator: true,
1346
- surfaces: { express: ON, element: ON, sheet: ON, embedded: ON }
1347
- },
1348
- // PSP **and** orchestrator (own acquiring + connectors routing across Stripe/Adyen/
1349
- // PayPal). One Payment Form (element/sheet/embedded) + hosted page + wallet button.
1350
- // Strong native off-session (Recurring API). No popup.
1351
- solidgate: {
1352
- isMoR: false,
1353
- offSessionUpsells: ALL_UPSELLS,
1354
- tokenModel: "merchant",
1355
- orchestrator: true,
1356
- surfaces: { express: ON, element: ON, sheet: ON, embedded: ON, redirect: ON }
1357
- }
1358
- };
1359
- function surfacesFor(provider) {
1360
- return Object.keys(PROVIDER_PROFILES[provider]?.surfaces ?? {});
1361
- }
1362
- function isMerchantOfRecord(provider) {
1363
- return PROVIDER_PROFILES[provider]?.isMoR ?? false;
1364
- }
1365
- function isOrchestrator(provider) {
1366
- return PROVIDER_PROFILES[provider]?.orchestrator ?? false;
1367
- }
1368
- function validateCheckout(provider, surface) {
1369
- const cap = PROVIDER_PROFILES[provider]?.surfaces[surface];
1370
- if (!cap) return { ok: false, reason: `${provider} has no '${surface}' surface` };
1371
- return cap.purchase ? { ok: true } : { ok: false, reason: `'${surface}' can't take a purchase on ${provider}` };
1372
- }
1373
- function validateUpsell(provider, paywallSurface, kind = "subscription") {
1374
- const profile = PROVIDER_PROFILES[provider];
1375
- if (!profile) return { ok: false, reason: `unknown provider '${provider}'` };
1376
- if (!profile.offSessionUpsells.includes(kind)) {
1377
- const can = profile.offSessionUpsells.length ? ` (it supports: ${profile.offSessionUpsells.join(", ")})` : "";
1378
- return { ok: false, reason: `${provider} can't charge a '${kind}' upsell off-session${can}` };
1379
- }
1380
- const cap = profile.surfaces[paywallSurface];
1381
- if (!cap) return { ok: false, reason: `${provider} has no '${paywallSurface}' surface to upsell after` };
1382
- if (cap.offSession === "none") {
1383
- return { ok: false, reason: `a purchase on '${paywallSurface}' retains no chargeable method for an upsell` };
1384
- }
1385
- if (cap.offSession === "conditional") {
1386
- return { ok: true, note: `off-session upsell after ${provider} '${paywallSurface}' (managed checkout) may decline \u2014 prefer element/sheet/express` };
1387
- }
1388
- return { ok: true };
1389
- }
1390
- function checkoutError(category, message, extra = {}) {
1391
- const retryable = extra.retryable ?? (category === "authentication_required" || category === "requires_payment_method" || category === "processing_error");
1392
- return { category, message, declineCode: extra.declineCode, retryable };
1393
- }
1394
1374
  function createMockDriver(provider = "stripe", options = {}) {
1395
- const success = options.result ?? { ok: true, amountMinor: 0, currency: "USD" };
1396
- const offError = typeof options.failOffSession === "object" ? options.failOffSession : checkoutError("authentication_required", "off-session charge declined (mock)");
1375
+ const success = options.result ?? {
1376
+ ok: true,
1377
+ amountMinor: 0,
1378
+ currency: "USD"
1379
+ };
1380
+ const offError = typeof options.failOffSession === "object" ? options.failOffSession : checkoutError(
1381
+ "authentication_required",
1382
+ "off-session charge declined (mock)"
1383
+ );
1397
1384
  return {
1398
1385
  provider,
1399
1386
  start: (req) => {
1400
- if (options.failOffSession && !req.surface) return Promise.resolve({ ok: false, error: offError });
1387
+ if (options.failOffSession && !req.surface)
1388
+ return Promise.resolve({ ok: false, error: offError });
1401
1389
  return Promise.resolve(success);
1402
1390
  },
1403
1391
  renderInline: (req, cb) => createElement(
@@ -1405,10 +1393,37 @@ function createMockDriver(provider = "stripe", options = {}) {
1405
1393
  {
1406
1394
  type: "button",
1407
1395
  "data-mock-surface": req.surface,
1408
- onClick: () => success.ok ? cb.onSuccess(success) : cb.onError(success.error ?? checkoutError("processing_error", "failed (mock)"))
1396
+ onClick: () => {
1397
+ cb.onStart?.();
1398
+ if (success.ok) cb.onSuccess(success);
1399
+ else
1400
+ cb.onError(
1401
+ success.error ?? checkoutError(
1402
+ "processing_error",
1403
+ "failed (mock)"
1404
+ )
1405
+ );
1406
+ }
1409
1407
  },
1410
1408
  `Pay (mock ${req.surface})`
1411
- )
1409
+ ),
1410
+ resume: () => Promise.resolve(success)
1411
+ };
1412
+ }
1413
+ function driverWithEmail(driver, getEmail) {
1414
+ const enrich = (req) => req.email !== void 0 ? req : { ...req, email: getEmail() };
1415
+ return {
1416
+ ...driver,
1417
+ start: (req) => driver.start(enrich(req)),
1418
+ renderInline: driver.renderInline ? (req, cb) => driver.renderInline(enrich(req), cb) : void 0
1419
+ };
1420
+ }
1421
+ function driverWithCatalog(driver, catalog) {
1422
+ const enrich = (req) => req.catalogKey !== void 0 ? req : { ...req, catalogKey: catalog.get(req.product)?.catalogKey };
1423
+ return {
1424
+ ...driver,
1425
+ start: (req) => driver.start(enrich(req)),
1426
+ renderInline: driver.renderInline ? (req, cb) => driver.renderInline(enrich(req), cb) : void 0
1412
1427
  };
1413
1428
  }
1414
1429
  var DriverContext = createContext(null);
@@ -1425,132 +1440,343 @@ var FALLBACK_DRIVER = createMockDriver();
1425
1440
  function useCheckout(opts = {}) {
1426
1441
  const driver = useDriver();
1427
1442
  const tracker = useTrackerRef();
1443
+ const catalog = useCatalog();
1428
1444
  const nav = useNavigation();
1429
1445
  const [status, setStatus] = useState("idle");
1430
1446
  const [error, setError] = useState(null);
1431
1447
  const intent = opts.intent ?? "purchase";
1432
1448
  const kind = opts.kind;
1433
1449
  const advance = opts.advanceOnSuccess ?? true;
1450
+ const productFacts = useCallback(
1451
+ (product) => {
1452
+ const resolved = product ? catalog.get(product) : void 0;
1453
+ return {
1454
+ productId: resolved?.catalogKey ?? product,
1455
+ amount: resolved ? resolved.priceMinor / 100 : void 0,
1456
+ currency: resolved?.currency,
1457
+ isSubscription: resolved ? resolved.period !== "one-time" : kind === "subscription" || kind === "upgrade"
1458
+ };
1459
+ },
1460
+ [catalog, kind]
1461
+ );
1462
+ const recovering = useRef(false);
1463
+ const failRef = useRef(
1464
+ () => {
1465
+ }
1466
+ );
1467
+ const attemptRef = useRef(async () => ({ ok: false }));
1434
1468
  const succeed = useCallback(
1435
- (result) => {
1469
+ (result, product) => {
1436
1470
  setStatus("success");
1471
+ const { productId, isSubscription } = productFacts(product);
1472
+ const amount = result.amountMinor != null ? result.amountMinor / 100 : void 0;
1437
1473
  tracker.track("purchase.complete", {
1438
- amount: result.amountMinor != null ? result.amountMinor / 100 : void 0,
1474
+ amount,
1439
1475
  currency: result.currency,
1476
+ productId,
1440
1477
  eventId: result.eventId
1441
1478
  // server-minted (browser↔CAPI dedup); undefined if none
1442
1479
  });
1480
+ if (isSubscription) {
1481
+ tracker.track("subscription.created", {
1482
+ amount,
1483
+ currency: result.currency,
1484
+ productId,
1485
+ eventId: result.eventId
1486
+ });
1487
+ }
1443
1488
  opts.onSuccess?.(result);
1444
1489
  if (advance) nav.next();
1445
1490
  },
1446
- [tracker, nav, advance, opts]
1447
- );
1448
- const fail = useCallback(
1449
- (err, product) => {
1450
- setStatus(err.category === "authentication_required" ? "requires_action" : "error");
1451
- setError(err);
1452
- tracker.track("checkout.failed", {
1453
- category: err.category,
1454
- declineCode: err.declineCode,
1455
- productId: product,
1456
- intent
1457
- });
1458
- opts.onError?.(err);
1459
- },
1460
- [tracker, intent, opts]
1491
+ [tracker, nav, advance, opts, productFacts]
1461
1492
  );
1493
+ const succeedRef = useRef(succeed);
1494
+ succeedRef.current = succeed;
1462
1495
  const begin = useCallback(
1463
1496
  (product, surface) => {
1464
1497
  setStatus("loading");
1465
1498
  setError(null);
1466
- tracker.track("checkout.start", { productId: product, surface });
1499
+ const { productId, amount, currency } = productFacts(product);
1500
+ tracker.track("checkout.start", {
1501
+ productId,
1502
+ amount,
1503
+ currency,
1504
+ surface,
1505
+ eventId: newEventId()
1506
+ });
1467
1507
  },
1468
- [tracker]
1508
+ [tracker, productFacts]
1469
1509
  );
1470
- const paymentAdded = useCallback(() => tracker.track("checkout.payment_added", {}), [tracker]);
1471
- const open = useCallback(
1472
- async (product, surface) => {
1473
- begin(product, surface);
1474
- try {
1475
- const result = await driver.start({ product, intent, surface, kind });
1476
- if (result.ok) succeed(result);
1477
- else fail(result.error ?? checkoutError("unknown", "Checkout failed"), product);
1478
- return result;
1479
- } catch (e) {
1480
- const err = checkoutError("processing_error", e instanceof Error ? e.message : "Checkout failed");
1481
- fail(err, product);
1482
- return { ok: false, error: err };
1483
- }
1510
+ const paymentAdded = useCallback(
1511
+ (product) => {
1512
+ const { productId, amount, currency } = productFacts(product);
1513
+ tracker.track("checkout.payment_added", {
1514
+ productId,
1515
+ amount,
1516
+ currency,
1517
+ eventId: newEventId()
1518
+ });
1484
1519
  },
1485
- [driver, intent, kind, begin, succeed, fail]
1486
- );
1487
- const callbacksFor = useCallback(
1488
- (product, surface) => ({
1489
- onStart: () => begin(product, surface),
1490
- onSuccess: (result) => succeed(result),
1491
- onError: (err) => fail(err, product),
1492
- onPaymentAdded: () => paymentAdded()
1493
- }),
1494
- [begin, succeed, fail, paymentAdded]
1520
+ [tracker, productFacts]
1495
1521
  );
1496
- const reset = useCallback(() => {
1497
- setStatus("idle");
1498
- setError(null);
1499
- }, []);
1500
- return { open, status, error, isLoading: status === "loading", reset, callbacksFor };
1501
- }
1502
- var CheckoutSheet = defineModal(({ product, surface, intent, onSuccess, onError }) => {
1503
- const driver = useDriver();
1522
+ const hostInSheet = (surface) => surface === "sheet" || isInlineSurface(surface) && !!PROVIDER_PROFILES[driver.provider]?.surfaces.sheet;
1523
+ const startRecovery = (product, surface) => {
1524
+ recovering.current = true;
1525
+ if (hostInSheet(surface)) {
1526
+ begin(product, "sheet");
1527
+ void showModal(CheckoutSheet, {
1528
+ product,
1529
+ surface: "sheet",
1530
+ intent,
1531
+ kind,
1532
+ onSuccess: (r) => succeedRef.current(r, product),
1533
+ onError: (e) => failRef.current(e, product),
1534
+ onPaymentAdded: () => paymentAdded(product)
1535
+ });
1536
+ } else {
1537
+ void attemptRef.current(product, surface);
1538
+ }
1539
+ };
1540
+ failRef.current = (err, product) => {
1541
+ setStatus(
1542
+ err.category === "authentication_required" ? "requires_action" : "error"
1543
+ );
1544
+ setError(err);
1545
+ tracker.track("checkout.failed", {
1546
+ category: err.category,
1547
+ declineCode: err.declineCode,
1548
+ productId: product,
1549
+ intent
1550
+ });
1551
+ opts.onError?.(err);
1552
+ if (err.category === "canceled" && !opts.onFailed?.canceled) return;
1553
+ if (product && !recovering.current && opts.recoverySurface && err.category !== "canceled") {
1554
+ startRecovery(product, opts.recoverySurface);
1555
+ return;
1556
+ }
1557
+ let route = opts.onFailed?.[err.category] ?? (err.category === "canceled" ? void 0 : opts.onFailed?.default);
1558
+ if (route && typeof route === "object" && "fallback" in route && recovering.current) {
1559
+ const dflt = opts.onFailed?.default;
1560
+ route = dflt && dflt !== route && !(typeof dflt === "object" && "fallback" in dflt) ? dflt : void 0;
1561
+ }
1562
+ if (!route) return;
1563
+ if (typeof route === "function") route(err);
1564
+ else if ("go" in route) nav.go(route.go);
1565
+ else if ("modal" in route) {
1566
+ const props = {
1567
+ error: err,
1568
+ product,
1569
+ go: (pageKey) => nav.go(pageKey),
1570
+ retry: (surface) => {
1571
+ if (!product) return;
1572
+ recovering.current = true;
1573
+ if (!surface) {
1574
+ if (intent === "upsell")
1575
+ void attemptRef.current(product);
1576
+ else
1577
+ startRecovery(
1578
+ product,
1579
+ PROVIDER_PROFILES[driver.provider]?.surfaces.sheet ? "sheet" : "popup"
1580
+ );
1581
+ return;
1582
+ }
1583
+ startRecovery(product, surface);
1584
+ }
1585
+ };
1586
+ void showModal(
1587
+ route.modal,
1588
+ props
1589
+ );
1590
+ } else if ("fallback" in route) {
1591
+ if (product && !recovering.current)
1592
+ startRecovery(product, route.fallback);
1593
+ }
1594
+ };
1595
+ attemptRef.current = async (product, surface) => {
1596
+ begin(product, surface);
1597
+ try {
1598
+ const result = await driver.start({
1599
+ product,
1600
+ intent,
1601
+ surface,
1602
+ kind
1603
+ });
1604
+ if (result.ok) succeedRef.current(result, product);
1605
+ else
1606
+ failRef.current(
1607
+ result.error ?? checkoutError("unknown", "Checkout failed"),
1608
+ product
1609
+ );
1610
+ return result;
1611
+ } catch (e) {
1612
+ const err = checkoutError(
1613
+ "processing_error",
1614
+ e instanceof Error ? e.message : "Checkout failed"
1615
+ );
1616
+ failRef.current(err, product);
1617
+ return { ok: false, error: err };
1618
+ }
1619
+ };
1620
+ const open = useCallback(
1621
+ async (product, surface) => {
1622
+ recovering.current = false;
1623
+ return attemptRef.current(product, surface);
1624
+ },
1625
+ []
1626
+ );
1627
+ const callbacksFor = useCallback(
1628
+ (product, surface) => ({
1629
+ onStart: () => {
1630
+ recovering.current = false;
1631
+ begin(product, surface);
1632
+ },
1633
+ onSuccess: (result) => succeedRef.current(result, product),
1634
+ onError: (err) => failRef.current(err, product),
1635
+ onPaymentAdded: () => paymentAdded(product)
1636
+ }),
1637
+ [begin, paymentAdded]
1638
+ );
1639
+ const reset = useCallback(() => {
1640
+ recovering.current = false;
1641
+ setStatus("idle");
1642
+ setError(null);
1643
+ }, []);
1644
+ return {
1645
+ open,
1646
+ status,
1647
+ error,
1648
+ isLoading: status === "loading",
1649
+ reset,
1650
+ callbacksFor
1651
+ };
1652
+ }
1653
+ var CheckoutSheet = defineModal(({ product, surface, intent, kind, onSuccess, onError, onPaymentAdded }) => {
1654
+ const driver = useDriver();
1504
1655
  const modal = useModal();
1656
+ const resolved = useRef(false);
1657
+ const onErrorRef = useRef(onError);
1658
+ onErrorRef.current = onError;
1659
+ const hostable = !!driver.renderInline;
1660
+ useEffect(() => {
1661
+ if (!hostable) {
1662
+ resolved.current = true;
1663
+ onErrorRef.current(
1664
+ checkoutError(
1665
+ "processing_error",
1666
+ "This checkout driver has no inline surface to host in the sheet"
1667
+ )
1668
+ );
1669
+ void modal.hide();
1670
+ modal.remove();
1671
+ return;
1672
+ }
1673
+ return () => {
1674
+ if (!resolved.current) {
1675
+ onErrorRef.current(
1676
+ checkoutError(
1677
+ "canceled",
1678
+ "Checkout was dismissed before completing"
1679
+ )
1680
+ );
1681
+ }
1682
+ };
1683
+ }, []);
1505
1684
  if (!driver.renderInline) return null;
1685
+ const close = () => {
1686
+ resolved.current = true;
1687
+ void modal.hide();
1688
+ modal.remove();
1689
+ };
1506
1690
  return createElement(Sheet, {
1507
1691
  children: driver.renderInline(
1508
- { product, intent, surface },
1692
+ { product, intent, surface, kind },
1509
1693
  {
1510
1694
  onSuccess: (r) => {
1511
- modal.hide();
1695
+ close();
1512
1696
  onSuccess(r);
1513
1697
  },
1514
1698
  onError: (e) => {
1515
- modal.hide();
1699
+ close();
1516
1700
  onError(e);
1517
- }
1701
+ },
1702
+ onPaymentAdded
1518
1703
  }
1519
1704
  )
1520
1705
  });
1521
1706
  });
1522
- function trigger(onClick, { asChild, children, className, disabled }) {
1707
+ function trigger(onClick, {
1708
+ asChild,
1709
+ children,
1710
+ className,
1711
+ disabled
1712
+ }) {
1523
1713
  if (asChild && isValidElement(children)) {
1524
1714
  const child = children;
1525
1715
  return cloneElement(child, { onClick });
1526
1716
  }
1527
- return createElement("button", { type: "button", className, disabled, onClick }, children ?? "Continue");
1717
+ return createElement(
1718
+ "button",
1719
+ { type: "button", className, disabled, onClick },
1720
+ children ?? "Continue"
1721
+ );
1722
+ }
1723
+ function detectDeviceClass() {
1724
+ if (typeof window === "undefined") return "desktop";
1725
+ const ua = navigator.userAgent?.toLowerCase() ?? "";
1726
+ if (/mobile|android(?!.*tablet)|iphone|ipod/.test(ua)) return "mobile";
1727
+ try {
1728
+ if (window.matchMedia?.("(pointer: coarse)").matches && window.innerWidth > 0 && window.innerWidth < 768) {
1729
+ return "mobile";
1730
+ }
1731
+ } catch {
1732
+ }
1733
+ return "desktop";
1528
1734
  }
1529
1735
  function Checkout({
1530
- product,
1531
- surface,
1736
+ offering,
1737
+ surface: surfacePreference,
1532
1738
  asChild,
1533
1739
  children,
1534
1740
  className,
1535
1741
  ...options
1536
1742
  }) {
1743
+ const product = offering;
1537
1744
  const driver = useDriver();
1745
+ const [surface] = useState(
1746
+ () => resolveSurface(driver.provider, surfacePreference, detectDeviceClass())
1747
+ );
1538
1748
  const checkout = useCheckout(options);
1539
1749
  const intent = options.intent ?? "purchase";
1540
1750
  if (isInlineSurface(surface)) {
1541
1751
  return driver.renderInline ? createElement(
1542
1752
  "div",
1543
1753
  { key: product, className },
1544
- driver.renderInline({ product, intent, surface }, checkout.callbacksFor(product, surface))
1754
+ driver.renderInline(
1755
+ { product, intent, surface },
1756
+ checkout.callbacksFor(product, surface)
1757
+ )
1545
1758
  ) : null;
1546
1759
  }
1547
1760
  if (surface === "sheet") {
1548
1761
  const cb = checkout.callbacksFor(product, surface);
1549
1762
  const open = () => {
1550
1763
  cb.onStart();
1551
- showModal(CheckoutSheet, { product, surface, intent, onSuccess: cb.onSuccess, onError: cb.onError });
1764
+ showModal(CheckoutSheet, {
1765
+ product,
1766
+ surface,
1767
+ intent,
1768
+ kind: options.kind,
1769
+ onSuccess: cb.onSuccess,
1770
+ onError: cb.onError,
1771
+ onPaymentAdded: cb.onPaymentAdded
1772
+ });
1552
1773
  };
1553
- return trigger(open, { asChild, children, className, disabled: checkout.isLoading });
1774
+ return trigger(open, {
1775
+ asChild,
1776
+ children,
1777
+ className,
1778
+ disabled: checkout.isLoading
1779
+ });
1554
1780
  }
1555
1781
  return trigger(() => checkout.open(product, surface), {
1556
1782
  asChild,
@@ -1559,32 +1785,1150 @@ function Checkout({
1559
1785
  disabled: checkout.isLoading
1560
1786
  });
1561
1787
  }
1562
- function Upsell({ product, fallback, asChild, children, className, ...options }) {
1563
- const checkout = useCheckout({ ...options, intent: "upsell" });
1564
- const start = async () => {
1565
- const result = await checkout.open(product);
1566
- if (result.ok || !fallback) return;
1567
- const cb = checkout.callbacksFor(product, fallback);
1568
- if (fallback === "sheet") {
1569
- cb.onStart();
1570
- showModal(CheckoutSheet, { product, surface: "sheet", intent: "upsell", onSuccess: cb.onSuccess, onError: cb.onError });
1788
+ function Upsell({
1789
+ offering,
1790
+ fallback,
1791
+ asChild,
1792
+ children,
1793
+ className,
1794
+ ...options
1795
+ }) {
1796
+ const product = offering;
1797
+ const checkout = useCheckout({
1798
+ ...options,
1799
+ intent: "upsell",
1800
+ recoverySurface: fallback ?? options.recoverySurface
1801
+ });
1802
+ return trigger(
1803
+ () => void checkout.open(product),
1804
+ {
1805
+ asChild,
1806
+ children: children ?? "Add to my plan",
1807
+ className,
1808
+ disabled: checkout.isLoading
1809
+ }
1810
+ );
1811
+ }
1812
+ var consumedResumes = /* @__PURE__ */ new Set();
1813
+ function CheckoutResume(options = {}) {
1814
+ const driver = useDriver();
1815
+ const checkout = useCheckout(options);
1816
+ const callbacksRef = useRef(checkout.callbacksFor);
1817
+ callbacksRef.current = checkout.callbacksFor;
1818
+ useEffect(() => {
1819
+ if (typeof window === "undefined" || !driver.resume) return;
1820
+ const url = new URL(window.location.href);
1821
+ const correlationId = url.searchParams.get("af_checkout");
1822
+ if (!correlationId || consumedResumes.has(correlationId)) return;
1823
+ consumedResumes.add(correlationId);
1824
+ void driver.resume(correlationId).then((result) => {
1825
+ const cb = callbacksRef.current();
1826
+ if (result.ok) {
1827
+ url.searchParams.delete("af_checkout");
1828
+ window.history.replaceState(
1829
+ window.history.state,
1830
+ "",
1831
+ url.toString()
1832
+ );
1833
+ cb.onSuccess(result);
1834
+ } else {
1835
+ cb.onError(
1836
+ result.error ?? checkoutError(
1837
+ "unknown",
1838
+ "The checkout could not be verified"
1839
+ )
1840
+ );
1841
+ }
1842
+ });
1843
+ }, []);
1844
+ return null;
1845
+ }
1846
+ function __insertCSS(code) {
1847
+ if (typeof document == "undefined") return;
1848
+ let head = document.head || document.getElementsByTagName("head")[0];
1849
+ let style = document.createElement("style");
1850
+ style.type = "text/css";
1851
+ head.appendChild(style);
1852
+ style.styleSheet ? style.styleSheet.cssText = code : style.appendChild(document.createTextNode(code));
1853
+ }
1854
+ var getAsset = (type) => {
1855
+ switch (type) {
1856
+ case "success":
1857
+ return SuccessIcon;
1858
+ case "info":
1859
+ return InfoIcon;
1860
+ case "warning":
1861
+ return WarningIcon;
1862
+ case "error":
1863
+ return ErrorIcon;
1864
+ default:
1865
+ return null;
1866
+ }
1867
+ };
1868
+ var bars = Array(12).fill(0);
1869
+ var Loader = ({ visible, className }) => {
1870
+ return /* @__PURE__ */ React.createElement("div", {
1871
+ className: [
1872
+ "sonner-loading-wrapper",
1873
+ className
1874
+ ].filter(Boolean).join(" "),
1875
+ "data-visible": visible
1876
+ }, /* @__PURE__ */ React.createElement("div", {
1877
+ className: "sonner-spinner"
1878
+ }, bars.map((_, i) => /* @__PURE__ */ React.createElement("div", {
1879
+ className: "sonner-loading-bar",
1880
+ key: `spinner-bar-${i}`
1881
+ }))));
1882
+ };
1883
+ var SuccessIcon = /* @__PURE__ */ React.createElement("svg", {
1884
+ xmlns: "http://www.w3.org/2000/svg",
1885
+ viewBox: "0 0 20 20",
1886
+ fill: "currentColor",
1887
+ height: "20",
1888
+ width: "20"
1889
+ }, /* @__PURE__ */ React.createElement("path", {
1890
+ fillRule: "evenodd",
1891
+ d: "M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z",
1892
+ clipRule: "evenodd"
1893
+ }));
1894
+ var WarningIcon = /* @__PURE__ */ React.createElement("svg", {
1895
+ xmlns: "http://www.w3.org/2000/svg",
1896
+ viewBox: "0 0 24 24",
1897
+ fill: "currentColor",
1898
+ height: "20",
1899
+ width: "20"
1900
+ }, /* @__PURE__ */ React.createElement("path", {
1901
+ fillRule: "evenodd",
1902
+ d: "M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z",
1903
+ clipRule: "evenodd"
1904
+ }));
1905
+ var InfoIcon = /* @__PURE__ */ React.createElement("svg", {
1906
+ xmlns: "http://www.w3.org/2000/svg",
1907
+ viewBox: "0 0 20 20",
1908
+ fill: "currentColor",
1909
+ height: "20",
1910
+ width: "20"
1911
+ }, /* @__PURE__ */ React.createElement("path", {
1912
+ fillRule: "evenodd",
1913
+ d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z",
1914
+ clipRule: "evenodd"
1915
+ }));
1916
+ var ErrorIcon = /* @__PURE__ */ React.createElement("svg", {
1917
+ xmlns: "http://www.w3.org/2000/svg",
1918
+ viewBox: "0 0 20 20",
1919
+ fill: "currentColor",
1920
+ height: "20",
1921
+ width: "20"
1922
+ }, /* @__PURE__ */ React.createElement("path", {
1923
+ fillRule: "evenodd",
1924
+ d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z",
1925
+ clipRule: "evenodd"
1926
+ }));
1927
+ var CloseIcon = /* @__PURE__ */ React.createElement("svg", {
1928
+ xmlns: "http://www.w3.org/2000/svg",
1929
+ width: "12",
1930
+ height: "12",
1931
+ viewBox: "0 0 24 24",
1932
+ fill: "none",
1933
+ stroke: "currentColor",
1934
+ strokeWidth: "1.5",
1935
+ strokeLinecap: "round",
1936
+ strokeLinejoin: "round"
1937
+ }, /* @__PURE__ */ React.createElement("line", {
1938
+ x1: "18",
1939
+ y1: "6",
1940
+ x2: "6",
1941
+ y2: "18"
1942
+ }), /* @__PURE__ */ React.createElement("line", {
1943
+ x1: "6",
1944
+ y1: "6",
1945
+ x2: "18",
1946
+ y2: "18"
1947
+ }));
1948
+ var useIsDocumentHidden = () => {
1949
+ const [isDocumentHidden, setIsDocumentHidden] = React.useState(document.hidden);
1950
+ React.useEffect(() => {
1951
+ const callback = () => {
1952
+ setIsDocumentHidden(document.hidden);
1953
+ };
1954
+ document.addEventListener("visibilitychange", callback);
1955
+ return () => window.removeEventListener("visibilitychange", callback);
1956
+ }, []);
1957
+ return isDocumentHidden;
1958
+ };
1959
+ var toastsCounter = 1;
1960
+ var Observer = class {
1961
+ constructor() {
1962
+ this.subscribe = (subscriber) => {
1963
+ this.subscribers.push(subscriber);
1964
+ return () => {
1965
+ const index = this.subscribers.indexOf(subscriber);
1966
+ this.subscribers.splice(index, 1);
1967
+ };
1968
+ };
1969
+ this.publish = (data) => {
1970
+ this.subscribers.forEach((subscriber) => subscriber(data));
1971
+ };
1972
+ this.addToast = (data) => {
1973
+ this.publish(data);
1974
+ this.toasts = [
1975
+ ...this.toasts,
1976
+ data
1977
+ ];
1978
+ };
1979
+ this.create = (data) => {
1980
+ var _data_id;
1981
+ const { message, ...rest } = data;
1982
+ const id = typeof (data == null ? void 0 : data.id) === "number" || ((_data_id = data.id) == null ? void 0 : _data_id.length) > 0 ? data.id : toastsCounter++;
1983
+ const alreadyExists = this.toasts.find((toast2) => {
1984
+ return toast2.id === id;
1985
+ });
1986
+ const dismissible = data.dismissible === void 0 ? true : data.dismissible;
1987
+ if (this.dismissedToasts.has(id)) {
1988
+ this.dismissedToasts.delete(id);
1989
+ }
1990
+ if (alreadyExists) {
1991
+ this.toasts = this.toasts.map((toast2) => {
1992
+ if (toast2.id === id) {
1993
+ this.publish({
1994
+ ...toast2,
1995
+ ...data,
1996
+ id,
1997
+ title: message
1998
+ });
1999
+ return {
2000
+ ...toast2,
2001
+ ...data,
2002
+ id,
2003
+ dismissible,
2004
+ title: message
2005
+ };
2006
+ }
2007
+ return toast2;
2008
+ });
2009
+ } else {
2010
+ this.addToast({
2011
+ title: message,
2012
+ ...rest,
2013
+ dismissible,
2014
+ id
2015
+ });
2016
+ }
2017
+ return id;
2018
+ };
2019
+ this.dismiss = (id) => {
2020
+ if (id) {
2021
+ this.dismissedToasts.add(id);
2022
+ requestAnimationFrame(() => this.subscribers.forEach((subscriber) => subscriber({
2023
+ id,
2024
+ dismiss: true
2025
+ })));
2026
+ } else {
2027
+ this.toasts.forEach((toast2) => {
2028
+ this.subscribers.forEach((subscriber) => subscriber({
2029
+ id: toast2.id,
2030
+ dismiss: true
2031
+ }));
2032
+ });
2033
+ }
2034
+ return id;
2035
+ };
2036
+ this.message = (message, data) => {
2037
+ return this.create({
2038
+ ...data,
2039
+ message
2040
+ });
2041
+ };
2042
+ this.error = (message, data) => {
2043
+ return this.create({
2044
+ ...data,
2045
+ message,
2046
+ type: "error"
2047
+ });
2048
+ };
2049
+ this.success = (message, data) => {
2050
+ return this.create({
2051
+ ...data,
2052
+ type: "success",
2053
+ message
2054
+ });
2055
+ };
2056
+ this.info = (message, data) => {
2057
+ return this.create({
2058
+ ...data,
2059
+ type: "info",
2060
+ message
2061
+ });
2062
+ };
2063
+ this.warning = (message, data) => {
2064
+ return this.create({
2065
+ ...data,
2066
+ type: "warning",
2067
+ message
2068
+ });
2069
+ };
2070
+ this.loading = (message, data) => {
2071
+ return this.create({
2072
+ ...data,
2073
+ type: "loading",
2074
+ message
2075
+ });
2076
+ };
2077
+ this.promise = (promise, data) => {
2078
+ if (!data) {
2079
+ return;
2080
+ }
2081
+ let id = void 0;
2082
+ if (data.loading !== void 0) {
2083
+ id = this.create({
2084
+ ...data,
2085
+ promise,
2086
+ type: "loading",
2087
+ message: data.loading,
2088
+ description: typeof data.description !== "function" ? data.description : void 0
2089
+ });
2090
+ }
2091
+ const p = Promise.resolve(promise instanceof Function ? promise() : promise);
2092
+ let shouldDismiss = id !== void 0;
2093
+ let result;
2094
+ const originalPromise = p.then(async (response) => {
2095
+ result = [
2096
+ "resolve",
2097
+ response
2098
+ ];
2099
+ const isReactElementResponse = React.isValidElement(response);
2100
+ if (isReactElementResponse) {
2101
+ shouldDismiss = false;
2102
+ this.create({
2103
+ id,
2104
+ type: "default",
2105
+ message: response
2106
+ });
2107
+ } else if (isHttpResponse(response) && !response.ok) {
2108
+ shouldDismiss = false;
2109
+ const promiseData = typeof data.error === "function" ? await data.error(`HTTP error! status: ${response.status}`) : data.error;
2110
+ const description = typeof data.description === "function" ? await data.description(`HTTP error! status: ${response.status}`) : data.description;
2111
+ const isExtendedResult = typeof promiseData === "object" && !React.isValidElement(promiseData);
2112
+ const toastSettings = isExtendedResult ? promiseData : {
2113
+ message: promiseData
2114
+ };
2115
+ this.create({
2116
+ id,
2117
+ type: "error",
2118
+ description,
2119
+ ...toastSettings
2120
+ });
2121
+ } else if (response instanceof Error) {
2122
+ shouldDismiss = false;
2123
+ const promiseData = typeof data.error === "function" ? await data.error(response) : data.error;
2124
+ const description = typeof data.description === "function" ? await data.description(response) : data.description;
2125
+ const isExtendedResult = typeof promiseData === "object" && !React.isValidElement(promiseData);
2126
+ const toastSettings = isExtendedResult ? promiseData : {
2127
+ message: promiseData
2128
+ };
2129
+ this.create({
2130
+ id,
2131
+ type: "error",
2132
+ description,
2133
+ ...toastSettings
2134
+ });
2135
+ } else if (data.success !== void 0) {
2136
+ shouldDismiss = false;
2137
+ const promiseData = typeof data.success === "function" ? await data.success(response) : data.success;
2138
+ const description = typeof data.description === "function" ? await data.description(response) : data.description;
2139
+ const isExtendedResult = typeof promiseData === "object" && !React.isValidElement(promiseData);
2140
+ const toastSettings = isExtendedResult ? promiseData : {
2141
+ message: promiseData
2142
+ };
2143
+ this.create({
2144
+ id,
2145
+ type: "success",
2146
+ description,
2147
+ ...toastSettings
2148
+ });
2149
+ }
2150
+ }).catch(async (error) => {
2151
+ result = [
2152
+ "reject",
2153
+ error
2154
+ ];
2155
+ if (data.error !== void 0) {
2156
+ shouldDismiss = false;
2157
+ const promiseData = typeof data.error === "function" ? await data.error(error) : data.error;
2158
+ const description = typeof data.description === "function" ? await data.description(error) : data.description;
2159
+ const isExtendedResult = typeof promiseData === "object" && !React.isValidElement(promiseData);
2160
+ const toastSettings = isExtendedResult ? promiseData : {
2161
+ message: promiseData
2162
+ };
2163
+ this.create({
2164
+ id,
2165
+ type: "error",
2166
+ description,
2167
+ ...toastSettings
2168
+ });
2169
+ }
2170
+ }).finally(() => {
2171
+ if (shouldDismiss) {
2172
+ this.dismiss(id);
2173
+ id = void 0;
2174
+ }
2175
+ data.finally == null ? void 0 : data.finally.call(data);
2176
+ });
2177
+ const unwrap = () => new Promise((resolve, reject) => originalPromise.then(() => result[0] === "reject" ? reject(result[1]) : resolve(result[1])).catch(reject));
2178
+ if (typeof id !== "string" && typeof id !== "number") {
2179
+ return {
2180
+ unwrap
2181
+ };
2182
+ } else {
2183
+ return Object.assign(id, {
2184
+ unwrap
2185
+ });
2186
+ }
2187
+ };
2188
+ this.custom = (jsx2, data) => {
2189
+ const id = (data == null ? void 0 : data.id) || toastsCounter++;
2190
+ this.create({
2191
+ jsx: jsx2(id),
2192
+ id,
2193
+ ...data
2194
+ });
2195
+ return id;
2196
+ };
2197
+ this.getActiveToasts = () => {
2198
+ return this.toasts.filter((toast2) => !this.dismissedToasts.has(toast2.id));
2199
+ };
2200
+ this.subscribers = [];
2201
+ this.toasts = [];
2202
+ this.dismissedToasts = /* @__PURE__ */ new Set();
2203
+ }
2204
+ };
2205
+ var ToastState = new Observer();
2206
+ var toastFunction = (message, data) => {
2207
+ const id = (data == null ? void 0 : data.id) || toastsCounter++;
2208
+ ToastState.addToast({
2209
+ title: message,
2210
+ ...data,
2211
+ id
2212
+ });
2213
+ return id;
2214
+ };
2215
+ var isHttpResponse = (data) => {
2216
+ return data && typeof data === "object" && "ok" in data && typeof data.ok === "boolean" && "status" in data && typeof data.status === "number";
2217
+ };
2218
+ var basicToast = toastFunction;
2219
+ var getHistory = () => ToastState.toasts;
2220
+ var getToasts = () => ToastState.getActiveToasts();
2221
+ var toast = Object.assign(basicToast, {
2222
+ success: ToastState.success,
2223
+ info: ToastState.info,
2224
+ warning: ToastState.warning,
2225
+ error: ToastState.error,
2226
+ custom: ToastState.custom,
2227
+ message: ToastState.message,
2228
+ promise: ToastState.promise,
2229
+ dismiss: ToastState.dismiss,
2230
+ loading: ToastState.loading
2231
+ }, {
2232
+ getHistory,
2233
+ getToasts
2234
+ });
2235
+ __insertCSS("[data-sonner-toaster][dir=ltr],html[dir=ltr]{--toast-icon-margin-start:-3px;--toast-icon-margin-end:4px;--toast-svg-margin-start:-1px;--toast-svg-margin-end:0px;--toast-button-margin-start:auto;--toast-button-margin-end:0;--toast-close-button-start:0;--toast-close-button-end:unset;--toast-close-button-transform:translate(-35%, -35%)}[data-sonner-toaster][dir=rtl],html[dir=rtl]{--toast-icon-margin-start:4px;--toast-icon-margin-end:-3px;--toast-svg-margin-start:0px;--toast-svg-margin-end:-1px;--toast-button-margin-start:0;--toast-button-margin-end:auto;--toast-close-button-start:unset;--toast-close-button-end:0;--toast-close-button-transform:translate(35%, -35%)}[data-sonner-toaster]{position:fixed;width:var(--width);font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;--gray1:hsl(0, 0%, 99%);--gray2:hsl(0, 0%, 97.3%);--gray3:hsl(0, 0%, 95.1%);--gray4:hsl(0, 0%, 93%);--gray5:hsl(0, 0%, 90.9%);--gray6:hsl(0, 0%, 88.7%);--gray7:hsl(0, 0%, 85.8%);--gray8:hsl(0, 0%, 78%);--gray9:hsl(0, 0%, 56.1%);--gray10:hsl(0, 0%, 52.3%);--gray11:hsl(0, 0%, 43.5%);--gray12:hsl(0, 0%, 9%);--border-radius:8px;box-sizing:border-box;padding:0;margin:0;list-style:none;outline:0;z-index:999999999;transition:transform .4s ease}@media (hover:none) and (pointer:coarse){[data-sonner-toaster][data-lifted=true]{transform:none}}[data-sonner-toaster][data-x-position=right]{right:var(--offset-right)}[data-sonner-toaster][data-x-position=left]{left:var(--offset-left)}[data-sonner-toaster][data-x-position=center]{left:50%;transform:translateX(-50%)}[data-sonner-toaster][data-y-position=top]{top:var(--offset-top)}[data-sonner-toaster][data-y-position=bottom]{bottom:var(--offset-bottom)}[data-sonner-toast]{--y:translateY(100%);--lift-amount:calc(var(--lift) * var(--gap));z-index:var(--z-index);position:absolute;opacity:0;transform:var(--y);touch-action:none;transition:transform .4s,opacity .4s,height .4s,box-shadow .2s;box-sizing:border-box;outline:0;overflow-wrap:anywhere}[data-sonner-toast][data-styled=true]{padding:16px;background:var(--normal-bg);border:1px solid var(--normal-border);color:var(--normal-text);border-radius:var(--border-radius);box-shadow:0 4px 12px rgba(0,0,0,.1);width:var(--width);font-size:13px;display:flex;align-items:center;gap:6px}[data-sonner-toast]:focus-visible{box-shadow:0 4px 12px rgba(0,0,0,.1),0 0 0 2px rgba(0,0,0,.2)}[data-sonner-toast][data-y-position=top]{top:0;--y:translateY(-100%);--lift:1;--lift-amount:calc(1 * var(--gap))}[data-sonner-toast][data-y-position=bottom]{bottom:0;--y:translateY(100%);--lift:-1;--lift-amount:calc(var(--lift) * var(--gap))}[data-sonner-toast][data-styled=true] [data-description]{font-weight:400;line-height:1.4;color:#3f3f3f}[data-rich-colors=true][data-sonner-toast][data-styled=true] [data-description]{color:inherit}[data-sonner-toaster][data-sonner-theme=dark] [data-description]{color:#e8e8e8}[data-sonner-toast][data-styled=true] [data-title]{font-weight:500;line-height:1.5;color:inherit}[data-sonner-toast][data-styled=true] [data-icon]{display:flex;height:16px;width:16px;position:relative;justify-content:flex-start;align-items:center;flex-shrink:0;margin-left:var(--toast-icon-margin-start);margin-right:var(--toast-icon-margin-end)}[data-sonner-toast][data-promise=true] [data-icon]>svg{opacity:0;transform:scale(.8);transform-origin:center;animation:sonner-fade-in .3s ease forwards}[data-sonner-toast][data-styled=true] [data-icon]>*{flex-shrink:0}[data-sonner-toast][data-styled=true] [data-icon] svg{margin-left:var(--toast-svg-margin-start);margin-right:var(--toast-svg-margin-end)}[data-sonner-toast][data-styled=true] [data-content]{display:flex;flex-direction:column;gap:2px}[data-sonner-toast][data-styled=true] [data-button]{border-radius:4px;padding-left:8px;padding-right:8px;height:24px;font-size:12px;color:var(--normal-bg);background:var(--normal-text);margin-left:var(--toast-button-margin-start);margin-right:var(--toast-button-margin-end);border:none;font-weight:500;cursor:pointer;outline:0;display:flex;align-items:center;flex-shrink:0;transition:opacity .4s,box-shadow .2s}[data-sonner-toast][data-styled=true] [data-button]:focus-visible{box-shadow:0 0 0 2px rgba(0,0,0,.4)}[data-sonner-toast][data-styled=true] [data-button]:first-of-type{margin-left:var(--toast-button-margin-start);margin-right:var(--toast-button-margin-end)}[data-sonner-toast][data-styled=true] [data-cancel]{color:var(--normal-text);background:rgba(0,0,0,.08)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast][data-styled=true] [data-cancel]{background:rgba(255,255,255,.3)}[data-sonner-toast][data-styled=true] [data-close-button]{position:absolute;left:var(--toast-close-button-start);right:var(--toast-close-button-end);top:0;height:20px;width:20px;display:flex;justify-content:center;align-items:center;padding:0;color:var(--gray12);background:var(--normal-bg);border:1px solid var(--gray4);transform:var(--toast-close-button-transform);border-radius:50%;cursor:pointer;z-index:1;transition:opacity .1s,background .2s,border-color .2s}[data-sonner-toast][data-styled=true] [data-close-button]:focus-visible{box-shadow:0 4px 12px rgba(0,0,0,.1),0 0 0 2px rgba(0,0,0,.2)}[data-sonner-toast][data-styled=true] [data-disabled=true]{cursor:not-allowed}[data-sonner-toast][data-styled=true]:hover [data-close-button]:hover{background:var(--gray2);border-color:var(--gray5)}[data-sonner-toast][data-swiping=true]::before{content:'';position:absolute;left:-100%;right:-100%;height:100%;z-index:-1}[data-sonner-toast][data-y-position=top][data-swiping=true]::before{bottom:50%;transform:scaleY(3) translateY(50%)}[data-sonner-toast][data-y-position=bottom][data-swiping=true]::before{top:50%;transform:scaleY(3) translateY(-50%)}[data-sonner-toast][data-swiping=false][data-removed=true]::before{content:'';position:absolute;inset:0;transform:scaleY(2)}[data-sonner-toast][data-expanded=true]::after{content:'';position:absolute;left:0;height:calc(var(--gap) + 1px);bottom:100%;width:100%}[data-sonner-toast][data-mounted=true]{--y:translateY(0);opacity:1}[data-sonner-toast][data-expanded=false][data-front=false]{--scale:var(--toasts-before) * 0.05 + 1;--y:translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)));height:var(--front-toast-height)}[data-sonner-toast]>*{transition:opacity .4s}[data-sonner-toast][data-x-position=right]{right:0}[data-sonner-toast][data-x-position=left]{left:0}[data-sonner-toast][data-expanded=false][data-front=false][data-styled=true]>*{opacity:0}[data-sonner-toast][data-visible=false]{opacity:0;pointer-events:none}[data-sonner-toast][data-mounted=true][data-expanded=true]{--y:translateY(calc(var(--lift) * var(--offset)));height:var(--initial-height)}[data-sonner-toast][data-removed=true][data-front=true][data-swipe-out=false]{--y:translateY(calc(var(--lift) * -100%));opacity:0}[data-sonner-toast][data-removed=true][data-front=false][data-swipe-out=false][data-expanded=true]{--y:translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));opacity:0}[data-sonner-toast][data-removed=true][data-front=false][data-swipe-out=false][data-expanded=false]{--y:translateY(40%);opacity:0;transition:transform .5s,opacity .2s}[data-sonner-toast][data-removed=true][data-front=false]::before{height:calc(var(--initial-height) + 20%)}[data-sonner-toast][data-swiping=true]{transform:var(--y) translateY(var(--swipe-amount-y,0)) translateX(var(--swipe-amount-x,0));transition:none}[data-sonner-toast][data-swiped=true]{user-select:none}[data-sonner-toast][data-swipe-out=true][data-y-position=bottom],[data-sonner-toast][data-swipe-out=true][data-y-position=top]{animation-duration:.2s;animation-timing-function:ease-out;animation-fill-mode:forwards}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=left]{animation-name:swipe-out-left}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=right]{animation-name:swipe-out-right}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=up]{animation-name:swipe-out-up}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=down]{animation-name:swipe-out-down}@keyframes swipe-out-left{from{transform:var(--y) translateX(var(--swipe-amount-x));opacity:1}to{transform:var(--y) translateX(calc(var(--swipe-amount-x) - 100%));opacity:0}}@keyframes swipe-out-right{from{transform:var(--y) translateX(var(--swipe-amount-x));opacity:1}to{transform:var(--y) translateX(calc(var(--swipe-amount-x) + 100%));opacity:0}}@keyframes swipe-out-up{from{transform:var(--y) translateY(var(--swipe-amount-y));opacity:1}to{transform:var(--y) translateY(calc(var(--swipe-amount-y) - 100%));opacity:0}}@keyframes swipe-out-down{from{transform:var(--y) translateY(var(--swipe-amount-y));opacity:1}to{transform:var(--y) translateY(calc(var(--swipe-amount-y) + 100%));opacity:0}}@media (max-width:600px){[data-sonner-toaster]{position:fixed;right:var(--mobile-offset-right);left:var(--mobile-offset-left);width:100%}[data-sonner-toaster][dir=rtl]{left:calc(var(--mobile-offset-left) * -1)}[data-sonner-toaster] [data-sonner-toast]{left:0;right:0;width:calc(100% - var(--mobile-offset-left) * 2)}[data-sonner-toaster][data-x-position=left]{left:var(--mobile-offset-left)}[data-sonner-toaster][data-y-position=bottom]{bottom:var(--mobile-offset-bottom)}[data-sonner-toaster][data-y-position=top]{top:var(--mobile-offset-top)}[data-sonner-toaster][data-x-position=center]{left:var(--mobile-offset-left);right:var(--mobile-offset-right);transform:none}}[data-sonner-toaster][data-sonner-theme=light]{--normal-bg:#fff;--normal-border:var(--gray4);--normal-text:var(--gray12);--success-bg:hsl(143, 85%, 96%);--success-border:hsl(145, 92%, 87%);--success-text:hsl(140, 100%, 27%);--info-bg:hsl(208, 100%, 97%);--info-border:hsl(221, 91%, 93%);--info-text:hsl(210, 92%, 45%);--warning-bg:hsl(49, 100%, 97%);--warning-border:hsl(49, 91%, 84%);--warning-text:hsl(31, 92%, 45%);--error-bg:hsl(359, 100%, 97%);--error-border:hsl(359, 100%, 94%);--error-text:hsl(360, 100%, 45%)}[data-sonner-toaster][data-sonner-theme=light] [data-sonner-toast][data-invert=true]{--normal-bg:#000;--normal-border:hsl(0, 0%, 20%);--normal-text:var(--gray1)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast][data-invert=true]{--normal-bg:#fff;--normal-border:var(--gray3);--normal-text:var(--gray12)}[data-sonner-toaster][data-sonner-theme=dark]{--normal-bg:#000;--normal-bg-hover:hsl(0, 0%, 12%);--normal-border:hsl(0, 0%, 20%);--normal-border-hover:hsl(0, 0%, 25%);--normal-text:var(--gray1);--success-bg:hsl(150, 100%, 6%);--success-border:hsl(147, 100%, 12%);--success-text:hsl(150, 86%, 65%);--info-bg:hsl(215, 100%, 6%);--info-border:hsl(223, 43%, 17%);--info-text:hsl(216, 87%, 65%);--warning-bg:hsl(64, 100%, 6%);--warning-border:hsl(60, 100%, 9%);--warning-text:hsl(46, 87%, 65%);--error-bg:hsl(358, 76%, 10%);--error-border:hsl(357, 89%, 16%);--error-text:hsl(358, 100%, 81%)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast] [data-close-button]{background:var(--normal-bg);border-color:var(--normal-border);color:var(--normal-text)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast] [data-close-button]:hover{background:var(--normal-bg-hover);border-color:var(--normal-border-hover)}[data-rich-colors=true][data-sonner-toast][data-type=success]{background:var(--success-bg);border-color:var(--success-border);color:var(--success-text)}[data-rich-colors=true][data-sonner-toast][data-type=success] [data-close-button]{background:var(--success-bg);border-color:var(--success-border);color:var(--success-text)}[data-rich-colors=true][data-sonner-toast][data-type=info]{background:var(--info-bg);border-color:var(--info-border);color:var(--info-text)}[data-rich-colors=true][data-sonner-toast][data-type=info] [data-close-button]{background:var(--info-bg);border-color:var(--info-border);color:var(--info-text)}[data-rich-colors=true][data-sonner-toast][data-type=warning]{background:var(--warning-bg);border-color:var(--warning-border);color:var(--warning-text)}[data-rich-colors=true][data-sonner-toast][data-type=warning] [data-close-button]{background:var(--warning-bg);border-color:var(--warning-border);color:var(--warning-text)}[data-rich-colors=true][data-sonner-toast][data-type=error]{background:var(--error-bg);border-color:var(--error-border);color:var(--error-text)}[data-rich-colors=true][data-sonner-toast][data-type=error] [data-close-button]{background:var(--error-bg);border-color:var(--error-border);color:var(--error-text)}.sonner-loading-wrapper{--size:16px;height:var(--size);width:var(--size);position:absolute;inset:0;z-index:10}.sonner-loading-wrapper[data-visible=false]{transform-origin:center;animation:sonner-fade-out .2s ease forwards}.sonner-spinner{position:relative;top:50%;left:50%;height:var(--size);width:var(--size)}.sonner-loading-bar{animation:sonner-spin 1.2s linear infinite;background:var(--gray11);border-radius:6px;height:8%;left:-10%;position:absolute;top:-3.9%;width:24%}.sonner-loading-bar:first-child{animation-delay:-1.2s;transform:rotate(.0001deg) translate(146%)}.sonner-loading-bar:nth-child(2){animation-delay:-1.1s;transform:rotate(30deg) translate(146%)}.sonner-loading-bar:nth-child(3){animation-delay:-1s;transform:rotate(60deg) translate(146%)}.sonner-loading-bar:nth-child(4){animation-delay:-.9s;transform:rotate(90deg) translate(146%)}.sonner-loading-bar:nth-child(5){animation-delay:-.8s;transform:rotate(120deg) translate(146%)}.sonner-loading-bar:nth-child(6){animation-delay:-.7s;transform:rotate(150deg) translate(146%)}.sonner-loading-bar:nth-child(7){animation-delay:-.6s;transform:rotate(180deg) translate(146%)}.sonner-loading-bar:nth-child(8){animation-delay:-.5s;transform:rotate(210deg) translate(146%)}.sonner-loading-bar:nth-child(9){animation-delay:-.4s;transform:rotate(240deg) translate(146%)}.sonner-loading-bar:nth-child(10){animation-delay:-.3s;transform:rotate(270deg) translate(146%)}.sonner-loading-bar:nth-child(11){animation-delay:-.2s;transform:rotate(300deg) translate(146%)}.sonner-loading-bar:nth-child(12){animation-delay:-.1s;transform:rotate(330deg) translate(146%)}@keyframes sonner-fade-in{0%{opacity:0;transform:scale(.8)}100%{opacity:1;transform:scale(1)}}@keyframes sonner-fade-out{0%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(.8)}}@keyframes sonner-spin{0%{opacity:1}100%{opacity:.15}}@media (prefers-reduced-motion){.sonner-loading-bar,[data-sonner-toast],[data-sonner-toast]>*{transition:none!important;animation:none!important}}.sonner-loader{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);transform-origin:center;transition:opacity .2s,transform .2s}.sonner-loader[data-visible=false]{opacity:0;transform:scale(.8) translate(-50%,-50%)}");
2236
+ function isAction(action) {
2237
+ return action.label !== void 0;
2238
+ }
2239
+ var VISIBLE_TOASTS_AMOUNT = 3;
2240
+ var VIEWPORT_OFFSET = "24px";
2241
+ var MOBILE_VIEWPORT_OFFSET = "16px";
2242
+ var TOAST_LIFETIME = 4e3;
2243
+ var TOAST_WIDTH = 356;
2244
+ var GAP = 14;
2245
+ var SWIPE_THRESHOLD = 45;
2246
+ var TIME_BEFORE_UNMOUNT = 200;
2247
+ function cn(...classes) {
2248
+ return classes.filter(Boolean).join(" ");
2249
+ }
2250
+ function getDefaultSwipeDirections(position) {
2251
+ const [y, x] = position.split("-");
2252
+ const directions = [];
2253
+ if (y) {
2254
+ directions.push(y);
2255
+ }
2256
+ if (x) {
2257
+ directions.push(x);
2258
+ }
2259
+ return directions;
2260
+ }
2261
+ var Toast = (props) => {
2262
+ var _toast_classNames, _toast_classNames1, _toast_classNames2, _toast_classNames3, _toast_classNames4, _toast_classNames5, _toast_classNames6, _toast_classNames7, _toast_classNames8;
2263
+ const { invert: ToasterInvert, toast: toast2, unstyled, interacting, setHeights, visibleToasts, heights, index, toasts, expanded, removeToast, defaultRichColors, closeButton: closeButtonFromToaster, style, cancelButtonStyle, actionButtonStyle, className = "", descriptionClassName = "", duration: durationFromToaster, position, gap, expandByDefault, classNames, icons, closeButtonAriaLabel = "Close toast" } = props;
2264
+ const [swipeDirection, setSwipeDirection] = React.useState(null);
2265
+ const [swipeOutDirection, setSwipeOutDirection] = React.useState(null);
2266
+ const [mounted, setMounted] = React.useState(false);
2267
+ const [removed, setRemoved] = React.useState(false);
2268
+ const [swiping, setSwiping] = React.useState(false);
2269
+ const [swipeOut, setSwipeOut] = React.useState(false);
2270
+ const [isSwiped, setIsSwiped] = React.useState(false);
2271
+ const [offsetBeforeRemove, setOffsetBeforeRemove] = React.useState(0);
2272
+ const [initialHeight, setInitialHeight] = React.useState(0);
2273
+ const remainingTime = React.useRef(toast2.duration || durationFromToaster || TOAST_LIFETIME);
2274
+ const dragStartTime = React.useRef(null);
2275
+ const toastRef = React.useRef(null);
2276
+ const isFront = index === 0;
2277
+ const isVisible = index + 1 <= visibleToasts;
2278
+ const toastType = toast2.type;
2279
+ const dismissible = toast2.dismissible !== false;
2280
+ const toastClassname = toast2.className || "";
2281
+ const toastDescriptionClassname = toast2.descriptionClassName || "";
2282
+ const heightIndex = React.useMemo(() => heights.findIndex((height) => height.toastId === toast2.id) || 0, [
2283
+ heights,
2284
+ toast2.id
2285
+ ]);
2286
+ const closeButton = React.useMemo(() => {
2287
+ var _toast_closeButton;
2288
+ return (_toast_closeButton = toast2.closeButton) != null ? _toast_closeButton : closeButtonFromToaster;
2289
+ }, [
2290
+ toast2.closeButton,
2291
+ closeButtonFromToaster
2292
+ ]);
2293
+ const duration = React.useMemo(() => toast2.duration || durationFromToaster || TOAST_LIFETIME, [
2294
+ toast2.duration,
2295
+ durationFromToaster
2296
+ ]);
2297
+ const closeTimerStartTimeRef = React.useRef(0);
2298
+ const offset = React.useRef(0);
2299
+ const lastCloseTimerStartTimeRef = React.useRef(0);
2300
+ const pointerStartRef = React.useRef(null);
2301
+ const [y, x] = position.split("-");
2302
+ const toastsHeightBefore = React.useMemo(() => {
2303
+ return heights.reduce((prev, curr, reducerIndex) => {
2304
+ if (reducerIndex >= heightIndex) {
2305
+ return prev;
2306
+ }
2307
+ return prev + curr.height;
2308
+ }, 0);
2309
+ }, [
2310
+ heights,
2311
+ heightIndex
2312
+ ]);
2313
+ const isDocumentHidden = useIsDocumentHidden();
2314
+ const invert = toast2.invert || ToasterInvert;
2315
+ const disabled = toastType === "loading";
2316
+ offset.current = React.useMemo(() => heightIndex * gap + toastsHeightBefore, [
2317
+ heightIndex,
2318
+ toastsHeightBefore
2319
+ ]);
2320
+ React.useEffect(() => {
2321
+ remainingTime.current = duration;
2322
+ }, [
2323
+ duration
2324
+ ]);
2325
+ React.useEffect(() => {
2326
+ setMounted(true);
2327
+ }, []);
2328
+ React.useEffect(() => {
2329
+ const toastNode = toastRef.current;
2330
+ if (toastNode) {
2331
+ const height = toastNode.getBoundingClientRect().height;
2332
+ setInitialHeight(height);
2333
+ setHeights((h) => [
2334
+ {
2335
+ toastId: toast2.id,
2336
+ height,
2337
+ position: toast2.position
2338
+ },
2339
+ ...h
2340
+ ]);
2341
+ return () => setHeights((h) => h.filter((height2) => height2.toastId !== toast2.id));
2342
+ }
2343
+ }, [
2344
+ setHeights,
2345
+ toast2.id
2346
+ ]);
2347
+ React.useLayoutEffect(() => {
2348
+ if (!mounted) return;
2349
+ const toastNode = toastRef.current;
2350
+ const originalHeight = toastNode.style.height;
2351
+ toastNode.style.height = "auto";
2352
+ const newHeight = toastNode.getBoundingClientRect().height;
2353
+ toastNode.style.height = originalHeight;
2354
+ setInitialHeight(newHeight);
2355
+ setHeights((heights2) => {
2356
+ const alreadyExists = heights2.find((height) => height.toastId === toast2.id);
2357
+ if (!alreadyExists) {
2358
+ return [
2359
+ {
2360
+ toastId: toast2.id,
2361
+ height: newHeight,
2362
+ position: toast2.position
2363
+ },
2364
+ ...heights2
2365
+ ];
2366
+ } else {
2367
+ return heights2.map((height) => height.toastId === toast2.id ? {
2368
+ ...height,
2369
+ height: newHeight
2370
+ } : height);
2371
+ }
2372
+ });
2373
+ }, [
2374
+ mounted,
2375
+ toast2.title,
2376
+ toast2.description,
2377
+ setHeights,
2378
+ toast2.id,
2379
+ toast2.jsx,
2380
+ toast2.action,
2381
+ toast2.cancel
2382
+ ]);
2383
+ const deleteToast = React.useCallback(() => {
2384
+ setRemoved(true);
2385
+ setOffsetBeforeRemove(offset.current);
2386
+ setHeights((h) => h.filter((height) => height.toastId !== toast2.id));
2387
+ setTimeout(() => {
2388
+ removeToast(toast2);
2389
+ }, TIME_BEFORE_UNMOUNT);
2390
+ }, [
2391
+ toast2,
2392
+ removeToast,
2393
+ setHeights,
2394
+ offset
2395
+ ]);
2396
+ React.useEffect(() => {
2397
+ if (toast2.promise && toastType === "loading" || toast2.duration === Infinity || toast2.type === "loading") return;
2398
+ let timeoutId;
2399
+ const pauseTimer = () => {
2400
+ if (lastCloseTimerStartTimeRef.current < closeTimerStartTimeRef.current) {
2401
+ const elapsedTime = (/* @__PURE__ */ new Date()).getTime() - closeTimerStartTimeRef.current;
2402
+ remainingTime.current = remainingTime.current - elapsedTime;
2403
+ }
2404
+ lastCloseTimerStartTimeRef.current = (/* @__PURE__ */ new Date()).getTime();
2405
+ };
2406
+ const startTimer = () => {
2407
+ if (remainingTime.current === Infinity) return;
2408
+ closeTimerStartTimeRef.current = (/* @__PURE__ */ new Date()).getTime();
2409
+ timeoutId = setTimeout(() => {
2410
+ toast2.onAutoClose == null ? void 0 : toast2.onAutoClose.call(toast2, toast2);
2411
+ deleteToast();
2412
+ }, remainingTime.current);
2413
+ };
2414
+ if (expanded || interacting || isDocumentHidden) {
2415
+ pauseTimer();
1571
2416
  } else {
1572
- await checkout.open(product, fallback);
2417
+ startTimer();
2418
+ }
2419
+ return () => clearTimeout(timeoutId);
2420
+ }, [
2421
+ expanded,
2422
+ interacting,
2423
+ toast2,
2424
+ toastType,
2425
+ isDocumentHidden,
2426
+ deleteToast
2427
+ ]);
2428
+ React.useEffect(() => {
2429
+ if (toast2.delete) {
2430
+ deleteToast();
2431
+ toast2.onDismiss == null ? void 0 : toast2.onDismiss.call(toast2, toast2);
2432
+ }
2433
+ }, [
2434
+ deleteToast,
2435
+ toast2.delete
2436
+ ]);
2437
+ function getLoadingIcon() {
2438
+ var _toast_classNames9;
2439
+ if (icons == null ? void 0 : icons.loading) {
2440
+ var _toast_classNames12;
2441
+ return /* @__PURE__ */ React.createElement("div", {
2442
+ className: cn(classNames == null ? void 0 : classNames.loader, toast2 == null ? void 0 : (_toast_classNames12 = toast2.classNames) == null ? void 0 : _toast_classNames12.loader, "sonner-loader"),
2443
+ "data-visible": toastType === "loading"
2444
+ }, icons.loading);
2445
+ }
2446
+ return /* @__PURE__ */ React.createElement(Loader, {
2447
+ className: cn(classNames == null ? void 0 : classNames.loader, toast2 == null ? void 0 : (_toast_classNames9 = toast2.classNames) == null ? void 0 : _toast_classNames9.loader),
2448
+ visible: toastType === "loading"
2449
+ });
2450
+ }
2451
+ const icon = toast2.icon || (icons == null ? void 0 : icons[toastType]) || getAsset(toastType);
2452
+ var _toast_richColors, _icons_close;
2453
+ return /* @__PURE__ */ React.createElement("li", {
2454
+ tabIndex: 0,
2455
+ ref: toastRef,
2456
+ className: cn(className, toastClassname, classNames == null ? void 0 : classNames.toast, toast2 == null ? void 0 : (_toast_classNames = toast2.classNames) == null ? void 0 : _toast_classNames.toast, classNames == null ? void 0 : classNames.default, classNames == null ? void 0 : classNames[toastType], toast2 == null ? void 0 : (_toast_classNames1 = toast2.classNames) == null ? void 0 : _toast_classNames1[toastType]),
2457
+ "data-sonner-toast": "",
2458
+ "data-rich-colors": (_toast_richColors = toast2.richColors) != null ? _toast_richColors : defaultRichColors,
2459
+ "data-styled": !Boolean(toast2.jsx || toast2.unstyled || unstyled),
2460
+ "data-mounted": mounted,
2461
+ "data-promise": Boolean(toast2.promise),
2462
+ "data-swiped": isSwiped,
2463
+ "data-removed": removed,
2464
+ "data-visible": isVisible,
2465
+ "data-y-position": y,
2466
+ "data-x-position": x,
2467
+ "data-index": index,
2468
+ "data-front": isFront,
2469
+ "data-swiping": swiping,
2470
+ "data-dismissible": dismissible,
2471
+ "data-type": toastType,
2472
+ "data-invert": invert,
2473
+ "data-swipe-out": swipeOut,
2474
+ "data-swipe-direction": swipeOutDirection,
2475
+ "data-expanded": Boolean(expanded || expandByDefault && mounted),
2476
+ "data-testid": toast2.testId,
2477
+ style: {
2478
+ "--index": index,
2479
+ "--toasts-before": index,
2480
+ "--z-index": toasts.length - index,
2481
+ "--offset": `${removed ? offsetBeforeRemove : offset.current}px`,
2482
+ "--initial-height": expandByDefault ? "auto" : `${initialHeight}px`,
2483
+ ...style,
2484
+ ...toast2.style
2485
+ },
2486
+ onDragEnd: () => {
2487
+ setSwiping(false);
2488
+ setSwipeDirection(null);
2489
+ pointerStartRef.current = null;
2490
+ },
2491
+ onPointerDown: (event) => {
2492
+ if (event.button === 2) return;
2493
+ if (disabled || !dismissible) return;
2494
+ dragStartTime.current = /* @__PURE__ */ new Date();
2495
+ setOffsetBeforeRemove(offset.current);
2496
+ event.target.setPointerCapture(event.pointerId);
2497
+ if (event.target.tagName === "BUTTON") return;
2498
+ setSwiping(true);
2499
+ pointerStartRef.current = {
2500
+ x: event.clientX,
2501
+ y: event.clientY
2502
+ };
2503
+ },
2504
+ onPointerUp: () => {
2505
+ var _toastRef_current, _toastRef_current1, _dragStartTime_current;
2506
+ if (swipeOut || !dismissible) return;
2507
+ pointerStartRef.current = null;
2508
+ const swipeAmountX = Number(((_toastRef_current = toastRef.current) == null ? void 0 : _toastRef_current.style.getPropertyValue("--swipe-amount-x").replace("px", "")) || 0);
2509
+ const swipeAmountY = Number(((_toastRef_current1 = toastRef.current) == null ? void 0 : _toastRef_current1.style.getPropertyValue("--swipe-amount-y").replace("px", "")) || 0);
2510
+ const timeTaken = (/* @__PURE__ */ new Date()).getTime() - ((_dragStartTime_current = dragStartTime.current) == null ? void 0 : _dragStartTime_current.getTime());
2511
+ const swipeAmount = swipeDirection === "x" ? swipeAmountX : swipeAmountY;
2512
+ const velocity = Math.abs(swipeAmount) / timeTaken;
2513
+ if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
2514
+ setOffsetBeforeRemove(offset.current);
2515
+ toast2.onDismiss == null ? void 0 : toast2.onDismiss.call(toast2, toast2);
2516
+ if (swipeDirection === "x") {
2517
+ setSwipeOutDirection(swipeAmountX > 0 ? "right" : "left");
2518
+ } else {
2519
+ setSwipeOutDirection(swipeAmountY > 0 ? "down" : "up");
2520
+ }
2521
+ deleteToast();
2522
+ setSwipeOut(true);
2523
+ return;
2524
+ } else {
2525
+ var _toastRef_current2, _toastRef_current3;
2526
+ (_toastRef_current2 = toastRef.current) == null ? void 0 : _toastRef_current2.style.setProperty("--swipe-amount-x", `0px`);
2527
+ (_toastRef_current3 = toastRef.current) == null ? void 0 : _toastRef_current3.style.setProperty("--swipe-amount-y", `0px`);
2528
+ }
2529
+ setIsSwiped(false);
2530
+ setSwiping(false);
2531
+ setSwipeDirection(null);
2532
+ },
2533
+ onPointerMove: (event) => {
2534
+ var _window_getSelection, _toastRef_current, _toastRef_current1;
2535
+ if (!pointerStartRef.current || !dismissible) return;
2536
+ const isHighlighted = ((_window_getSelection = window.getSelection()) == null ? void 0 : _window_getSelection.toString().length) > 0;
2537
+ if (isHighlighted) return;
2538
+ const yDelta = event.clientY - pointerStartRef.current.y;
2539
+ const xDelta = event.clientX - pointerStartRef.current.x;
2540
+ var _props_swipeDirections;
2541
+ const swipeDirections = (_props_swipeDirections = props.swipeDirections) != null ? _props_swipeDirections : getDefaultSwipeDirections(position);
2542
+ if (!swipeDirection && (Math.abs(xDelta) > 1 || Math.abs(yDelta) > 1)) {
2543
+ setSwipeDirection(Math.abs(xDelta) > Math.abs(yDelta) ? "x" : "y");
2544
+ }
2545
+ let swipeAmount = {
2546
+ x: 0,
2547
+ y: 0
2548
+ };
2549
+ const getDampening = (delta) => {
2550
+ const factor = Math.abs(delta) / 20;
2551
+ return 1 / (1.5 + factor);
2552
+ };
2553
+ if (swipeDirection === "y") {
2554
+ if (swipeDirections.includes("top") || swipeDirections.includes("bottom")) {
2555
+ if (swipeDirections.includes("top") && yDelta < 0 || swipeDirections.includes("bottom") && yDelta > 0) {
2556
+ swipeAmount.y = yDelta;
2557
+ } else {
2558
+ const dampenedDelta = yDelta * getDampening(yDelta);
2559
+ swipeAmount.y = Math.abs(dampenedDelta) < Math.abs(yDelta) ? dampenedDelta : yDelta;
2560
+ }
2561
+ }
2562
+ } else if (swipeDirection === "x") {
2563
+ if (swipeDirections.includes("left") || swipeDirections.includes("right")) {
2564
+ if (swipeDirections.includes("left") && xDelta < 0 || swipeDirections.includes("right") && xDelta > 0) {
2565
+ swipeAmount.x = xDelta;
2566
+ } else {
2567
+ const dampenedDelta = xDelta * getDampening(xDelta);
2568
+ swipeAmount.x = Math.abs(dampenedDelta) < Math.abs(xDelta) ? dampenedDelta : xDelta;
2569
+ }
2570
+ }
2571
+ }
2572
+ if (Math.abs(swipeAmount.x) > 0 || Math.abs(swipeAmount.y) > 0) {
2573
+ setIsSwiped(true);
2574
+ }
2575
+ (_toastRef_current = toastRef.current) == null ? void 0 : _toastRef_current.style.setProperty("--swipe-amount-x", `${swipeAmount.x}px`);
2576
+ (_toastRef_current1 = toastRef.current) == null ? void 0 : _toastRef_current1.style.setProperty("--swipe-amount-y", `${swipeAmount.y}px`);
2577
+ }
2578
+ }, closeButton && !toast2.jsx && toastType !== "loading" ? /* @__PURE__ */ React.createElement("button", {
2579
+ "aria-label": closeButtonAriaLabel,
2580
+ "data-disabled": disabled,
2581
+ "data-close-button": true,
2582
+ onClick: disabled || !dismissible ? () => {
2583
+ } : () => {
2584
+ deleteToast();
2585
+ toast2.onDismiss == null ? void 0 : toast2.onDismiss.call(toast2, toast2);
2586
+ },
2587
+ className: cn(classNames == null ? void 0 : classNames.closeButton, toast2 == null ? void 0 : (_toast_classNames2 = toast2.classNames) == null ? void 0 : _toast_classNames2.closeButton)
2588
+ }, (_icons_close = icons == null ? void 0 : icons.close) != null ? _icons_close : CloseIcon) : null, (toastType || toast2.icon || toast2.promise) && toast2.icon !== null && ((icons == null ? void 0 : icons[toastType]) !== null || toast2.icon) ? /* @__PURE__ */ React.createElement("div", {
2589
+ "data-icon": "",
2590
+ className: cn(classNames == null ? void 0 : classNames.icon, toast2 == null ? void 0 : (_toast_classNames3 = toast2.classNames) == null ? void 0 : _toast_classNames3.icon)
2591
+ }, toast2.promise || toast2.type === "loading" && !toast2.icon ? toast2.icon || getLoadingIcon() : null, toast2.type !== "loading" ? icon : null) : null, /* @__PURE__ */ React.createElement("div", {
2592
+ "data-content": "",
2593
+ className: cn(classNames == null ? void 0 : classNames.content, toast2 == null ? void 0 : (_toast_classNames4 = toast2.classNames) == null ? void 0 : _toast_classNames4.content)
2594
+ }, /* @__PURE__ */ React.createElement("div", {
2595
+ "data-title": "",
2596
+ className: cn(classNames == null ? void 0 : classNames.title, toast2 == null ? void 0 : (_toast_classNames5 = toast2.classNames) == null ? void 0 : _toast_classNames5.title)
2597
+ }, toast2.jsx ? toast2.jsx : typeof toast2.title === "function" ? toast2.title() : toast2.title), toast2.description ? /* @__PURE__ */ React.createElement("div", {
2598
+ "data-description": "",
2599
+ className: cn(descriptionClassName, toastDescriptionClassname, classNames == null ? void 0 : classNames.description, toast2 == null ? void 0 : (_toast_classNames6 = toast2.classNames) == null ? void 0 : _toast_classNames6.description)
2600
+ }, typeof toast2.description === "function" ? toast2.description() : toast2.description) : null), /* @__PURE__ */ React.isValidElement(toast2.cancel) ? toast2.cancel : toast2.cancel && isAction(toast2.cancel) ? /* @__PURE__ */ React.createElement("button", {
2601
+ "data-button": true,
2602
+ "data-cancel": true,
2603
+ style: toast2.cancelButtonStyle || cancelButtonStyle,
2604
+ onClick: (event) => {
2605
+ if (!isAction(toast2.cancel)) return;
2606
+ if (!dismissible) return;
2607
+ toast2.cancel.onClick == null ? void 0 : toast2.cancel.onClick.call(toast2.cancel, event);
2608
+ deleteToast();
2609
+ },
2610
+ className: cn(classNames == null ? void 0 : classNames.cancelButton, toast2 == null ? void 0 : (_toast_classNames7 = toast2.classNames) == null ? void 0 : _toast_classNames7.cancelButton)
2611
+ }, toast2.cancel.label) : null, /* @__PURE__ */ React.isValidElement(toast2.action) ? toast2.action : toast2.action && isAction(toast2.action) ? /* @__PURE__ */ React.createElement("button", {
2612
+ "data-button": true,
2613
+ "data-action": true,
2614
+ style: toast2.actionButtonStyle || actionButtonStyle,
2615
+ onClick: (event) => {
2616
+ if (!isAction(toast2.action)) return;
2617
+ toast2.action.onClick == null ? void 0 : toast2.action.onClick.call(toast2.action, event);
2618
+ if (event.defaultPrevented) return;
2619
+ deleteToast();
2620
+ },
2621
+ className: cn(classNames == null ? void 0 : classNames.actionButton, toast2 == null ? void 0 : (_toast_classNames8 = toast2.classNames) == null ? void 0 : _toast_classNames8.actionButton)
2622
+ }, toast2.action.label) : null);
2623
+ };
2624
+ function getDocumentDirection() {
2625
+ if (typeof window === "undefined") return "ltr";
2626
+ if (typeof document === "undefined") return "ltr";
2627
+ const dirAttribute = document.documentElement.getAttribute("dir");
2628
+ if (dirAttribute === "auto" || !dirAttribute) {
2629
+ return window.getComputedStyle(document.documentElement).direction;
2630
+ }
2631
+ return dirAttribute;
2632
+ }
2633
+ function assignOffset(defaultOffset, mobileOffset) {
2634
+ const styles = {};
2635
+ [
2636
+ defaultOffset,
2637
+ mobileOffset
2638
+ ].forEach((offset, index) => {
2639
+ const isMobile = index === 1;
2640
+ const prefix = isMobile ? "--mobile-offset" : "--offset";
2641
+ const defaultValue = isMobile ? MOBILE_VIEWPORT_OFFSET : VIEWPORT_OFFSET;
2642
+ function assignAll(offset2) {
2643
+ [
2644
+ "top",
2645
+ "right",
2646
+ "bottom",
2647
+ "left"
2648
+ ].forEach((key) => {
2649
+ styles[`${prefix}-${key}`] = typeof offset2 === "number" ? `${offset2}px` : offset2;
2650
+ });
2651
+ }
2652
+ if (typeof offset === "number" || typeof offset === "string") {
2653
+ assignAll(offset);
2654
+ } else if (typeof offset === "object") {
2655
+ [
2656
+ "top",
2657
+ "right",
2658
+ "bottom",
2659
+ "left"
2660
+ ].forEach((key) => {
2661
+ if (offset[key] === void 0) {
2662
+ styles[`${prefix}-${key}`] = defaultValue;
2663
+ } else {
2664
+ styles[`${prefix}-${key}`] = typeof offset[key] === "number" ? `${offset[key]}px` : offset[key];
2665
+ }
2666
+ });
2667
+ } else {
2668
+ assignAll(defaultValue);
1573
2669
  }
1574
- };
1575
- return trigger(start, {
1576
- asChild,
1577
- children: children ?? "Add to my plan",
1578
- className,
1579
- disabled: checkout.isLoading
1580
2670
  });
1581
- }
2671
+ return styles;
2672
+ }
2673
+ var Toaster = /* @__PURE__ */ React.forwardRef(function Toaster2(props, ref) {
2674
+ const { id, invert, position = "bottom-right", hotkey = [
2675
+ "altKey",
2676
+ "KeyT"
2677
+ ], expand, closeButton, className, offset, mobileOffset, theme = "light", richColors, duration, style, visibleToasts = VISIBLE_TOASTS_AMOUNT, toastOptions, dir = getDocumentDirection(), gap = GAP, icons, containerAriaLabel = "Notifications" } = props;
2678
+ const [toasts, setToasts] = React.useState([]);
2679
+ const filteredToasts = React.useMemo(() => {
2680
+ if (id) {
2681
+ return toasts.filter((toast2) => toast2.toasterId === id);
2682
+ }
2683
+ return toasts.filter((toast2) => !toast2.toasterId);
2684
+ }, [
2685
+ toasts,
2686
+ id
2687
+ ]);
2688
+ const possiblePositions = React.useMemo(() => {
2689
+ return Array.from(new Set([
2690
+ position
2691
+ ].concat(filteredToasts.filter((toast2) => toast2.position).map((toast2) => toast2.position))));
2692
+ }, [
2693
+ filteredToasts,
2694
+ position
2695
+ ]);
2696
+ const [heights, setHeights] = React.useState([]);
2697
+ const [expanded, setExpanded] = React.useState(false);
2698
+ const [interacting, setInteracting] = React.useState(false);
2699
+ const [actualTheme, setActualTheme] = React.useState(theme !== "system" ? theme : typeof window !== "undefined" ? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : "light");
2700
+ const listRef = React.useRef(null);
2701
+ const hotkeyLabel = hotkey.join("+").replace(/Key/g, "").replace(/Digit/g, "");
2702
+ const lastFocusedElementRef = React.useRef(null);
2703
+ const isFocusWithinRef = React.useRef(false);
2704
+ const removeToast = React.useCallback((toastToRemove) => {
2705
+ setToasts((toasts2) => {
2706
+ var _toasts_find;
2707
+ if (!((_toasts_find = toasts2.find((toast2) => toast2.id === toastToRemove.id)) == null ? void 0 : _toasts_find.delete)) {
2708
+ ToastState.dismiss(toastToRemove.id);
2709
+ }
2710
+ return toasts2.filter(({ id: id2 }) => id2 !== toastToRemove.id);
2711
+ });
2712
+ }, []);
2713
+ React.useEffect(() => {
2714
+ return ToastState.subscribe((toast2) => {
2715
+ if (toast2.dismiss) {
2716
+ requestAnimationFrame(() => {
2717
+ setToasts((toasts2) => toasts2.map((t) => t.id === toast2.id ? {
2718
+ ...t,
2719
+ delete: true
2720
+ } : t));
2721
+ });
2722
+ return;
2723
+ }
2724
+ setTimeout(() => {
2725
+ ReactDOM.flushSync(() => {
2726
+ setToasts((toasts2) => {
2727
+ const indexOfExistingToast = toasts2.findIndex((t) => t.id === toast2.id);
2728
+ if (indexOfExistingToast !== -1) {
2729
+ return [
2730
+ ...toasts2.slice(0, indexOfExistingToast),
2731
+ {
2732
+ ...toasts2[indexOfExistingToast],
2733
+ ...toast2
2734
+ },
2735
+ ...toasts2.slice(indexOfExistingToast + 1)
2736
+ ];
2737
+ }
2738
+ return [
2739
+ toast2,
2740
+ ...toasts2
2741
+ ];
2742
+ });
2743
+ });
2744
+ });
2745
+ });
2746
+ }, [
2747
+ toasts
2748
+ ]);
2749
+ React.useEffect(() => {
2750
+ if (theme !== "system") {
2751
+ setActualTheme(theme);
2752
+ return;
2753
+ }
2754
+ if (theme === "system") {
2755
+ if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
2756
+ setActualTheme("dark");
2757
+ } else {
2758
+ setActualTheme("light");
2759
+ }
2760
+ }
2761
+ if (typeof window === "undefined") return;
2762
+ const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
2763
+ try {
2764
+ darkMediaQuery.addEventListener("change", ({ matches }) => {
2765
+ if (matches) {
2766
+ setActualTheme("dark");
2767
+ } else {
2768
+ setActualTheme("light");
2769
+ }
2770
+ });
2771
+ } catch (error) {
2772
+ darkMediaQuery.addListener(({ matches }) => {
2773
+ try {
2774
+ if (matches) {
2775
+ setActualTheme("dark");
2776
+ } else {
2777
+ setActualTheme("light");
2778
+ }
2779
+ } catch (e) {
2780
+ console.error(e);
2781
+ }
2782
+ });
2783
+ }
2784
+ }, [
2785
+ theme
2786
+ ]);
2787
+ React.useEffect(() => {
2788
+ if (toasts.length <= 1) {
2789
+ setExpanded(false);
2790
+ }
2791
+ }, [
2792
+ toasts
2793
+ ]);
2794
+ React.useEffect(() => {
2795
+ const handleKeyDown = (event) => {
2796
+ var _listRef_current;
2797
+ const isHotkeyPressed = hotkey.every((key) => event[key] || event.code === key);
2798
+ if (isHotkeyPressed) {
2799
+ var _listRef_current1;
2800
+ setExpanded(true);
2801
+ (_listRef_current1 = listRef.current) == null ? void 0 : _listRef_current1.focus();
2802
+ }
2803
+ if (event.code === "Escape" && (document.activeElement === listRef.current || ((_listRef_current = listRef.current) == null ? void 0 : _listRef_current.contains(document.activeElement)))) {
2804
+ setExpanded(false);
2805
+ }
2806
+ };
2807
+ document.addEventListener("keydown", handleKeyDown);
2808
+ return () => document.removeEventListener("keydown", handleKeyDown);
2809
+ }, [
2810
+ hotkey
2811
+ ]);
2812
+ React.useEffect(() => {
2813
+ if (listRef.current) {
2814
+ return () => {
2815
+ if (lastFocusedElementRef.current) {
2816
+ lastFocusedElementRef.current.focus({
2817
+ preventScroll: true
2818
+ });
2819
+ lastFocusedElementRef.current = null;
2820
+ isFocusWithinRef.current = false;
2821
+ }
2822
+ };
2823
+ }
2824
+ }, [
2825
+ listRef.current
2826
+ ]);
2827
+ return (
2828
+ // Remove item from normal navigation flow, only available via hotkey
2829
+ /* @__PURE__ */ React.createElement("section", {
2830
+ ref,
2831
+ "aria-label": `${containerAriaLabel} ${hotkeyLabel}`,
2832
+ tabIndex: -1,
2833
+ "aria-live": "polite",
2834
+ "aria-relevant": "additions text",
2835
+ "aria-atomic": "false",
2836
+ suppressHydrationWarning: true
2837
+ }, possiblePositions.map((position2, index) => {
2838
+ var _heights_;
2839
+ const [y, x] = position2.split("-");
2840
+ if (!filteredToasts.length) return null;
2841
+ return /* @__PURE__ */ React.createElement("ol", {
2842
+ key: position2,
2843
+ dir: dir === "auto" ? getDocumentDirection() : dir,
2844
+ tabIndex: -1,
2845
+ ref: listRef,
2846
+ className,
2847
+ "data-sonner-toaster": true,
2848
+ "data-sonner-theme": actualTheme,
2849
+ "data-y-position": y,
2850
+ "data-x-position": x,
2851
+ style: {
2852
+ "--front-toast-height": `${((_heights_ = heights[0]) == null ? void 0 : _heights_.height) || 0}px`,
2853
+ "--width": `${TOAST_WIDTH}px`,
2854
+ "--gap": `${gap}px`,
2855
+ ...style,
2856
+ ...assignOffset(offset, mobileOffset)
2857
+ },
2858
+ onBlur: (event) => {
2859
+ if (isFocusWithinRef.current && !event.currentTarget.contains(event.relatedTarget)) {
2860
+ isFocusWithinRef.current = false;
2861
+ if (lastFocusedElementRef.current) {
2862
+ lastFocusedElementRef.current.focus({
2863
+ preventScroll: true
2864
+ });
2865
+ lastFocusedElementRef.current = null;
2866
+ }
2867
+ }
2868
+ },
2869
+ onFocus: (event) => {
2870
+ const isNotDismissible = event.target instanceof HTMLElement && event.target.dataset.dismissible === "false";
2871
+ if (isNotDismissible) return;
2872
+ if (!isFocusWithinRef.current) {
2873
+ isFocusWithinRef.current = true;
2874
+ lastFocusedElementRef.current = event.relatedTarget;
2875
+ }
2876
+ },
2877
+ onMouseEnter: () => setExpanded(true),
2878
+ onMouseMove: () => setExpanded(true),
2879
+ onMouseLeave: () => {
2880
+ if (!interacting) {
2881
+ setExpanded(false);
2882
+ }
2883
+ },
2884
+ onDragEnd: () => setExpanded(false),
2885
+ onPointerDown: (event) => {
2886
+ const isNotDismissible = event.target instanceof HTMLElement && event.target.dataset.dismissible === "false";
2887
+ if (isNotDismissible) return;
2888
+ setInteracting(true);
2889
+ },
2890
+ onPointerUp: () => setInteracting(false)
2891
+ }, filteredToasts.filter((toast2) => !toast2.position && index === 0 || toast2.position === position2).map((toast2, index2) => {
2892
+ var _toastOptions_duration, _toastOptions_closeButton;
2893
+ return /* @__PURE__ */ React.createElement(Toast, {
2894
+ key: toast2.id,
2895
+ icons,
2896
+ index: index2,
2897
+ toast: toast2,
2898
+ defaultRichColors: richColors,
2899
+ duration: (_toastOptions_duration = toastOptions == null ? void 0 : toastOptions.duration) != null ? _toastOptions_duration : duration,
2900
+ className: toastOptions == null ? void 0 : toastOptions.className,
2901
+ descriptionClassName: toastOptions == null ? void 0 : toastOptions.descriptionClassName,
2902
+ invert,
2903
+ visibleToasts,
2904
+ closeButton: (_toastOptions_closeButton = toastOptions == null ? void 0 : toastOptions.closeButton) != null ? _toastOptions_closeButton : closeButton,
2905
+ interacting,
2906
+ position: position2,
2907
+ style: toastOptions == null ? void 0 : toastOptions.style,
2908
+ unstyled: toastOptions == null ? void 0 : toastOptions.unstyled,
2909
+ classNames: toastOptions == null ? void 0 : toastOptions.classNames,
2910
+ cancelButtonStyle: toastOptions == null ? void 0 : toastOptions.cancelButtonStyle,
2911
+ actionButtonStyle: toastOptions == null ? void 0 : toastOptions.actionButtonStyle,
2912
+ closeButtonAriaLabel: toastOptions == null ? void 0 : toastOptions.closeButtonAriaLabel,
2913
+ removeToast,
2914
+ toasts: filteredToasts.filter((t) => t.position == toast2.position),
2915
+ heights: heights.filter((h) => h.position == toast2.position),
2916
+ setHeights,
2917
+ expandByDefault: expand,
2918
+ gap,
2919
+ expanded,
2920
+ swipeDirections: props.swipeDirections
2921
+ });
2922
+ }));
2923
+ }))
2924
+ );
2925
+ });
1582
2926
  function FunnelToaster(props = {}) {
1583
2927
  return createElement(Toaster, { position: "top-center", ...props });
1584
2928
  }
1585
2929
  function renderToaster(toaster) {
1586
2930
  if (toaster === false) return null;
1587
- return createElement(Toaster, { position: "top-center", ...toaster === true ? {} : toaster });
2931
+ return /* @__PURE__ */ jsx(FunnelToaster, { ...toaster === true ? {} : toaster });
1588
2932
  }
1589
2933
 
1590
2934
  // src/state/namespaces.ts
@@ -1608,7 +2952,8 @@ var Namespace = class {
1608
2952
  const out = {};
1609
2953
  const all = this.store.getState();
1610
2954
  for (const key in all) {
1611
- if (key.startsWith(this.dot)) out[key.slice(this.dot.length)] = all[key];
2955
+ if (key.startsWith(this.dot))
2956
+ out[key.slice(this.dot.length)] = all[key];
1612
2957
  }
1613
2958
  return out;
1614
2959
  }
@@ -1654,9 +2999,10 @@ var FunnelCtx = createContext(null);
1654
2999
  function FunnelProvider({
1655
3000
  config,
1656
3001
  sessionValues,
3002
+ deferSessionValues = false,
1657
3003
  tracker,
1658
3004
  context: contextOpts,
1659
- products,
3005
+ offerings,
1660
3006
  experiments,
1661
3007
  checkout,
1662
3008
  messages,
@@ -1669,7 +3015,12 @@ function FunnelProvider({
1669
3015
  const [value] = useState(() => {
1670
3016
  const s = store ?? createFunnelStore(
1671
3017
  { responses: config?.responses, data: config?.data },
1672
- sessionValues
3018
+ // FLASH-FREE HYDRATION: with deferSessionValues the initial render is seeded with
3019
+ // DEFAULTS ONLY, so the client's first render is byte-identical to the build-time
3020
+ // DEFAULT-state prerender it hydrates against (clean match). The visitor's saved
3021
+ // answers arrive as a post-hydration setMany below. Without the flag, sessionValues
3022
+ // seed the store at construction (SSR + non-prerendered paths).
3023
+ deferSessionValues ? void 0 : sessionValues
1673
3024
  );
1674
3025
  const ctx = buildContext({
1675
3026
  funnelId: config?.id,
@@ -1683,17 +3034,37 @@ function FunnelProvider({
1683
3034
  getVisitorId: () => ctx.identity.visitorId
1684
3035
  });
1685
3036
  const busTracker = withBus(resolvedTracker, bus);
3037
+ const emailForCheckout = () => {
3038
+ const value2 = s.get("user.email");
3039
+ return typeof value2 === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value2) ? value2 : void 0;
3040
+ };
3041
+ const catalog = buildCatalog(offerings ?? []);
1686
3042
  return {
1687
3043
  funnel: createFunnel(s, ctx, busTracker, experiments ?? []),
1688
3044
  store: s,
1689
3045
  context: ctx,
1690
- catalog: buildCatalog(products ?? []),
1691
- driver: checkout ?? createMockDriver(),
3046
+ catalog,
3047
+ // Slot → catalog-key resolution wraps the email-enriching driver, so the wire charges the real
3048
+ // catalog key while pages + analytics keep the durable SLOT identity (req.product).
3049
+ driver: driverWithCatalog(
3050
+ driverWithEmail(
3051
+ checkout ?? createMockDriver(),
3052
+ emailForCheckout
3053
+ ),
3054
+ catalog
3055
+ ),
1692
3056
  tracker: busTracker,
1693
3057
  bus
1694
3058
  };
1695
3059
  });
1696
3060
  useEffect(() => attachBus(value.bus), [value.bus]);
3061
+ useEffect(() => {
3062
+ if (!deferSessionValues || !sessionValues) return;
3063
+ const updates = {};
3064
+ for (const [key, v] of Object.entries(sessionValues))
3065
+ if (v !== void 0) updates[key] = v;
3066
+ if (Object.keys(updates).length > 0) value.store.setMany(updates);
3067
+ }, []);
1697
3068
  useEffect(() => {
1698
3069
  value.tracker.setAcquisition?.(buildAcquisition(value.context));
1699
3070
  }, [value]);
@@ -1722,13 +3093,18 @@ var FALLBACK_TRACKER = createConsoleTracker({ silent: true });
1722
3093
  function useFunnelCtx() {
1723
3094
  const ctx = useContext(FunnelCtx);
1724
3095
  if (!ctx) {
1725
- throw new Error("useFunnel/useResponse/\u2026 must be used inside <FunnelProvider>");
3096
+ throw new Error(
3097
+ "useFunnel/useResponse/\u2026 must be used inside <FunnelProvider>"
3098
+ );
1726
3099
  }
1727
3100
  return ctx;
1728
3101
  }
1729
3102
  function useFunnel() {
1730
3103
  return useFunnelCtx().funnel;
1731
3104
  }
3105
+ function useBusInternal() {
3106
+ return useContext(FunnelCtx)?.bus ?? null;
3107
+ }
1732
3108
  function useStoreKey(store, key) {
1733
3109
  const subscribe = useCallback(
1734
3110
  (cb) => store.subscribe(cb, { keys: [key] }),
@@ -1757,7 +3133,9 @@ function useField(path) {
1757
3133
  const key = path.slice(dot + 1);
1758
3134
  const { funnel, store } = useFunnelCtx();
1759
3135
  if (!(ns in NAMESPACE_PREFIX)) {
1760
- throw new Error(`useField: "${path}" is not a writable namespace (user/responses/data)`);
3136
+ throw new Error(
3137
+ `useField: "${path}" is not a writable namespace (user/responses/data)`
3138
+ );
1761
3139
  }
1762
3140
  const value = useStoreKey(store, `${NAMESPACE_PREFIX[ns]}.${key}`);
1763
3141
  const set = useCallback(
@@ -1787,10 +3165,403 @@ function useSystem() {
1787
3165
  function useContextValue(path) {
1788
3166
  const ctx = useContextObject();
1789
3167
  return useMemo(
1790
- () => path.split(".").reduce((acc, k) => acc?.[k], ctx),
3168
+ () => path.split(".").reduce(
3169
+ (acc, k) => acc?.[k],
3170
+ ctx
3171
+ ),
1791
3172
  [ctx, path]
1792
3173
  );
1793
3174
  }
3175
+
3176
+ // src/tracking/snapshotPersistence.ts
3177
+ function createSnapshotPersistence(opts) {
3178
+ const baseDelay = opts.baseDelayMs ?? 1e3;
3179
+ const maxDelay = opts.maxDelayMs ?? 3e4;
3180
+ const setT = opts.timers?.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
3181
+ const clearT = opts.timers?.clearTimeout ?? ((h) => clearTimeout(h));
3182
+ const rand = opts.random ?? Math.random;
3183
+ let pending = null;
3184
+ let seq = 0;
3185
+ let pendingSeq = 0;
3186
+ let inFlight = false;
3187
+ let attempt = 0;
3188
+ let retryTimer = null;
3189
+ let disposed = false;
3190
+ let fatalSeq = -1;
3191
+ const clearRetry = () => {
3192
+ if (retryTimer !== null) {
3193
+ clearT(retryTimer);
3194
+ retryTimer = null;
3195
+ }
3196
+ };
3197
+ const buildBody = (snapshot, s) => JSON.stringify({ sessionId: opts.getSessionId(), userData: snapshot, seq: s });
3198
+ const trySend = () => {
3199
+ if (disposed || inFlight || pending === null) return;
3200
+ if (retryTimer !== null) return;
3201
+ const sessionId = opts.getSessionId();
3202
+ if (!sessionId) return;
3203
+ if (typeof navigator !== "undefined" && navigator.onLine === false) return;
3204
+ if (pendingSeq === fatalSeq) return;
3205
+ const snapshot = pending;
3206
+ const sentSeq = pendingSeq;
3207
+ const body = buildBody(snapshot, sentSeq);
3208
+ inFlight = true;
3209
+ void fetch(opts.url, {
3210
+ method: "POST",
3211
+ headers: { "content-type": "application/json" },
3212
+ body,
3213
+ keepalive: true
3214
+ }).then((res) => {
3215
+ inFlight = false;
3216
+ if (disposed) return;
3217
+ if (res.ok) {
3218
+ attempt = 0;
3219
+ if (pendingSeq === sentSeq) pending = null;
3220
+ else trySend();
3221
+ return;
3222
+ }
3223
+ if (res.status >= 400 && res.status < 500) {
3224
+ if (process.env.NODE_ENV !== "production") {
3225
+ console.warn(
3226
+ `[appfunnel] snapshot persist rejected ${res.status} (dropping snapshot; check session/payload)`
3227
+ );
3228
+ }
3229
+ fatalSeq = sentSeq;
3230
+ if (pendingSeq === sentSeq) pending = null;
3231
+ else trySend();
3232
+ return;
3233
+ }
3234
+ scheduleRetry();
3235
+ }).catch(() => {
3236
+ inFlight = false;
3237
+ if (disposed) return;
3238
+ scheduleRetry();
3239
+ });
3240
+ };
3241
+ const scheduleRetry = () => {
3242
+ if (disposed || pending === null) return;
3243
+ const exp = Math.min(maxDelay, baseDelay * 2 ** attempt);
3244
+ const delay2 = Math.round(exp * (0.5 + rand() * 0.5));
3245
+ attempt++;
3246
+ clearRetry();
3247
+ retryTimer = setT(() => {
3248
+ retryTimer = null;
3249
+ trySend();
3250
+ }, delay2);
3251
+ };
3252
+ const push = (snapshot) => {
3253
+ if (disposed) return;
3254
+ pending = snapshot;
3255
+ pendingSeq = ++seq;
3256
+ if (fatalSeq !== -1 && pendingSeq !== fatalSeq) fatalSeq = -1;
3257
+ if (retryTimer === null) trySend();
3258
+ };
3259
+ const flushBeacon = () => {
3260
+ if (disposed || pending === null) return;
3261
+ const sessionId = opts.getSessionId();
3262
+ if (!sessionId) return;
3263
+ const body = buildBody(pending, pendingSeq);
3264
+ let delivered = false;
3265
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
3266
+ try {
3267
+ delivered = navigator.sendBeacon(
3268
+ opts.url,
3269
+ new Blob([body], { type: "application/json" })
3270
+ );
3271
+ } catch {
3272
+ delivered = false;
3273
+ }
3274
+ }
3275
+ if (!delivered) {
3276
+ try {
3277
+ void fetch(opts.url, {
3278
+ method: "POST",
3279
+ headers: { "content-type": "application/json" },
3280
+ body,
3281
+ keepalive: true
3282
+ }).catch(() => {
3283
+ });
3284
+ delivered = true;
3285
+ } catch {
3286
+ delivered = false;
3287
+ }
3288
+ }
3289
+ if (delivered) pending = null;
3290
+ };
3291
+ const onHidden = () => {
3292
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
3293
+ flushBeacon();
3294
+ }
3295
+ };
3296
+ const onPageHide = () => flushBeacon();
3297
+ const onOnline = () => trySend();
3298
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
3299
+ try {
3300
+ document.addEventListener("visibilitychange", onHidden);
3301
+ window.addEventListener("pagehide", onPageHide);
3302
+ window.addEventListener("online", onOnline);
3303
+ } catch {
3304
+ }
3305
+ }
3306
+ const dispose = () => {
3307
+ disposed = true;
3308
+ clearRetry();
3309
+ pending = null;
3310
+ if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
3311
+ try {
3312
+ document.removeEventListener("visibilitychange", onHidden);
3313
+ window.removeEventListener("pagehide", onPageHide);
3314
+ window.removeEventListener("online", onOnline);
3315
+ } catch {
3316
+ }
3317
+ }
3318
+ };
3319
+ return { push, flushBeacon, dispose };
3320
+ }
3321
+
3322
+ // src/tracking/platformTracker.ts
3323
+ function validClickIds(acq) {
3324
+ return Object.entries(acq.clickIds ?? {}).filter(
3325
+ (e) => typeof e[1] === "string" && e[1] !== ""
3326
+ );
3327
+ }
3328
+ function acquisitionQueryBag(acq) {
3329
+ const q = {};
3330
+ const utm = acq.utm ?? {};
3331
+ for (const [k, v] of Object.entries({
3332
+ utm_source: utm.source,
3333
+ utm_medium: utm.medium,
3334
+ utm_campaign: utm.campaign,
3335
+ utm_term: utm.term,
3336
+ utm_content: utm.content
3337
+ })) if (v) q[k] = v;
3338
+ for (const [k, v] of validClickIds(acq)) q[k] = v;
3339
+ if (acq.cookies?.fbp) q._fbp = acq.cookies.fbp;
3340
+ if (acq.cookies?.fbc) q._fbc = acq.cookies.fbc;
3341
+ return q;
3342
+ }
3343
+ function acquisitionAdAttribution(acq) {
3344
+ const a = {};
3345
+ for (const [k, v] of validClickIds(acq)) a[k] = v;
3346
+ if (acq.cookies?.fbp) a.fbp = acq.cookies.fbp;
3347
+ if (acq.cookies?.fbc) a.fbc = acq.cookies.fbc;
3348
+ else if (acq.clickIds?.fbclid) a.fbc = `fb.1.${Date.now()}.${acq.clickIds.fbclid}`;
3349
+ return a;
3350
+ }
3351
+ function createPlatformTracker(cfg) {
3352
+ if (typeof window === "undefined" || cfg.mode === "test") return createConsoleTracker();
3353
+ const base = cfg.apiBase.replace(/\/+$/, "");
3354
+ const url = `${base}/campaign/${encodeURIComponent(cfg.campaignId)}/event`;
3355
+ const landingUrl = window.location.href;
3356
+ const sidKey = `af_sid:${cfg.campaignId}`;
3357
+ let sessionId = null;
3358
+ try {
3359
+ sessionId = window.sessionStorage.getItem(sidKey);
3360
+ } catch {
3361
+ }
3362
+ if (!sessionId && cfg.sessionId) sessionId = cfg.sessionId;
3363
+ let acquisition = null;
3364
+ let acquisitionSent = false;
3365
+ let chain = Promise.resolve();
3366
+ const send = async (event, data, userData) => {
3367
+ let mergedUserData = landingUrl ? { landingUrl, ...userData } : userData;
3368
+ let adAttribution;
3369
+ let attachingAcquisition = false;
3370
+ if (acquisition && !acquisitionSent) {
3371
+ attachingAcquisition = true;
3372
+ const query = acquisitionQueryBag(acquisition);
3373
+ if (Object.keys(query).length) mergedUserData = { ...mergedUserData, query };
3374
+ const ad = acquisitionAdAttribution(acquisition);
3375
+ if (Object.keys(ad).length) adAttribution = ad;
3376
+ }
3377
+ const body = {
3378
+ campaignId: cfg.campaignId,
3379
+ funnelId: cfg.funnelId,
3380
+ event,
3381
+ sessionId: sessionId ?? void 0,
3382
+ visitorId: cfg.visitorId ?? void 0,
3383
+ // Campaign route stamp (campaign-routing pivot): ingestion reads this ONLY at session create
3384
+ // (WebFunnelSession.campaignRouteId, snapshot semantics) — carried on every event but ignored
3385
+ // once the session exists. Undefined ⇒ default funnel / no route matched.
3386
+ campaignRouteId: cfg.route?.id ?? void 0,
3387
+ eventId: typeof data?.eventId === "string" ? data.eventId : void 0,
3388
+ data: data ?? {},
3389
+ metadata: {
3390
+ language: typeof navigator !== "undefined" ? navigator.language : void 0,
3391
+ screen: typeof window.screen !== "undefined" ? `${window.screen.width}x${window.screen.height}` : void 0,
3392
+ timezone: (() => {
3393
+ try {
3394
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
3395
+ } catch {
3396
+ return void 0;
3397
+ }
3398
+ })(),
3399
+ referrer: typeof document !== "undefined" && document.referrer ? document.referrer : void 0
3400
+ },
3401
+ ...mergedUserData ? { userData: mergedUserData } : {},
3402
+ ...adAttribution ? { adAttribution } : {}
3403
+ };
3404
+ const res = await fetch(url, {
3405
+ method: "POST",
3406
+ headers: { "content-type": "application/json" },
3407
+ body: JSON.stringify(body),
3408
+ keepalive: true
3409
+ // survives page.exit / unload
3410
+ });
3411
+ if (attachingAcquisition) acquisitionSent = true;
3412
+ const json = await res.json().catch(() => null);
3413
+ if (json?.sessionId && json.sessionId !== sessionId) {
3414
+ sessionId = json.sessionId;
3415
+ try {
3416
+ window.sessionStorage.setItem(sidKey, sessionId);
3417
+ } catch {
3418
+ }
3419
+ }
3420
+ };
3421
+ const enqueue = (event, data, userData) => {
3422
+ const task = () => send(event, data, userData).catch((e) => {
3423
+ if (process.env.NODE_ENV !== "production") console.warn(`[appfunnel] track failed (${event}):`, e);
3424
+ });
3425
+ if (!sessionId) {
3426
+ chain = chain.then(task);
3427
+ } else {
3428
+ void task();
3429
+ }
3430
+ };
3431
+ const VARS_DEBOUNCE_MS = 800;
3432
+ const dataUrl = `${base}/campaign/${encodeURIComponent(cfg.campaignId)}/event/session/data`;
3433
+ let pendingVars = null;
3434
+ let varsTimer = null;
3435
+ const persistence = createSnapshotPersistence({
3436
+ url: dataUrl,
3437
+ getSessionId: () => sessionId
3438
+ });
3439
+ const flushVars = () => {
3440
+ if (varsTimer) {
3441
+ clearTimeout(varsTimer);
3442
+ varsTimer = null;
3443
+ }
3444
+ if (!pendingVars) return;
3445
+ const snapshot = pendingVars;
3446
+ pendingVars = null;
3447
+ persistence.push(snapshot);
3448
+ if (!sessionId) chain = chain.then(() => {
3449
+ persistence.push(snapshot);
3450
+ });
3451
+ };
3452
+ const drainOnExit = () => {
3453
+ if (pendingVars) {
3454
+ flushVars();
3455
+ persistence.flushBeacon();
3456
+ }
3457
+ };
3458
+ try {
3459
+ window.addEventListener("visibilitychange", drainOnExit);
3460
+ window.addEventListener("pagehide", drainOnExit);
3461
+ } catch {
3462
+ }
3463
+ if (cfg.experiment?.id && cfg.experiment.variant) {
3464
+ enqueue("experiment.exposure", exposurePayload(cfg.experiment.id, cfg.experiment.variant, cfg.experiment.version));
3465
+ }
3466
+ return {
3467
+ track: (event, data, userData) => enqueue(event, data, userData),
3468
+ identify: (email, eventId) => enqueue("user.registered", { email, ...eventId ? { eventId } : {} }),
3469
+ setVariables: (variables) => {
3470
+ pendingVars = variables;
3471
+ if (varsTimer) clearTimeout(varsTimer);
3472
+ varsTimer = setTimeout(flushVars, VARS_DEBOUNCE_MS);
3473
+ },
3474
+ setAcquisition: (acq) => {
3475
+ acquisition = acq;
3476
+ }
3477
+ };
3478
+ }
3479
+
3480
+ // src/flow/mount.tsx
3481
+ var AssetsContext = createContext({});
3482
+ function AssetsProvider({
3483
+ assets,
3484
+ children
3485
+ }) {
3486
+ return createElement(AssetsContext.Provider, { value: assets ?? {} }, children);
3487
+ }
3488
+ function useAssets() {
3489
+ return useContext(AssetsContext);
3490
+ }
3491
+ function useAsset(name) {
3492
+ return useContext(AssetsContext)[name];
3493
+ }
3494
+ function createFunnelTree({
3495
+ config,
3496
+ pages,
3497
+ layout,
3498
+ checkoutDriver,
3499
+ messages
3500
+ }) {
3501
+ return function tree(opts) {
3502
+ const mode = opts.mode ?? "live";
3503
+ const embedEnabled = opts.embed ?? false;
3504
+ const tracker = opts.tracking ? createPlatformTracker({
3505
+ ...opts.tracking,
3506
+ visitorId: opts.visitorId,
3507
+ sessionId: opts.sessionId,
3508
+ mode
3509
+ }) : void 0;
3510
+ const checkout = checkoutDriver && opts.tracking ? checkoutDriver({
3511
+ apiBase: opts.tracking.apiBase,
3512
+ campaignId: opts.tracking.campaignId,
3513
+ funnelId: opts.tracking.funnelId,
3514
+ mode,
3515
+ sessionId: opts.sessionId,
3516
+ visitorId: opts.visitorId
3517
+ }) : void 0;
3518
+ const layoutWithResume = function LayoutWithCheckoutResume({
3519
+ children
3520
+ }) {
3521
+ return createElement(
3522
+ Fragment,
3523
+ null,
3524
+ createElement(CheckoutResume, null),
3525
+ embedEnabled ? createElement(EmbedBridge, null) : null,
3526
+ layout ? createElement(layout, null, children) : children
3527
+ );
3528
+ };
3529
+ return createElement(AssetsProvider, {
3530
+ assets: opts.assets,
3531
+ children: createElement(FunnelProvider, {
3532
+ config,
3533
+ offerings: opts.offerings ?? [],
3534
+ messages: opts.messages ?? messages,
3535
+ locale: opts.locale,
3536
+ tracker,
3537
+ checkout,
3538
+ sessionValues: opts.sessionValues,
3539
+ deferSessionValues: opts.deferSessionValues,
3540
+ experiments: opts.experiments,
3541
+ context: {
3542
+ mode,
3543
+ visitorId: opts.visitorId ?? null,
3544
+ customerId: opts.customerId ?? null,
3545
+ sessionId: opts.sessionId ?? null
3546
+ },
3547
+ children: createElement(FunnelView, {
3548
+ pages,
3549
+ layout: layoutWithResume,
3550
+ initialKey: opts.initialKey,
3551
+ // FLASH-FREE HYDRATION (Option B, FIX 1): trust the SERVER-resolved initialKey instead of
3552
+ // re-running the entry guard. The CLIENT sets this via deferSessionValues (its deferred
3553
+ // snapshot is empty, so re-running the guard would bounce a qualified deep-link and
3554
+ // mismatch the markup). The build-time PRERENDER sets `trustInitialKey` explicitly so a
3555
+ // guarded page prerenders its OWN default-state markup (the renderer picks target-vs-bounce
3556
+ // at serve time). Live SSR fallback leaves both off → the guard runs against real values.
3557
+ trustInitialKey: opts.trustInitialKey ?? opts.deferSessionValues,
3558
+ prefetch: "off"
3559
+ // renderer background-modulepreloads all chunks
3560
+ })
3561
+ })
3562
+ });
3563
+ };
3564
+ }
1794
3565
  function Choice({
1795
3566
  bind,
1796
3567
  options,
@@ -1930,95 +3701,6 @@ function Script({
1930
3701
  return null;
1931
3702
  }
1932
3703
 
1933
- // src/manifest/manifest.ts
1934
- function isVariantNode(page) {
1935
- return isVariantKey(page.key);
1936
- }
1937
- function serializeGate(when) {
1938
- if (!when) return void 0;
1939
- if (typeof when === "function") return { kind: "predicate" };
1940
- return { kind: "declarative", condition: when };
1941
- }
1942
- function compileManifest(input) {
1943
- const { funnel, pages } = input;
1944
- const keys = new Set(pages.map((p) => p.key));
1945
- const manifestPages = pages.map((p, index) => ({
1946
- id: p.key,
1947
- key: p.key,
1948
- type: p.meta?.type ?? "default",
1949
- slug: p.meta?.slug ?? p.key,
1950
- index,
1951
- variantOf: isVariantKey(p.key) ? parseSlotKey(p.key).slot : void 0
1952
- }));
1953
- const edges = [];
1954
- const badTargets = [];
1955
- const nextAnchor = (from) => {
1956
- for (let j = from + 1; j < pages.length; j++) {
1957
- if (!isVariantNode(pages[j])) return pages[j];
1958
- }
1959
- return void 0;
1960
- };
1961
- pages.forEach((p, i) => {
1962
- if (isVariantNode(p)) return;
1963
- const routes = p.meta?.next ?? [];
1964
- let hasUnconditional = false;
1965
- for (const route of routes) {
1966
- if (!keys.has(route.to)) {
1967
- badTargets.push({ from: p.key, to: route.to });
1968
- continue;
1969
- }
1970
- edges.push({ from: p.key, to: route.to, kind: "branch", condition: serializeGate(route.when) });
1971
- if (!route.when) hasUnconditional = true;
1972
- }
1973
- const next = nextAnchor(i);
1974
- if (!hasUnconditional && next && p.meta?.type !== "finish") {
1975
- edges.push({ from: p.key, to: next.key, kind: "linear" });
1976
- }
1977
- });
1978
- const startPage = pages.find((p) => !isVariantNode(p));
1979
- const start = startPage?.key ?? null;
1980
- const variantKeys = new Set(pages.filter(isVariantNode).map((p) => p.key));
1981
- const validation = validate(manifestPages, edges, badTargets, start, variantKeys);
1982
- return {
1983
- id: funnel.id,
1984
- pages: manifestPages,
1985
- flow: { start, nodes: manifestPages.map((p) => p.id), edges },
1986
- products: funnel.products ?? [],
1987
- locales: funnel.locales,
1988
- validation
1989
- };
1990
- }
1991
- function validate(pages, edges, badTargets, start, variantKeys) {
1992
- const outgoing = /* @__PURE__ */ new Map();
1993
- for (const e of edges) outgoing.set(e.from, (outgoing.get(e.from) ?? 0) + 1);
1994
- const deadEnds = pages.filter((p) => p.type !== "finish" && !variantKeys.has(p.id) && !(outgoing.get(p.id) > 0)).map((p) => p.id);
1995
- const reachable = /* @__PURE__ */ new Set();
1996
- if (start) {
1997
- const adj = /* @__PURE__ */ new Map();
1998
- for (const e of edges) {
1999
- const list = adj.get(e.from);
2000
- if (list) list.push(e.to);
2001
- else adj.set(e.from, [e.to]);
2002
- }
2003
- const stack = [start];
2004
- while (stack.length) {
2005
- const id = stack.pop();
2006
- if (reachable.has(id)) continue;
2007
- reachable.add(id);
2008
- for (const to of adj.get(id) ?? []) stack.push(to);
2009
- }
2010
- }
2011
- const unreachable = pages.filter((p) => !variantKeys.has(p.id) && !reachable.has(p.id)).map((p) => p.id);
2012
- const missingEmailCapture = !pages.some((p) => p.type === "email-capture");
2013
- return {
2014
- ok: deadEnds.length === 0 && unreachable.length === 0 && badTargets.length === 0 && !missingEmailCapture,
2015
- deadEnds,
2016
- unreachable,
2017
- badTargets,
2018
- missingEmailCapture
2019
- };
2020
- }
2021
-
2022
- export { Back, Checkout, Choice, EVENT_CATALOG, FunnelProvider, FunnelToaster, FunnelView, INLINE_SURFACES, Modal, Next, PROVIDER_PROFILES, Script, Sheet, Upsell, VariableStore, assignVariant, attachBus, bucketingSeed, buildAcquisition, buildCatalog, checkoutError, compileManifest, createBus, createConsoleTracker, createFunnelStore, createMockDriver, defineFunnel, defineModal, definePage, entryGuard, evaluateCondition, evaluateGate, expectedPathLength, fnv1a, formatProduct, hashToUnit, hideModal, integrationEvents, isInlineSurface, isIntegrationEvent, isMerchantOfRecord, isOrchestrator, isRtl, isTrackableEvent, isVariantKey, newEventId, nextPage, outgoingKeys, pageMeta, parseSlotKey, pickByWeight, registerModal, removeModal, resolveExperiments, resolveLocale, resolveProduct, resolveRoute, showModal, surfacesFor, unregisterModal, useCheckout, useClickIds, useContextValue, useData, useDevice, useExperiment, useField, useFunnel, useLocale, useModal, useNavigation, usePage, useProduct, useProducts, useResponse, useSystem, useTracker, useTranslation, useUserAttribute, useUtm, validateCheckout, validateExperiments, validateUpsell, withBus };
3704
+ export { AssetsContext, AssetsProvider, Back, Checkout, CheckoutResume, Choice, EVENT_CATALOG, FunnelProvider, FunnelToaster, FunnelView, Modal, Next, Script, Sheet, Upsell, VariableStore, attachBus, buildAcquisition, buildContext, createBus, createConsoleTracker, createFunnelStore, createFunnelTree, createMockDriver, createPlatformTracker, defineModal, driverWithEmail, hideModal, integrationEvents, isIntegrationEvent, isTrackableEvent, newEventId, registerModal, removeModal, showModal, toast, unregisterModal, useActiveLocale, useAsset, useAssets, useCheckout, useClickIds, useContextValue, useData, useDevice, useExperiment, useField, useFunnel, useLocale, useModal, useNavigation, useOffering, useOfferings, usePage, useResponse, useSystem, useTracker, useTranslation, useUserAttribute, useUtm, withBus };
2023
3705
  //# sourceMappingURL=index.js.map
2024
3706
  //# sourceMappingURL=index.js.map