@appfunnel-dev/sdk 2.0.0-canary.1 → 2.0.0-canary.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/capabilities-Dq22RCr_.d.cts +180 -0
- package/dist/capabilities-Dq22RCr_.d.ts +180 -0
- package/dist/checkout-ClaO5IYV.d.ts +333 -0
- package/dist/checkout-DBp4bCpC.d.cts +333 -0
- package/dist/chunk-7JLOF6CJ.cjs +172 -0
- package/dist/chunk-7JLOF6CJ.cjs.map +1 -0
- package/dist/chunk-EMMSS5I5.cjs +37 -0
- package/dist/chunk-EMMSS5I5.cjs.map +1 -0
- package/dist/chunk-G3PMV62Z.js +33 -0
- package/dist/chunk-G3PMV62Z.js.map +1 -0
- package/dist/chunk-JAO6AA4R.js +99 -0
- package/dist/chunk-JAO6AA4R.js.map +1 -0
- package/dist/chunk-OXQBEKZ5.js +157 -0
- package/dist/chunk-OXQBEKZ5.js.map +1 -0
- package/dist/chunk-RKPRMTSU.cjs +517 -0
- package/dist/chunk-RKPRMTSU.cjs.map +1 -0
- package/dist/chunk-SXENKZ4U.js +486 -0
- package/dist/chunk-SXENKZ4U.js.map +1 -0
- package/dist/chunk-VV7TFC64.cjs +106 -0
- package/dist/chunk-VV7TFC64.cjs.map +1 -0
- package/dist/chunk-WYUDL4FI.cjs +8 -0
- package/dist/chunk-WYUDL4FI.cjs.map +1 -0
- package/dist/chunk-ZZJG4EYL.js +6 -0
- package/dist/chunk-ZZJG4EYL.js.map +1 -0
- package/dist/driver-paddle.cjs +817 -0
- package/dist/driver-paddle.cjs.map +1 -0
- package/dist/driver-paddle.d.cts +10 -0
- package/dist/driver-paddle.d.ts +10 -0
- package/dist/driver-paddle.js +814 -0
- package/dist/driver-paddle.js.map +1 -0
- package/dist/driver-stripe.cjs +2312 -0
- package/dist/driver-stripe.cjs.map +1 -0
- package/dist/driver-stripe.d.cts +24 -0
- package/dist/driver-stripe.d.ts +24 -0
- package/dist/driver-stripe.js +2305 -0
- package/dist/driver-stripe.js.map +1 -0
- package/dist/index.cjs +2677 -846
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +358 -1005
- package/dist/index.d.ts +358 -1005
- package/dist/index.js +2395 -698
- package/dist/index.js.map +1 -1
- package/dist/manifest-C2mDXWNU.d.cts +996 -0
- package/dist/manifest-C2mDXWNU.d.ts +996 -0
- package/dist/manifest-entry.cjs +408 -0
- package/dist/manifest-entry.cjs.map +1 -0
- package/dist/manifest-entry.d.cts +192 -0
- package/dist/manifest-entry.d.ts +192 -0
- package/dist/manifest-entry.js +270 -0
- package/dist/manifest-entry.js.map +1 -0
- package/dist/protocol.cjs +13 -0
- package/dist/protocol.cjs.map +1 -0
- package/dist/protocol.d.cts +182 -0
- package/dist/protocol.d.ts +182 -0
- package/dist/protocol.js +4 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +49 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { isRtl, formatOffering, bucketingSeed, resolveExperiments, entryGuard, evaluateGate, expectedPathLength, groupProgress, nextPage, outgoingKeys, buildCatalog, resolveLocale } from './chunk-SXENKZ4U.js';
|
|
2
|
+
export { assignVariant, bucketingSeed, buildCatalog, compileManifest, currencyExponent, defineFunnel, definePage, entryGuard, evaluateCondition, evaluateGate, expectedPathLength, fnv1a, formatMoney, formatOffering, groupProgress, hashToUnit, isRtl, isVariantKey, nextPage, outgoingKeys, pageMeta, parseSlotKey, pickByWeight, resolveExperiments, resolveLocale, resolveOffering, resolveRoute } from './chunk-SXENKZ4U.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
|
-
|
|
261
|
-
|
|
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
|
|
281
|
+
const now2 = opts.now ?? 0;
|
|
275
282
|
const referrer = typeof document !== "undefined" ? document.referrer : "";
|
|
276
|
-
const
|
|
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:
|
|
284
|
-
session: { id: opts.sessionId ?? null, startedAt:
|
|
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
|
|
300
|
+
if (!opts.overrides) return base;
|
|
294
301
|
return {
|
|
295
|
-
...
|
|
302
|
+
...base,
|
|
296
303
|
...opts.overrides,
|
|
297
|
-
device: { ...
|
|
298
|
-
browser: { ...
|
|
299
|
-
os: { ...
|
|
300
|
-
locale: { ...
|
|
301
|
-
utm: { ...
|
|
302
|
-
clickIds: { ...
|
|
303
|
-
page: { ...
|
|
304
|
-
session: { ...
|
|
305
|
-
identity: { ...
|
|
306
|
-
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,
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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) =>
|
|
495
|
-
|
|
496
|
-
|
|
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,256 +590,174 @@ function useCatalog() {
|
|
|
597
590
|
return useContext(CatalogContext) ?? EMPTY;
|
|
598
591
|
}
|
|
599
592
|
var EMPTY = /* @__PURE__ */ new Map();
|
|
600
|
-
function
|
|
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(
|
|
597
|
+
return useMemo(
|
|
598
|
+
() => resolved ? formatOffering(resolved, locale) : void 0,
|
|
599
|
+
[resolved, locale]
|
|
600
|
+
);
|
|
605
601
|
}
|
|
606
|
-
function
|
|
602
|
+
function useOfferings() {
|
|
607
603
|
const catalog = useCatalog();
|
|
608
604
|
const locale = useActiveLocale();
|
|
609
605
|
return useMemo(
|
|
610
|
-
() => Array.from(catalog.values()).map((r) =>
|
|
606
|
+
() => Array.from(catalog.values()).map((r) => formatOffering(r, locale)),
|
|
611
607
|
[catalog, locale]
|
|
612
608
|
);
|
|
613
609
|
}
|
|
614
610
|
|
|
615
|
-
// src/flow/
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
function
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
|
717
|
-
}
|
|
718
|
-
function
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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 (
|
|
814
|
-
if (
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
|
831
706
|
var NavContext = createContext(null);
|
|
707
|
+
var GroupProgressContext = createContext(null);
|
|
832
708
|
var ExperimentContext = createContext({});
|
|
833
709
|
function useExperiment(id) {
|
|
834
710
|
return useContext(ExperimentContext)[id];
|
|
835
711
|
}
|
|
836
|
-
function pageContextFor(currentKey, slug, index, total, startedAt) {
|
|
712
|
+
function pageContextFor(currentKey, slug, group, index, total, startedAt) {
|
|
837
713
|
const denom = Math.max(total, index + 1);
|
|
838
714
|
return {
|
|
839
715
|
key: currentKey,
|
|
840
716
|
slug,
|
|
717
|
+
group,
|
|
841
718
|
index,
|
|
842
719
|
total: denom,
|
|
843
720
|
progressPercentage: denom > 0 ? Math.min(100, Math.round((index + 1) / denom * 100)) : 0,
|
|
844
721
|
startedAt
|
|
845
722
|
};
|
|
846
723
|
}
|
|
724
|
+
var now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
725
|
+
function usePageExitTimer(tracker, currentKey) {
|
|
726
|
+
const timer = useRef(null);
|
|
727
|
+
const emitExit = useCallback(() => {
|
|
728
|
+
const t = timer.current;
|
|
729
|
+
if (!t) return;
|
|
730
|
+
const at = now();
|
|
731
|
+
const hidden = t.hidden + (t.hiddenSince != null ? at - t.hiddenSince : 0);
|
|
732
|
+
const durationMs = Math.round(at - t.enteredAt);
|
|
733
|
+
tracker.track("page.exit", { pageId: t.key, durationMs, activeMs: Math.max(0, Math.round(durationMs - hidden)) });
|
|
734
|
+
}, [tracker]);
|
|
735
|
+
useEffect(() => {
|
|
736
|
+
if (typeof document === "undefined") return;
|
|
737
|
+
const onVis = () => {
|
|
738
|
+
const t = timer.current;
|
|
739
|
+
if (!t) return;
|
|
740
|
+
if (document.visibilityState === "hidden") t.hiddenSince = now();
|
|
741
|
+
else if (t.hiddenSince != null) {
|
|
742
|
+
t.hidden += now() - t.hiddenSince;
|
|
743
|
+
t.hiddenSince = null;
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
document.addEventListener("visibilitychange", onVis);
|
|
747
|
+
return () => document.removeEventListener("visibilitychange", onVis);
|
|
748
|
+
}, []);
|
|
749
|
+
useEffect(() => {
|
|
750
|
+
if (!currentKey) return;
|
|
751
|
+
if (timer.current && timer.current.key !== currentKey) emitExit();
|
|
752
|
+
const hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
753
|
+
timer.current = { key: currentKey, enteredAt: now(), hidden: 0, hiddenSince: hidden ? now() : null };
|
|
754
|
+
}, [currentKey, emitExit]);
|
|
755
|
+
useEffect(() => () => emitExit(), [emitExit]);
|
|
756
|
+
}
|
|
847
757
|
function FunnelView({
|
|
848
758
|
pages,
|
|
849
759
|
initialKey,
|
|
760
|
+
trustInitialKey = false,
|
|
850
761
|
layout,
|
|
851
762
|
fallback,
|
|
852
763
|
prefetch: prefetchMode = "auto",
|
|
@@ -871,28 +782,38 @@ function FunnelView({
|
|
|
871
782
|
() => new Map(pages.filter((p) => p.load).map((p) => [p.key, p.load])),
|
|
872
783
|
[pages]
|
|
873
784
|
);
|
|
874
|
-
const
|
|
785
|
+
const seedRef = useRef(null);
|
|
786
|
+
if (seedRef.current === null) seedRef.current = bucketingSeed(funnel.context);
|
|
787
|
+
const seed = seedRef.current;
|
|
875
788
|
const experiments = useMemo(
|
|
876
789
|
() => resolveExperiments(pages, funnel.experiments, seed),
|
|
877
790
|
[pages, funnel.experiments, seed]
|
|
878
791
|
);
|
|
792
|
+
const toSlot = useCallback((k) => experiments.slotOf[k] ?? k, [experiments]);
|
|
793
|
+
const toRenderKey = useCallback((k) => experiments.render[k] ?? k, [experiments]);
|
|
879
794
|
const pageByKey = useMemo(() => new Map(pages.map((p) => [p.key, p])), [pages]);
|
|
880
795
|
const flowPages = useMemo(
|
|
881
|
-
() => experiments.activeKeys.map((k) => pageByKey.get(k)).filter(
|
|
796
|
+
() => experiments.activeKeys.map((k) => pageByKey.get(k)).filter((p) => p !== void 0),
|
|
882
797
|
[experiments, pageByKey]
|
|
883
798
|
);
|
|
884
799
|
const flow = useMemo(
|
|
885
|
-
() => flowPages.map((p) =>
|
|
886
|
-
|
|
800
|
+
() => flowPages.map((p) => {
|
|
801
|
+
const servedVariant = experiments.render[p.key];
|
|
802
|
+
const variantNext = servedVariant ? pageByKey.get(servedVariant)?.meta?.next : void 0;
|
|
803
|
+
return variantNext ? { key: p.key, meta: { ...p.meta ?? {}, next: variantNext } } : { key: p.key, meta: p.meta };
|
|
804
|
+
}),
|
|
805
|
+
[flowPages, experiments, pageByKey]
|
|
887
806
|
);
|
|
888
807
|
const [history, setHistory] = useState(() => {
|
|
889
808
|
const start = flowPages[0]?.key;
|
|
890
|
-
if (!initialKey || initialKey === start) return [start]
|
|
891
|
-
const target =
|
|
809
|
+
if (!initialKey || initialKey === start) return start ? [start] : [];
|
|
810
|
+
const target = toSlot(initialKey);
|
|
892
811
|
const page2 = flowPages.find((p) => p.key === target);
|
|
812
|
+
if (trustInitialKey) return page2 ? [target] : start ? [start] : [];
|
|
893
813
|
const guard = entryGuard(page2?.meta);
|
|
894
814
|
const ok = page2 && (!guard || evaluateGate(guard, funnel.snapshot()));
|
|
895
|
-
|
|
815
|
+
const first = ok ? target : start;
|
|
816
|
+
return first ? [first] : [];
|
|
896
817
|
});
|
|
897
818
|
const currentKey = history[history.length - 1];
|
|
898
819
|
const index = history.length - 1;
|
|
@@ -903,11 +824,19 @@ function FunnelView({
|
|
|
903
824
|
[flow, startKey, currentKey, funnel]
|
|
904
825
|
);
|
|
905
826
|
const slug = pageByKey.get(currentKey)?.meta?.slug ?? currentKey;
|
|
827
|
+
const group = pageByKey.get(currentKey)?.meta?.group;
|
|
906
828
|
const page = useMemo(
|
|
907
|
-
() => pageContextFor(currentKey, slug, index, total, funnel.context.system.now),
|
|
908
|
-
[currentKey, slug, index, total, funnel.context.system.now]
|
|
829
|
+
() => pageContextFor(currentKey, slug, group, index, total, funnel.context.system.now),
|
|
830
|
+
[currentKey, slug, group, index, total, funnel.context.system.now]
|
|
909
831
|
);
|
|
910
|
-
|
|
832
|
+
const groups = useMemo(
|
|
833
|
+
() => groupProgress(flow, startKey, currentKey, funnel.snapshot()),
|
|
834
|
+
// currentKey in deps: re-trace as each step's answers settle the branch.
|
|
835
|
+
[flow, startKey, currentKey, funnel]
|
|
836
|
+
);
|
|
837
|
+
useEffect(() => {
|
|
838
|
+
funnel.context.page = page;
|
|
839
|
+
}, [funnel, page]);
|
|
911
840
|
const started = useRef(false);
|
|
912
841
|
useEffect(() => {
|
|
913
842
|
if (started.current) return;
|
|
@@ -926,67 +855,37 @@ function FunnelView({
|
|
|
926
855
|
const expId = experiments.experimentOf[currentKey];
|
|
927
856
|
if (!expId || exposed.current.has(expId)) return;
|
|
928
857
|
exposed.current.add(expId);
|
|
929
|
-
tracker.track("experiment.exposure",
|
|
858
|
+
tracker.track("experiment.exposure", exposurePayload(expId, experiments.assignments[expId], experiments.versionOf[expId]));
|
|
930
859
|
}, [tracker, currentKey, experiments]);
|
|
931
|
-
|
|
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]);
|
|
860
|
+
usePageExitTimer(tracker, currentKey);
|
|
962
861
|
const [isNavigating, startNav] = useTransition();
|
|
963
862
|
const next = useCallback(() => {
|
|
964
863
|
const target = nextPage(flow, currentKey, funnel.snapshot());
|
|
965
|
-
const resolved = target ?
|
|
864
|
+
const resolved = target ? toSlot(target) : target;
|
|
966
865
|
if (resolved) startNav(() => setHistory((h) => [...h, resolved]));
|
|
967
|
-
}, [flow, currentKey, funnel,
|
|
866
|
+
}, [flow, currentKey, funnel, toSlot]);
|
|
968
867
|
const back = useCallback(
|
|
969
868
|
() => startNav(() => setHistory((h) => h.length > 1 ? h.slice(0, -1) : h)),
|
|
970
869
|
[]
|
|
971
870
|
);
|
|
972
871
|
const go = useCallback(
|
|
973
|
-
(key) => startNav(() => setHistory((h) => [...h,
|
|
974
|
-
[
|
|
872
|
+
(key) => startNav(() => setHistory((h) => [...h, toSlot(key)])),
|
|
873
|
+
[toSlot]
|
|
975
874
|
);
|
|
976
875
|
const prefetched = useRef(/* @__PURE__ */ new Set());
|
|
977
876
|
const prefetch = useCallback(
|
|
978
877
|
(key) => {
|
|
979
|
-
const target =
|
|
878
|
+
const target = toRenderKey(key);
|
|
980
879
|
const load = loaders.get(target);
|
|
981
880
|
if (!load || prefetched.current.has(target)) return;
|
|
982
881
|
prefetched.current.add(target);
|
|
983
882
|
load().catch(() => prefetched.current.delete(target));
|
|
984
883
|
},
|
|
985
|
-
[loaders,
|
|
884
|
+
[loaders, toRenderKey]
|
|
986
885
|
);
|
|
987
886
|
const nextCandidates = useMemo(
|
|
988
|
-
() => [...new Set(outgoingKeys(flow, currentKey).map(
|
|
989
|
-
[flow, currentKey,
|
|
887
|
+
() => [...new Set(outgoingKeys(flow, currentKey).map(toRenderKey))],
|
|
888
|
+
[flow, currentKey, toRenderKey]
|
|
990
889
|
);
|
|
991
890
|
useEffect(() => {
|
|
992
891
|
if (prefetchMode !== "auto") return;
|
|
@@ -1002,14 +901,56 @@ function FunnelView({
|
|
|
1002
901
|
canGoBack: history.length > 1,
|
|
1003
902
|
isNavigating
|
|
1004
903
|
};
|
|
1005
|
-
const
|
|
904
|
+
const bus = useBusInternal();
|
|
905
|
+
const navRef = useRef(value);
|
|
906
|
+
navRef.current = value;
|
|
907
|
+
useEffect(() => {
|
|
908
|
+
if (!bus) return;
|
|
909
|
+
const control = {
|
|
910
|
+
goTo: (key) => {
|
|
911
|
+
if (!components.has(toRenderKey(key))) return false;
|
|
912
|
+
navRef.current.go(key);
|
|
913
|
+
return true;
|
|
914
|
+
},
|
|
915
|
+
goNext: () => navRef.current.next(),
|
|
916
|
+
goPrevious: () => navRef.current.back(),
|
|
917
|
+
snapshot: () => {
|
|
918
|
+
const v = navRef.current;
|
|
919
|
+
return {
|
|
920
|
+
key: v.page.key,
|
|
921
|
+
index: v.page.index,
|
|
922
|
+
total: v.page.total,
|
|
923
|
+
progressPercentage: v.page.progressPercentage,
|
|
924
|
+
canGoBack: v.canGoBack,
|
|
925
|
+
nextCandidates: v.nextCandidates
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
return bus.registerNavigation(control);
|
|
930
|
+
}, [bus]);
|
|
931
|
+
useEffect(() => {
|
|
932
|
+
const v = navRef.current;
|
|
933
|
+
bus?.emit("navigation", {
|
|
934
|
+
key: v.page.key,
|
|
935
|
+
index: v.page.index,
|
|
936
|
+
total: v.page.total,
|
|
937
|
+
progressPercentage: v.page.progressPercentage,
|
|
938
|
+
canGoBack: v.canGoBack,
|
|
939
|
+
nextCandidates: v.nextCandidates
|
|
940
|
+
});
|
|
941
|
+
}, [bus, currentKey]);
|
|
942
|
+
const renderKey = toRenderKey(currentKey);
|
|
1006
943
|
const Current = components.get(renderKey);
|
|
1007
944
|
const pageEl = createElement(Suspense, { fallback: fallback ?? null }, Current ? createElement(Current) : null);
|
|
1008
945
|
const content = layout ? createElement(layout, null, pageEl) : pageEl;
|
|
1009
946
|
return createElement(
|
|
1010
947
|
ExperimentContext.Provider,
|
|
1011
948
|
{ value: experiments.assignments },
|
|
1012
|
-
createElement(
|
|
949
|
+
createElement(
|
|
950
|
+
NavContext.Provider,
|
|
951
|
+
{ value },
|
|
952
|
+
createElement(GroupProgressContext.Provider, { value: groups }, content)
|
|
953
|
+
)
|
|
1013
954
|
);
|
|
1014
955
|
}
|
|
1015
956
|
function useNavigation() {
|
|
@@ -1017,21 +958,109 @@ function useNavigation() {
|
|
|
1017
958
|
if (!ctx) throw new Error("useNavigation must be used inside <FunnelView>");
|
|
1018
959
|
return ctx;
|
|
1019
960
|
}
|
|
961
|
+
var EMBED_CAPABILITIES = {
|
|
962
|
+
methods: ["nav.snapshot", "nav.goTo", "nav.goNext", "nav.goPrevious", "screenshot"],
|
|
963
|
+
events: ["navigation"]
|
|
964
|
+
};
|
|
965
|
+
function EmbedBridge() {
|
|
966
|
+
const bus = useBusInternal();
|
|
967
|
+
useEffect(() => {
|
|
968
|
+
if (!bus || typeof window === "undefined" || window.parent === window) return;
|
|
969
|
+
const parent = window.parent;
|
|
970
|
+
let disposed = false;
|
|
971
|
+
const send = (msg) => {
|
|
972
|
+
try {
|
|
973
|
+
parent.postMessage({ af: AF_PROTOCOL, source: "af-preview", ...msg }, "*");
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
const hello = () => send({ kind: "hello", role: "funnel", protocol: AF_PROTOCOL, capabilities: EMBED_CAPABILITIES });
|
|
978
|
+
const sendSnapshot = () => {
|
|
979
|
+
const s = bus.navigation();
|
|
980
|
+
if (s) send({ kind: "event", event: "navigation", data: s });
|
|
981
|
+
};
|
|
982
|
+
async function invoke(method, params) {
|
|
983
|
+
switch (method) {
|
|
984
|
+
case "nav.snapshot":
|
|
985
|
+
return bus.navigation();
|
|
986
|
+
case "nav.goTo": {
|
|
987
|
+
const key = params?.key;
|
|
988
|
+
if (typeof key !== "string" || !bus.goTo(key)) throw { code: "bad_params", message: `unknown page key: ${String(key)}` };
|
|
989
|
+
return void 0;
|
|
990
|
+
}
|
|
991
|
+
case "nav.goNext":
|
|
992
|
+
bus.goNext();
|
|
993
|
+
return void 0;
|
|
994
|
+
case "nav.goPrevious":
|
|
995
|
+
bus.goPrevious();
|
|
996
|
+
return void 0;
|
|
997
|
+
case "screenshot":
|
|
998
|
+
return await captureScreenshot(params ?? {});
|
|
999
|
+
default:
|
|
1000
|
+
throw { code: "unknown_method", message: `unknown method: ${method}` };
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async function onMsg(e) {
|
|
1004
|
+
if (e.source !== parent) return;
|
|
1005
|
+
const d = e.data;
|
|
1006
|
+
if (!d || typeof d !== "object" || d.af !== AF_PROTOCOL || d.source !== "af-host") return;
|
|
1007
|
+
if (d.kind === "hello") {
|
|
1008
|
+
hello();
|
|
1009
|
+
sendSnapshot();
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (d.kind !== "request" || typeof d.id !== "string") return;
|
|
1013
|
+
try {
|
|
1014
|
+
const result = await invoke(d.method, d.params);
|
|
1015
|
+
if (!disposed) send({ kind: "response", id: d.id, ok: true, result });
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
const e2 = err;
|
|
1018
|
+
const error = e2 && typeof e2.code === "string" ? { code: e2.code, message: String(e2.message ?? e2.code) } : { code: "failed", message: String(e2?.message ?? err) };
|
|
1019
|
+
if (!disposed) send({ kind: "response", id: d.id, ok: false, error });
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
window.addEventListener("message", onMsg);
|
|
1023
|
+
const offNav = bus.on("navigation", (data) => {
|
|
1024
|
+
if (!disposed) send({ kind: "event", event: "navigation", data });
|
|
1025
|
+
});
|
|
1026
|
+
hello();
|
|
1027
|
+
sendSnapshot();
|
|
1028
|
+
return () => {
|
|
1029
|
+
disposed = true;
|
|
1030
|
+
offNav();
|
|
1031
|
+
window.removeEventListener("message", onMsg);
|
|
1032
|
+
};
|
|
1033
|
+
}, [bus]);
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1020
1036
|
function usePage() {
|
|
1021
1037
|
return useNavigation().page;
|
|
1022
1038
|
}
|
|
1023
|
-
|
|
1039
|
+
function useGroupProgress() {
|
|
1040
|
+
const ctx = useContext(GroupProgressContext);
|
|
1041
|
+
if (!ctx) throw new Error("useGroupProgress must be used inside <FunnelView>");
|
|
1042
|
+
return ctx;
|
|
1043
|
+
}
|
|
1044
|
+
var symModalId = /* @__PURE__ */ Symbol("AppfunnelModalId");
|
|
1024
1045
|
var initialState = {};
|
|
1025
1046
|
var ModalStateContext = createContext(initialState);
|
|
1026
1047
|
var ModalIdContext = createContext(null);
|
|
1048
|
+
var ModalDispatchContext = createContext(null);
|
|
1027
1049
|
var REGISTRY = {};
|
|
1028
1050
|
var ALREADY_MOUNTED = {};
|
|
1029
1051
|
var modalCallbacks = {};
|
|
1030
1052
|
var hideCallbacks = {};
|
|
1031
1053
|
var uid = 0;
|
|
1032
1054
|
var getUid = () => `_af_modal_${uid++}`;
|
|
1033
|
-
var
|
|
1034
|
-
|
|
1055
|
+
var runtimeStack = [];
|
|
1056
|
+
var dispatch = (action) => {
|
|
1057
|
+
const top = runtimeStack[runtimeStack.length - 1];
|
|
1058
|
+
if (!top) {
|
|
1059
|
+
throw new Error(
|
|
1060
|
+
"No modal dispatch \u2014 render inside <FunnelProvider> (which mounts the modal runtime)."
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
top(action);
|
|
1035
1064
|
};
|
|
1036
1065
|
var modalReducer = (state = initialState, action) => {
|
|
1037
1066
|
const { modalId, args, flags } = action.payload;
|
|
@@ -1074,10 +1103,10 @@ function registerModal(id, comp, props) {
|
|
|
1074
1103
|
function unregisterModal(id) {
|
|
1075
1104
|
delete REGISTRY[id];
|
|
1076
1105
|
}
|
|
1077
|
-
function
|
|
1106
|
+
function showModalVia(d, modal, args) {
|
|
1078
1107
|
const id = idOf(modal);
|
|
1079
1108
|
if (typeof modal !== "string" && !REGISTRY[id]) registerModal(id, modal);
|
|
1080
|
-
|
|
1109
|
+
d({ type: "show", payload: { modalId: id, args } });
|
|
1081
1110
|
if (!modalCallbacks[id]) {
|
|
1082
1111
|
let res;
|
|
1083
1112
|
let rej;
|
|
@@ -1089,9 +1118,12 @@ function showModal(modal, args) {
|
|
|
1089
1118
|
}
|
|
1090
1119
|
return modalCallbacks[id].promise;
|
|
1091
1120
|
}
|
|
1092
|
-
function
|
|
1121
|
+
function showModal(modal, args) {
|
|
1122
|
+
return showModalVia(dispatch, modal, args);
|
|
1123
|
+
}
|
|
1124
|
+
function hideModalVia(d, modal) {
|
|
1093
1125
|
const id = idOf(modal);
|
|
1094
|
-
|
|
1126
|
+
d({ type: "hide", payload: { modalId: id } });
|
|
1095
1127
|
delete modalCallbacks[id];
|
|
1096
1128
|
if (!hideCallbacks[id]) {
|
|
1097
1129
|
let res;
|
|
@@ -1104,40 +1136,63 @@ function hideModal(modal) {
|
|
|
1104
1136
|
}
|
|
1105
1137
|
return hideCallbacks[id].promise;
|
|
1106
1138
|
}
|
|
1107
|
-
function
|
|
1139
|
+
function hideModal(modal) {
|
|
1140
|
+
return hideModalVia(dispatch, modal);
|
|
1141
|
+
}
|
|
1142
|
+
function removeModalVia(d, modal) {
|
|
1108
1143
|
const id = idOf(modal);
|
|
1109
|
-
|
|
1144
|
+
d({ type: "remove", payload: { modalId: id } });
|
|
1110
1145
|
delete modalCallbacks[id];
|
|
1111
1146
|
delete hideCallbacks[id];
|
|
1112
1147
|
}
|
|
1113
|
-
function
|
|
1114
|
-
dispatch
|
|
1148
|
+
function removeModal(modal) {
|
|
1149
|
+
removeModalVia(dispatch, modal);
|
|
1150
|
+
}
|
|
1151
|
+
function setFlags(d, id, flags) {
|
|
1152
|
+
d({ type: "set-flags", payload: { modalId: id, flags } });
|
|
1115
1153
|
}
|
|
1116
1154
|
function useModal(modal, args) {
|
|
1117
1155
|
const modals = useContext(ModalStateContext);
|
|
1118
1156
|
const contextId = useContext(ModalIdContext);
|
|
1157
|
+
const d = useContext(ModalDispatchContext) ?? dispatch;
|
|
1119
1158
|
const isComponent = !!modal && typeof modal !== "string";
|
|
1120
1159
|
const id = modal ? idOf(modal) : contextId;
|
|
1121
|
-
if (!id)
|
|
1160
|
+
if (!id)
|
|
1161
|
+
throw new Error(
|
|
1162
|
+
"useModal: no modal id (call inside a defineModal component, or pass an id/component)."
|
|
1163
|
+
);
|
|
1122
1164
|
useEffect(() => {
|
|
1123
|
-
if (isComponent && !REGISTRY[id])
|
|
1165
|
+
if (isComponent && !REGISTRY[id])
|
|
1166
|
+
registerModal(id, modal, args);
|
|
1124
1167
|
}, [isComponent, id, modal, args]);
|
|
1125
1168
|
const info = modals[id];
|
|
1126
|
-
const show = useCallback(
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1169
|
+
const show = useCallback(
|
|
1170
|
+
(a) => showModalVia(d, id, a),
|
|
1171
|
+
[d, id]
|
|
1172
|
+
);
|
|
1173
|
+
const hide = useCallback(() => hideModalVia(d, id), [d, id]);
|
|
1174
|
+
const remove = useCallback(() => removeModalVia(d, id), [d, id]);
|
|
1175
|
+
const resolve = useCallback(
|
|
1176
|
+
(v) => {
|
|
1177
|
+
modalCallbacks[id]?.resolve(v);
|
|
1178
|
+
delete modalCallbacks[id];
|
|
1179
|
+
},
|
|
1180
|
+
[id]
|
|
1181
|
+
);
|
|
1182
|
+
const reject = useCallback(
|
|
1183
|
+
(v) => {
|
|
1184
|
+
modalCallbacks[id]?.reject(v);
|
|
1185
|
+
delete modalCallbacks[id];
|
|
1186
|
+
},
|
|
1187
|
+
[id]
|
|
1188
|
+
);
|
|
1189
|
+
const resolveHide = useCallback(
|
|
1190
|
+
(v) => {
|
|
1191
|
+
hideCallbacks[id]?.resolve(v);
|
|
1192
|
+
delete hideCallbacks[id];
|
|
1193
|
+
},
|
|
1194
|
+
[id]
|
|
1195
|
+
);
|
|
1141
1196
|
return useMemo(
|
|
1142
1197
|
() => ({
|
|
1143
1198
|
id,
|
|
@@ -1151,13 +1206,30 @@ function useModal(modal, args) {
|
|
|
1151
1206
|
reject,
|
|
1152
1207
|
resolveHide
|
|
1153
1208
|
}),
|
|
1154
|
-
[
|
|
1209
|
+
[
|
|
1210
|
+
id,
|
|
1211
|
+
info?.args,
|
|
1212
|
+
info?.visible,
|
|
1213
|
+
info?.keepMounted,
|
|
1214
|
+
show,
|
|
1215
|
+
hide,
|
|
1216
|
+
remove,
|
|
1217
|
+
resolve,
|
|
1218
|
+
reject,
|
|
1219
|
+
resolveHide
|
|
1220
|
+
]
|
|
1155
1221
|
);
|
|
1156
1222
|
}
|
|
1157
1223
|
function defineModal(Comp) {
|
|
1158
|
-
return function Wrapped({
|
|
1224
|
+
return function Wrapped({
|
|
1225
|
+
id,
|
|
1226
|
+
defaultVisible,
|
|
1227
|
+
keepMounted,
|
|
1228
|
+
...props
|
|
1229
|
+
}) {
|
|
1159
1230
|
const { args, show } = useModal(id);
|
|
1160
1231
|
const modals = useContext(ModalStateContext);
|
|
1232
|
+
const d = useContext(ModalDispatchContext) ?? dispatch;
|
|
1161
1233
|
const shouldMount = !!modals[id];
|
|
1162
1234
|
useEffect(() => {
|
|
1163
1235
|
if (defaultVisible) show();
|
|
@@ -1167,8 +1239,8 @@ function defineModal(Comp) {
|
|
|
1167
1239
|
};
|
|
1168
1240
|
}, [id, show, defaultVisible]);
|
|
1169
1241
|
useEffect(() => {
|
|
1170
|
-
if (keepMounted) setFlags(id, { keepMounted: true });
|
|
1171
|
-
}, [id, keepMounted]);
|
|
1242
|
+
if (keepMounted) setFlags(d, id, { keepMounted: true });
|
|
1243
|
+
}, [d, id, keepMounted]);
|
|
1172
1244
|
const delayVisible = modals[id]?.delayVisible;
|
|
1173
1245
|
useEffect(() => {
|
|
1174
1246
|
if (delayVisible) show(args);
|
|
@@ -1187,17 +1259,37 @@ function ModalRoot() {
|
|
|
1187
1259
|
return createElement(
|
|
1188
1260
|
"div",
|
|
1189
1261
|
{ "data-appfunnel-modal-root": "" },
|
|
1190
|
-
...toRender.map(
|
|
1262
|
+
...toRender.map(
|
|
1263
|
+
(t) => createElement(t.comp, { key: t.id, id: t.id, ...t.props })
|
|
1264
|
+
)
|
|
1191
1265
|
);
|
|
1192
1266
|
}
|
|
1193
1267
|
function ModalRuntime({ children }) {
|
|
1194
1268
|
const [state, localDispatch] = useReducer(modalReducer, initialState);
|
|
1195
|
-
|
|
1269
|
+
useEffect(() => {
|
|
1270
|
+
runtimeStack.push(localDispatch);
|
|
1271
|
+
if (runtimeStack.length > 1) {
|
|
1272
|
+
try {
|
|
1273
|
+
console.warn(
|
|
1274
|
+
`[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.`
|
|
1275
|
+
);
|
|
1276
|
+
} catch {
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return () => {
|
|
1280
|
+
const i = runtimeStack.indexOf(localDispatch);
|
|
1281
|
+
if (i !== -1) runtimeStack.splice(i, 1);
|
|
1282
|
+
};
|
|
1283
|
+
}, []);
|
|
1196
1284
|
return createElement(
|
|
1197
|
-
|
|
1198
|
-
{ value:
|
|
1199
|
-
|
|
1200
|
-
|
|
1285
|
+
ModalDispatchContext.Provider,
|
|
1286
|
+
{ value: localDispatch },
|
|
1287
|
+
createElement(
|
|
1288
|
+
ModalStateContext.Provider,
|
|
1289
|
+
{ value: state },
|
|
1290
|
+
children,
|
|
1291
|
+
createElement(ModalRoot, null)
|
|
1292
|
+
)
|
|
1201
1293
|
);
|
|
1202
1294
|
}
|
|
1203
1295
|
|
|
@@ -1296,110 +1388,21 @@ function Modal(props) {
|
|
|
1296
1388
|
}
|
|
1297
1389
|
|
|
1298
1390
|
// src/commerce/checkout.tsx
|
|
1299
|
-
var INLINE_SURFACES = /* @__PURE__ */ new Set([
|
|
1300
|
-
"express",
|
|
1301
|
-
"element",
|
|
1302
|
-
"embedded"
|
|
1303
|
-
]);
|
|
1304
|
-
function isInlineSurface(surface) {
|
|
1305
|
-
return INLINE_SURFACES.has(surface);
|
|
1306
|
-
}
|
|
1307
|
-
var ON = { purchase: true, wallets: true, offSession: "reliable" };
|
|
1308
|
-
var MANAGED = { purchase: true, wallets: true, offSession: "conditional" };
|
|
1309
|
-
var ALL_UPSELLS = ["subscription", "one-time", "upgrade"];
|
|
1310
|
-
var PROVIDER_PROFILES = {
|
|
1311
|
-
// PSP; you hold the PaymentMethod and fire your own off-session PaymentIntent on
|
|
1312
|
-
// the Elements surfaces (reliable). **Managed** Checkout (`embedded`/`redirect`)
|
|
1313
|
-
// upsells too, but as a direct charge against Stripe's vaulted card → conditional.
|
|
1314
|
-
// No provider "popup" surface.
|
|
1315
|
-
stripe: {
|
|
1316
|
-
isMoR: false,
|
|
1317
|
-
offSessionUpsells: ALL_UPSELLS,
|
|
1318
|
-
tokenModel: "merchant",
|
|
1319
|
-
orchestrator: false,
|
|
1320
|
-
surfaces: { express: ON, element: ON, sheet: ON, embedded: MANAGED, redirect: MANAGED }
|
|
1321
|
-
},
|
|
1322
|
-
// Merchant-of-Record; card entry is always Paddle's frame → no own element/sheet/
|
|
1323
|
-
// express. Off-session via subscription one-time charge / collection_mode, but
|
|
1324
|
-
// **no 2nd concurrent subscription** (upgrade + one-time only), provider-initiated.
|
|
1325
|
-
paddle: {
|
|
1326
|
-
isMoR: true,
|
|
1327
|
-
offSessionUpsells: ["one-time", "upgrade"],
|
|
1328
|
-
tokenModel: "provider",
|
|
1329
|
-
orchestrator: false,
|
|
1330
|
-
surfaces: { embedded: ON, popup: ON, redirect: ON }
|
|
1331
|
-
},
|
|
1332
|
-
// Merchant-of-Record; iframe-only (no raw card field). Off-session works
|
|
1333
|
-
// (provider-initiated, async via webhooks). `sheet` = our chrome hosting their embed.
|
|
1334
|
-
whop: {
|
|
1335
|
-
isMoR: true,
|
|
1336
|
-
offSessionUpsells: ALL_UPSELLS,
|
|
1337
|
-
tokenModel: "provider",
|
|
1338
|
-
orchestrator: false,
|
|
1339
|
-
surfaces: { embedded: ON, redirect: ON, sheet: ON }
|
|
1340
|
-
},
|
|
1341
|
-
// Pure orchestrator (PSP-side); Headless renders express/element/sheet/embedded.
|
|
1342
|
-
// No hosted redirect page, no popup checkout. Off-session is PSP-conditional.
|
|
1343
|
-
primer: {
|
|
1344
|
-
isMoR: false,
|
|
1345
|
-
offSessionUpsells: ALL_UPSELLS,
|
|
1346
|
-
tokenModel: "merchant",
|
|
1347
|
-
orchestrator: true,
|
|
1348
|
-
surfaces: { express: ON, element: ON, sheet: ON, embedded: ON }
|
|
1349
|
-
},
|
|
1350
|
-
// PSP **and** orchestrator (own acquiring + connectors routing across Stripe/Adyen/
|
|
1351
|
-
// PayPal). One Payment Form (element/sheet/embedded) + hosted page + wallet button.
|
|
1352
|
-
// Strong native off-session (Recurring API). No popup.
|
|
1353
|
-
solidgate: {
|
|
1354
|
-
isMoR: false,
|
|
1355
|
-
offSessionUpsells: ALL_UPSELLS,
|
|
1356
|
-
tokenModel: "merchant",
|
|
1357
|
-
orchestrator: true,
|
|
1358
|
-
surfaces: { express: ON, element: ON, sheet: ON, embedded: ON, redirect: ON }
|
|
1359
|
-
}
|
|
1360
|
-
};
|
|
1361
|
-
function surfacesFor(provider) {
|
|
1362
|
-
return Object.keys(PROVIDER_PROFILES[provider]?.surfaces ?? {});
|
|
1363
|
-
}
|
|
1364
|
-
function isMerchantOfRecord(provider) {
|
|
1365
|
-
return PROVIDER_PROFILES[provider]?.isMoR ?? false;
|
|
1366
|
-
}
|
|
1367
|
-
function isOrchestrator(provider) {
|
|
1368
|
-
return PROVIDER_PROFILES[provider]?.orchestrator ?? false;
|
|
1369
|
-
}
|
|
1370
|
-
function validateCheckout(provider, surface) {
|
|
1371
|
-
const cap = PROVIDER_PROFILES[provider]?.surfaces[surface];
|
|
1372
|
-
if (!cap) return { ok: false, reason: `${provider} has no '${surface}' surface` };
|
|
1373
|
-
return cap.purchase ? { ok: true } : { ok: false, reason: `'${surface}' can't take a purchase on ${provider}` };
|
|
1374
|
-
}
|
|
1375
|
-
function validateUpsell(provider, paywallSurface, kind = "subscription") {
|
|
1376
|
-
const profile = PROVIDER_PROFILES[provider];
|
|
1377
|
-
if (!profile) return { ok: false, reason: `unknown provider '${provider}'` };
|
|
1378
|
-
if (!profile.offSessionUpsells.includes(kind)) {
|
|
1379
|
-
const can = profile.offSessionUpsells.length ? ` (it supports: ${profile.offSessionUpsells.join(", ")})` : "";
|
|
1380
|
-
return { ok: false, reason: `${provider} can't charge a '${kind}' upsell off-session${can}` };
|
|
1381
|
-
}
|
|
1382
|
-
const cap = profile.surfaces[paywallSurface];
|
|
1383
|
-
if (!cap) return { ok: false, reason: `${provider} has no '${paywallSurface}' surface to upsell after` };
|
|
1384
|
-
if (cap.offSession === "none") {
|
|
1385
|
-
return { ok: false, reason: `a purchase on '${paywallSurface}' retains no chargeable method for an upsell` };
|
|
1386
|
-
}
|
|
1387
|
-
if (cap.offSession === "conditional") {
|
|
1388
|
-
return { ok: true, note: `off-session upsell after ${provider} '${paywallSurface}' (managed checkout) may decline \u2014 prefer element/sheet/express` };
|
|
1389
|
-
}
|
|
1390
|
-
return { ok: true };
|
|
1391
|
-
}
|
|
1392
|
-
function checkoutError(category, message, extra = {}) {
|
|
1393
|
-
const retryable = extra.retryable ?? (category === "authentication_required" || category === "requires_payment_method" || category === "processing_error");
|
|
1394
|
-
return { category, message, declineCode: extra.declineCode, retryable };
|
|
1395
|
-
}
|
|
1396
1391
|
function createMockDriver(provider = "stripe", options = {}) {
|
|
1397
|
-
const success = options.result ?? {
|
|
1398
|
-
|
|
1392
|
+
const success = options.result ?? {
|
|
1393
|
+
ok: true,
|
|
1394
|
+
amountMinor: 0,
|
|
1395
|
+
currency: "USD"
|
|
1396
|
+
};
|
|
1397
|
+
const offError = typeof options.failOffSession === "object" ? options.failOffSession : checkoutError(
|
|
1398
|
+
"authentication_required",
|
|
1399
|
+
"off-session charge declined (mock)"
|
|
1400
|
+
);
|
|
1399
1401
|
return {
|
|
1400
1402
|
provider,
|
|
1401
1403
|
start: (req) => {
|
|
1402
|
-
if (options.failOffSession && !req.surface)
|
|
1404
|
+
if (options.failOffSession && !req.surface)
|
|
1405
|
+
return Promise.resolve({ ok: false, error: offError });
|
|
1403
1406
|
return Promise.resolve(success);
|
|
1404
1407
|
},
|
|
1405
1408
|
renderInline: (req, cb) => createElement(
|
|
@@ -1407,10 +1410,37 @@ function createMockDriver(provider = "stripe", options = {}) {
|
|
|
1407
1410
|
{
|
|
1408
1411
|
type: "button",
|
|
1409
1412
|
"data-mock-surface": req.surface,
|
|
1410
|
-
onClick: () =>
|
|
1413
|
+
onClick: () => {
|
|
1414
|
+
cb.onStart?.();
|
|
1415
|
+
if (success.ok) cb.onSuccess(success);
|
|
1416
|
+
else
|
|
1417
|
+
cb.onError(
|
|
1418
|
+
success.error ?? checkoutError(
|
|
1419
|
+
"processing_error",
|
|
1420
|
+
"failed (mock)"
|
|
1421
|
+
)
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1411
1424
|
},
|
|
1412
1425
|
`Pay (mock ${req.surface})`
|
|
1413
|
-
)
|
|
1426
|
+
),
|
|
1427
|
+
resume: () => Promise.resolve(success)
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
function driverWithEmail(driver, getEmail) {
|
|
1431
|
+
const enrich = (req) => req.email !== void 0 ? req : { ...req, email: getEmail() };
|
|
1432
|
+
return {
|
|
1433
|
+
...driver,
|
|
1434
|
+
start: (req) => driver.start(enrich(req)),
|
|
1435
|
+
renderInline: driver.renderInline ? (req, cb) => driver.renderInline(enrich(req), cb) : void 0
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
function driverWithCatalog(driver, catalog) {
|
|
1439
|
+
const enrich = (req) => req.catalogKey !== void 0 ? req : { ...req, catalogKey: catalog.get(req.product)?.catalogKey };
|
|
1440
|
+
return {
|
|
1441
|
+
...driver,
|
|
1442
|
+
start: (req) => driver.start(enrich(req)),
|
|
1443
|
+
renderInline: driver.renderInline ? (req, cb) => driver.renderInline(enrich(req), cb) : void 0
|
|
1414
1444
|
};
|
|
1415
1445
|
}
|
|
1416
1446
|
var DriverContext = createContext(null);
|
|
@@ -1427,132 +1457,343 @@ var FALLBACK_DRIVER = createMockDriver();
|
|
|
1427
1457
|
function useCheckout(opts = {}) {
|
|
1428
1458
|
const driver = useDriver();
|
|
1429
1459
|
const tracker = useTrackerRef();
|
|
1460
|
+
const catalog = useCatalog();
|
|
1430
1461
|
const nav = useNavigation();
|
|
1431
1462
|
const [status, setStatus] = useState("idle");
|
|
1432
1463
|
const [error, setError] = useState(null);
|
|
1433
1464
|
const intent = opts.intent ?? "purchase";
|
|
1434
1465
|
const kind = opts.kind;
|
|
1435
1466
|
const advance = opts.advanceOnSuccess ?? true;
|
|
1467
|
+
const productFacts = useCallback(
|
|
1468
|
+
(product) => {
|
|
1469
|
+
const resolved = product ? catalog.get(product) : void 0;
|
|
1470
|
+
return {
|
|
1471
|
+
productId: resolved?.catalogKey ?? product,
|
|
1472
|
+
amount: resolved ? resolved.priceMinor / 100 : void 0,
|
|
1473
|
+
currency: resolved?.currency,
|
|
1474
|
+
isSubscription: resolved ? resolved.period !== "one-time" : kind === "subscription" || kind === "upgrade"
|
|
1475
|
+
};
|
|
1476
|
+
},
|
|
1477
|
+
[catalog, kind]
|
|
1478
|
+
);
|
|
1479
|
+
const recovering = useRef(false);
|
|
1480
|
+
const failRef = useRef(
|
|
1481
|
+
() => {
|
|
1482
|
+
}
|
|
1483
|
+
);
|
|
1484
|
+
const attemptRef = useRef(async () => ({ ok: false }));
|
|
1436
1485
|
const succeed = useCallback(
|
|
1437
|
-
(result) => {
|
|
1486
|
+
(result, product) => {
|
|
1438
1487
|
setStatus("success");
|
|
1488
|
+
const { productId, isSubscription } = productFacts(product);
|
|
1489
|
+
const amount = result.amountMinor != null ? result.amountMinor / 100 : void 0;
|
|
1439
1490
|
tracker.track("purchase.complete", {
|
|
1440
|
-
amount
|
|
1491
|
+
amount,
|
|
1441
1492
|
currency: result.currency,
|
|
1493
|
+
productId,
|
|
1442
1494
|
eventId: result.eventId
|
|
1443
1495
|
// server-minted (browser↔CAPI dedup); undefined if none
|
|
1444
1496
|
});
|
|
1497
|
+
if (isSubscription) {
|
|
1498
|
+
tracker.track("subscription.created", {
|
|
1499
|
+
amount,
|
|
1500
|
+
currency: result.currency,
|
|
1501
|
+
productId,
|
|
1502
|
+
eventId: result.eventId
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1445
1505
|
opts.onSuccess?.(result);
|
|
1446
1506
|
if (advance) nav.next();
|
|
1447
1507
|
},
|
|
1448
|
-
[tracker, nav, advance, opts]
|
|
1449
|
-
);
|
|
1450
|
-
const fail = useCallback(
|
|
1451
|
-
(err, product) => {
|
|
1452
|
-
setStatus(err.category === "authentication_required" ? "requires_action" : "error");
|
|
1453
|
-
setError(err);
|
|
1454
|
-
tracker.track("checkout.failed", {
|
|
1455
|
-
category: err.category,
|
|
1456
|
-
declineCode: err.declineCode,
|
|
1457
|
-
productId: product,
|
|
1458
|
-
intent
|
|
1459
|
-
});
|
|
1460
|
-
opts.onError?.(err);
|
|
1461
|
-
},
|
|
1462
|
-
[tracker, intent, opts]
|
|
1508
|
+
[tracker, nav, advance, opts, productFacts]
|
|
1463
1509
|
);
|
|
1510
|
+
const succeedRef = useRef(succeed);
|
|
1511
|
+
succeedRef.current = succeed;
|
|
1464
1512
|
const begin = useCallback(
|
|
1465
1513
|
(product, surface) => {
|
|
1466
1514
|
setStatus("loading");
|
|
1467
1515
|
setError(null);
|
|
1468
|
-
|
|
1516
|
+
const { productId, amount, currency } = productFacts(product);
|
|
1517
|
+
tracker.track("checkout.start", {
|
|
1518
|
+
productId,
|
|
1519
|
+
amount,
|
|
1520
|
+
currency,
|
|
1521
|
+
surface,
|
|
1522
|
+
eventId: newEventId()
|
|
1523
|
+
});
|
|
1469
1524
|
},
|
|
1470
|
-
[tracker]
|
|
1525
|
+
[tracker, productFacts]
|
|
1471
1526
|
);
|
|
1472
|
-
const paymentAdded = useCallback(
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
} catch (e) {
|
|
1482
|
-
const err = checkoutError("processing_error", e instanceof Error ? e.message : "Checkout failed");
|
|
1483
|
-
fail(err, product);
|
|
1484
|
-
return { ok: false, error: err };
|
|
1485
|
-
}
|
|
1527
|
+
const paymentAdded = useCallback(
|
|
1528
|
+
(product) => {
|
|
1529
|
+
const { productId, amount, currency } = productFacts(product);
|
|
1530
|
+
tracker.track("checkout.payment_added", {
|
|
1531
|
+
productId,
|
|
1532
|
+
amount,
|
|
1533
|
+
currency,
|
|
1534
|
+
eventId: newEventId()
|
|
1535
|
+
});
|
|
1486
1536
|
},
|
|
1487
|
-
[
|
|
1537
|
+
[tracker, productFacts]
|
|
1488
1538
|
);
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1539
|
+
const hostInSheet = (surface) => surface === "sheet" || isInlineSurface(surface) && !!PROVIDER_PROFILES[driver.provider]?.surfaces.sheet;
|
|
1540
|
+
const startRecovery = (product, surface) => {
|
|
1541
|
+
recovering.current = true;
|
|
1542
|
+
if (hostInSheet(surface)) {
|
|
1543
|
+
begin(product, "sheet");
|
|
1544
|
+
void showModal(CheckoutSheet, {
|
|
1545
|
+
product,
|
|
1546
|
+
surface: "sheet",
|
|
1547
|
+
intent,
|
|
1548
|
+
kind,
|
|
1549
|
+
onSuccess: (r) => succeedRef.current(r, product),
|
|
1550
|
+
onError: (e) => failRef.current(e, product),
|
|
1551
|
+
onPaymentAdded: () => paymentAdded(product)
|
|
1552
|
+
});
|
|
1553
|
+
} else {
|
|
1554
|
+
void attemptRef.current(product, surface);
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
failRef.current = (err, product) => {
|
|
1558
|
+
setStatus(
|
|
1559
|
+
err.category === "authentication_required" ? "requires_action" : "error"
|
|
1560
|
+
);
|
|
1561
|
+
setError(err);
|
|
1562
|
+
tracker.track("checkout.failed", {
|
|
1563
|
+
category: err.category,
|
|
1564
|
+
declineCode: err.declineCode,
|
|
1565
|
+
productId: product,
|
|
1566
|
+
intent
|
|
1567
|
+
});
|
|
1568
|
+
opts.onError?.(err);
|
|
1569
|
+
if (err.category === "canceled" && !opts.onFailed?.canceled) return;
|
|
1570
|
+
if (product && !recovering.current && opts.recoverySurface && err.category !== "canceled") {
|
|
1571
|
+
startRecovery(product, opts.recoverySurface);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
let route = opts.onFailed?.[err.category] ?? (err.category === "canceled" ? void 0 : opts.onFailed?.default);
|
|
1575
|
+
if (route && typeof route === "object" && "fallback" in route && recovering.current) {
|
|
1576
|
+
const dflt = opts.onFailed?.default;
|
|
1577
|
+
route = dflt && dflt !== route && !(typeof dflt === "object" && "fallback" in dflt) ? dflt : void 0;
|
|
1578
|
+
}
|
|
1579
|
+
if (!route) return;
|
|
1580
|
+
if (typeof route === "function") route(err);
|
|
1581
|
+
else if ("go" in route) nav.go(route.go);
|
|
1582
|
+
else if ("modal" in route) {
|
|
1583
|
+
const props = {
|
|
1584
|
+
error: err,
|
|
1585
|
+
product,
|
|
1586
|
+
go: (pageKey) => nav.go(pageKey),
|
|
1587
|
+
retry: (surface) => {
|
|
1588
|
+
if (!product) return;
|
|
1589
|
+
recovering.current = true;
|
|
1590
|
+
if (!surface) {
|
|
1591
|
+
if (intent === "upsell")
|
|
1592
|
+
void attemptRef.current(product);
|
|
1593
|
+
else
|
|
1594
|
+
startRecovery(
|
|
1595
|
+
product,
|
|
1596
|
+
PROVIDER_PROFILES[driver.provider]?.surfaces.sheet ? "sheet" : "popup"
|
|
1597
|
+
);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
startRecovery(product, surface);
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
void showModal(
|
|
1604
|
+
route.modal,
|
|
1605
|
+
props
|
|
1606
|
+
);
|
|
1607
|
+
} else if ("fallback" in route) {
|
|
1608
|
+
if (product && !recovering.current)
|
|
1609
|
+
startRecovery(product, route.fallback);
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
attemptRef.current = async (product, surface) => {
|
|
1613
|
+
begin(product, surface);
|
|
1614
|
+
try {
|
|
1615
|
+
const result = await driver.start({
|
|
1616
|
+
product,
|
|
1617
|
+
intent,
|
|
1618
|
+
surface,
|
|
1619
|
+
kind
|
|
1620
|
+
});
|
|
1621
|
+
if (result.ok) succeedRef.current(result, product);
|
|
1622
|
+
else
|
|
1623
|
+
failRef.current(
|
|
1624
|
+
result.error ?? checkoutError("unknown", "Checkout failed"),
|
|
1625
|
+
product
|
|
1626
|
+
);
|
|
1627
|
+
return result;
|
|
1628
|
+
} catch (e) {
|
|
1629
|
+
const err = checkoutError(
|
|
1630
|
+
"processing_error",
|
|
1631
|
+
e instanceof Error ? e.message : "Checkout failed"
|
|
1632
|
+
);
|
|
1633
|
+
failRef.current(err, product);
|
|
1634
|
+
return { ok: false, error: err };
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
const open = useCallback(
|
|
1638
|
+
async (product, surface) => {
|
|
1639
|
+
recovering.current = false;
|
|
1640
|
+
return attemptRef.current(product, surface);
|
|
1641
|
+
},
|
|
1642
|
+
[]
|
|
1643
|
+
);
|
|
1644
|
+
const callbacksFor = useCallback(
|
|
1645
|
+
(product, surface) => ({
|
|
1646
|
+
onStart: () => {
|
|
1647
|
+
recovering.current = false;
|
|
1648
|
+
begin(product, surface);
|
|
1649
|
+
},
|
|
1650
|
+
onSuccess: (result) => succeedRef.current(result, product),
|
|
1651
|
+
onError: (err) => failRef.current(err, product),
|
|
1652
|
+
onPaymentAdded: () => paymentAdded(product)
|
|
1653
|
+
}),
|
|
1654
|
+
[begin, paymentAdded]
|
|
1497
1655
|
);
|
|
1498
1656
|
const reset = useCallback(() => {
|
|
1657
|
+
recovering.current = false;
|
|
1499
1658
|
setStatus("idle");
|
|
1500
1659
|
setError(null);
|
|
1501
1660
|
}, []);
|
|
1502
|
-
return {
|
|
1661
|
+
return {
|
|
1662
|
+
open,
|
|
1663
|
+
status,
|
|
1664
|
+
error,
|
|
1665
|
+
isLoading: status === "loading",
|
|
1666
|
+
reset,
|
|
1667
|
+
callbacksFor
|
|
1668
|
+
};
|
|
1503
1669
|
}
|
|
1504
|
-
var CheckoutSheet = defineModal(({ product, surface, intent, onSuccess, onError }) => {
|
|
1670
|
+
var CheckoutSheet = defineModal(({ product, surface, intent, kind, onSuccess, onError, onPaymentAdded }) => {
|
|
1505
1671
|
const driver = useDriver();
|
|
1506
1672
|
const modal = useModal();
|
|
1673
|
+
const resolved = useRef(false);
|
|
1674
|
+
const onErrorRef = useRef(onError);
|
|
1675
|
+
onErrorRef.current = onError;
|
|
1676
|
+
const hostable = !!driver.renderInline;
|
|
1677
|
+
useEffect(() => {
|
|
1678
|
+
if (!hostable) {
|
|
1679
|
+
resolved.current = true;
|
|
1680
|
+
onErrorRef.current(
|
|
1681
|
+
checkoutError(
|
|
1682
|
+
"processing_error",
|
|
1683
|
+
"This checkout driver has no inline surface to host in the sheet"
|
|
1684
|
+
)
|
|
1685
|
+
);
|
|
1686
|
+
void modal.hide();
|
|
1687
|
+
modal.remove();
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
return () => {
|
|
1691
|
+
if (!resolved.current) {
|
|
1692
|
+
onErrorRef.current(
|
|
1693
|
+
checkoutError(
|
|
1694
|
+
"canceled",
|
|
1695
|
+
"Checkout was dismissed before completing"
|
|
1696
|
+
)
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
}, []);
|
|
1507
1701
|
if (!driver.renderInline) return null;
|
|
1702
|
+
const close = () => {
|
|
1703
|
+
resolved.current = true;
|
|
1704
|
+
void modal.hide();
|
|
1705
|
+
modal.remove();
|
|
1706
|
+
};
|
|
1508
1707
|
return createElement(Sheet, {
|
|
1509
1708
|
children: driver.renderInline(
|
|
1510
|
-
{ product, intent, surface },
|
|
1709
|
+
{ product, intent, surface, kind },
|
|
1511
1710
|
{
|
|
1512
1711
|
onSuccess: (r) => {
|
|
1513
|
-
|
|
1712
|
+
close();
|
|
1514
1713
|
onSuccess(r);
|
|
1515
1714
|
},
|
|
1516
1715
|
onError: (e) => {
|
|
1517
|
-
|
|
1716
|
+
close();
|
|
1518
1717
|
onError(e);
|
|
1519
|
-
}
|
|
1718
|
+
},
|
|
1719
|
+
onPaymentAdded
|
|
1520
1720
|
}
|
|
1521
1721
|
)
|
|
1522
1722
|
});
|
|
1523
1723
|
});
|
|
1524
|
-
function trigger(onClick, {
|
|
1724
|
+
function trigger(onClick, {
|
|
1725
|
+
asChild,
|
|
1726
|
+
children,
|
|
1727
|
+
className,
|
|
1728
|
+
disabled
|
|
1729
|
+
}) {
|
|
1525
1730
|
if (asChild && isValidElement(children)) {
|
|
1526
1731
|
const child = children;
|
|
1527
1732
|
return cloneElement(child, { onClick });
|
|
1528
1733
|
}
|
|
1529
|
-
return createElement(
|
|
1734
|
+
return createElement(
|
|
1735
|
+
"button",
|
|
1736
|
+
{ type: "button", className, disabled, onClick },
|
|
1737
|
+
children ?? "Continue"
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
function detectDeviceClass() {
|
|
1741
|
+
if (typeof window === "undefined") return "desktop";
|
|
1742
|
+
const ua = navigator.userAgent?.toLowerCase() ?? "";
|
|
1743
|
+
if (/mobile|android(?!.*tablet)|iphone|ipod/.test(ua)) return "mobile";
|
|
1744
|
+
try {
|
|
1745
|
+
if (window.matchMedia?.("(pointer: coarse)").matches && window.innerWidth > 0 && window.innerWidth < 768) {
|
|
1746
|
+
return "mobile";
|
|
1747
|
+
}
|
|
1748
|
+
} catch {
|
|
1749
|
+
}
|
|
1750
|
+
return "desktop";
|
|
1530
1751
|
}
|
|
1531
1752
|
function Checkout({
|
|
1532
|
-
|
|
1533
|
-
surface,
|
|
1753
|
+
offering,
|
|
1754
|
+
surface: surfacePreference,
|
|
1534
1755
|
asChild,
|
|
1535
1756
|
children,
|
|
1536
1757
|
className,
|
|
1537
1758
|
...options
|
|
1538
1759
|
}) {
|
|
1760
|
+
const product = offering;
|
|
1539
1761
|
const driver = useDriver();
|
|
1762
|
+
const [surface] = useState(
|
|
1763
|
+
() => resolveSurface(driver.provider, surfacePreference, detectDeviceClass())
|
|
1764
|
+
);
|
|
1540
1765
|
const checkout = useCheckout(options);
|
|
1541
1766
|
const intent = options.intent ?? "purchase";
|
|
1542
1767
|
if (isInlineSurface(surface)) {
|
|
1543
1768
|
return driver.renderInline ? createElement(
|
|
1544
1769
|
"div",
|
|
1545
1770
|
{ key: product, className },
|
|
1546
|
-
driver.renderInline(
|
|
1771
|
+
driver.renderInline(
|
|
1772
|
+
{ product, intent, surface },
|
|
1773
|
+
checkout.callbacksFor(product, surface)
|
|
1774
|
+
)
|
|
1547
1775
|
) : null;
|
|
1548
1776
|
}
|
|
1549
1777
|
if (surface === "sheet") {
|
|
1550
1778
|
const cb = checkout.callbacksFor(product, surface);
|
|
1551
1779
|
const open = () => {
|
|
1552
1780
|
cb.onStart();
|
|
1553
|
-
showModal(CheckoutSheet, {
|
|
1781
|
+
showModal(CheckoutSheet, {
|
|
1782
|
+
product,
|
|
1783
|
+
surface,
|
|
1784
|
+
intent,
|
|
1785
|
+
kind: options.kind,
|
|
1786
|
+
onSuccess: cb.onSuccess,
|
|
1787
|
+
onError: cb.onError,
|
|
1788
|
+
onPaymentAdded: cb.onPaymentAdded
|
|
1789
|
+
});
|
|
1554
1790
|
};
|
|
1555
|
-
return trigger(open, {
|
|
1791
|
+
return trigger(open, {
|
|
1792
|
+
asChild,
|
|
1793
|
+
children,
|
|
1794
|
+
className,
|
|
1795
|
+
disabled: checkout.isLoading
|
|
1796
|
+
});
|
|
1556
1797
|
}
|
|
1557
1798
|
return trigger(() => checkout.open(product, surface), {
|
|
1558
1799
|
asChild,
|
|
@@ -1561,32 +1802,1150 @@ function Checkout({
|
|
|
1561
1802
|
disabled: checkout.isLoading
|
|
1562
1803
|
});
|
|
1563
1804
|
}
|
|
1564
|
-
function Upsell({
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1805
|
+
function Upsell({
|
|
1806
|
+
offering,
|
|
1807
|
+
fallback,
|
|
1808
|
+
asChild,
|
|
1809
|
+
children,
|
|
1810
|
+
className,
|
|
1811
|
+
...options
|
|
1812
|
+
}) {
|
|
1813
|
+
const product = offering;
|
|
1814
|
+
const checkout = useCheckout({
|
|
1815
|
+
...options,
|
|
1816
|
+
intent: "upsell",
|
|
1817
|
+
recoverySurface: fallback ?? options.recoverySurface
|
|
1818
|
+
});
|
|
1819
|
+
return trigger(
|
|
1820
|
+
() => void checkout.open(product),
|
|
1821
|
+
{
|
|
1822
|
+
asChild,
|
|
1823
|
+
children: children ?? "Add to my plan",
|
|
1824
|
+
className,
|
|
1825
|
+
disabled: checkout.isLoading
|
|
1826
|
+
}
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
var consumedResumes = /* @__PURE__ */ new Set();
|
|
1830
|
+
function CheckoutResume(options = {}) {
|
|
1831
|
+
const driver = useDriver();
|
|
1832
|
+
const checkout = useCheckout(options);
|
|
1833
|
+
const callbacksRef = useRef(checkout.callbacksFor);
|
|
1834
|
+
callbacksRef.current = checkout.callbacksFor;
|
|
1835
|
+
useEffect(() => {
|
|
1836
|
+
if (typeof window === "undefined" || !driver.resume) return;
|
|
1837
|
+
const url = new URL(window.location.href);
|
|
1838
|
+
const correlationId = url.searchParams.get("af_checkout");
|
|
1839
|
+
if (!correlationId || consumedResumes.has(correlationId)) return;
|
|
1840
|
+
consumedResumes.add(correlationId);
|
|
1841
|
+
void driver.resume(correlationId).then((result) => {
|
|
1842
|
+
const cb = callbacksRef.current();
|
|
1843
|
+
if (result.ok) {
|
|
1844
|
+
url.searchParams.delete("af_checkout");
|
|
1845
|
+
window.history.replaceState(
|
|
1846
|
+
window.history.state,
|
|
1847
|
+
"",
|
|
1848
|
+
url.toString()
|
|
1849
|
+
);
|
|
1850
|
+
cb.onSuccess(result);
|
|
1851
|
+
} else {
|
|
1852
|
+
cb.onError(
|
|
1853
|
+
result.error ?? checkoutError(
|
|
1854
|
+
"unknown",
|
|
1855
|
+
"The checkout could not be verified"
|
|
1856
|
+
)
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
}, []);
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1863
|
+
function __insertCSS(code) {
|
|
1864
|
+
if (typeof document == "undefined") return;
|
|
1865
|
+
let head = document.head || document.getElementsByTagName("head")[0];
|
|
1866
|
+
let style = document.createElement("style");
|
|
1867
|
+
style.type = "text/css";
|
|
1868
|
+
head.appendChild(style);
|
|
1869
|
+
style.styleSheet ? style.styleSheet.cssText = code : style.appendChild(document.createTextNode(code));
|
|
1870
|
+
}
|
|
1871
|
+
var getAsset = (type) => {
|
|
1872
|
+
switch (type) {
|
|
1873
|
+
case "success":
|
|
1874
|
+
return SuccessIcon;
|
|
1875
|
+
case "info":
|
|
1876
|
+
return InfoIcon;
|
|
1877
|
+
case "warning":
|
|
1878
|
+
return WarningIcon;
|
|
1879
|
+
case "error":
|
|
1880
|
+
return ErrorIcon;
|
|
1881
|
+
default:
|
|
1882
|
+
return null;
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
var bars = Array(12).fill(0);
|
|
1886
|
+
var Loader = ({ visible, className }) => {
|
|
1887
|
+
return /* @__PURE__ */ React.createElement("div", {
|
|
1888
|
+
className: [
|
|
1889
|
+
"sonner-loading-wrapper",
|
|
1890
|
+
className
|
|
1891
|
+
].filter(Boolean).join(" "),
|
|
1892
|
+
"data-visible": visible
|
|
1893
|
+
}, /* @__PURE__ */ React.createElement("div", {
|
|
1894
|
+
className: "sonner-spinner"
|
|
1895
|
+
}, bars.map((_, i) => /* @__PURE__ */ React.createElement("div", {
|
|
1896
|
+
className: "sonner-loading-bar",
|
|
1897
|
+
key: `spinner-bar-${i}`
|
|
1898
|
+
}))));
|
|
1899
|
+
};
|
|
1900
|
+
var SuccessIcon = /* @__PURE__ */ React.createElement("svg", {
|
|
1901
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1902
|
+
viewBox: "0 0 20 20",
|
|
1903
|
+
fill: "currentColor",
|
|
1904
|
+
height: "20",
|
|
1905
|
+
width: "20"
|
|
1906
|
+
}, /* @__PURE__ */ React.createElement("path", {
|
|
1907
|
+
fillRule: "evenodd",
|
|
1908
|
+
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",
|
|
1909
|
+
clipRule: "evenodd"
|
|
1910
|
+
}));
|
|
1911
|
+
var WarningIcon = /* @__PURE__ */ React.createElement("svg", {
|
|
1912
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1913
|
+
viewBox: "0 0 24 24",
|
|
1914
|
+
fill: "currentColor",
|
|
1915
|
+
height: "20",
|
|
1916
|
+
width: "20"
|
|
1917
|
+
}, /* @__PURE__ */ React.createElement("path", {
|
|
1918
|
+
fillRule: "evenodd",
|
|
1919
|
+
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",
|
|
1920
|
+
clipRule: "evenodd"
|
|
1921
|
+
}));
|
|
1922
|
+
var InfoIcon = /* @__PURE__ */ React.createElement("svg", {
|
|
1923
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1924
|
+
viewBox: "0 0 20 20",
|
|
1925
|
+
fill: "currentColor",
|
|
1926
|
+
height: "20",
|
|
1927
|
+
width: "20"
|
|
1928
|
+
}, /* @__PURE__ */ React.createElement("path", {
|
|
1929
|
+
fillRule: "evenodd",
|
|
1930
|
+
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",
|
|
1931
|
+
clipRule: "evenodd"
|
|
1932
|
+
}));
|
|
1933
|
+
var ErrorIcon = /* @__PURE__ */ React.createElement("svg", {
|
|
1934
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1935
|
+
viewBox: "0 0 20 20",
|
|
1936
|
+
fill: "currentColor",
|
|
1937
|
+
height: "20",
|
|
1938
|
+
width: "20"
|
|
1939
|
+
}, /* @__PURE__ */ React.createElement("path", {
|
|
1940
|
+
fillRule: "evenodd",
|
|
1941
|
+
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",
|
|
1942
|
+
clipRule: "evenodd"
|
|
1943
|
+
}));
|
|
1944
|
+
var CloseIcon = /* @__PURE__ */ React.createElement("svg", {
|
|
1945
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1946
|
+
width: "12",
|
|
1947
|
+
height: "12",
|
|
1948
|
+
viewBox: "0 0 24 24",
|
|
1949
|
+
fill: "none",
|
|
1950
|
+
stroke: "currentColor",
|
|
1951
|
+
strokeWidth: "1.5",
|
|
1952
|
+
strokeLinecap: "round",
|
|
1953
|
+
strokeLinejoin: "round"
|
|
1954
|
+
}, /* @__PURE__ */ React.createElement("line", {
|
|
1955
|
+
x1: "18",
|
|
1956
|
+
y1: "6",
|
|
1957
|
+
x2: "6",
|
|
1958
|
+
y2: "18"
|
|
1959
|
+
}), /* @__PURE__ */ React.createElement("line", {
|
|
1960
|
+
x1: "6",
|
|
1961
|
+
y1: "6",
|
|
1962
|
+
x2: "18",
|
|
1963
|
+
y2: "18"
|
|
1964
|
+
}));
|
|
1965
|
+
var useIsDocumentHidden = () => {
|
|
1966
|
+
const [isDocumentHidden, setIsDocumentHidden] = React.useState(document.hidden);
|
|
1967
|
+
React.useEffect(() => {
|
|
1968
|
+
const callback = () => {
|
|
1969
|
+
setIsDocumentHidden(document.hidden);
|
|
1970
|
+
};
|
|
1971
|
+
document.addEventListener("visibilitychange", callback);
|
|
1972
|
+
return () => window.removeEventListener("visibilitychange", callback);
|
|
1973
|
+
}, []);
|
|
1974
|
+
return isDocumentHidden;
|
|
1975
|
+
};
|
|
1976
|
+
var toastsCounter = 1;
|
|
1977
|
+
var Observer = class {
|
|
1978
|
+
constructor() {
|
|
1979
|
+
this.subscribe = (subscriber) => {
|
|
1980
|
+
this.subscribers.push(subscriber);
|
|
1981
|
+
return () => {
|
|
1982
|
+
const index = this.subscribers.indexOf(subscriber);
|
|
1983
|
+
this.subscribers.splice(index, 1);
|
|
1984
|
+
};
|
|
1985
|
+
};
|
|
1986
|
+
this.publish = (data) => {
|
|
1987
|
+
this.subscribers.forEach((subscriber) => subscriber(data));
|
|
1988
|
+
};
|
|
1989
|
+
this.addToast = (data) => {
|
|
1990
|
+
this.publish(data);
|
|
1991
|
+
this.toasts = [
|
|
1992
|
+
...this.toasts,
|
|
1993
|
+
data
|
|
1994
|
+
];
|
|
1995
|
+
};
|
|
1996
|
+
this.create = (data) => {
|
|
1997
|
+
var _data_id;
|
|
1998
|
+
const { message, ...rest } = data;
|
|
1999
|
+
const id = typeof (data == null ? void 0 : data.id) === "number" || ((_data_id = data.id) == null ? void 0 : _data_id.length) > 0 ? data.id : toastsCounter++;
|
|
2000
|
+
const alreadyExists = this.toasts.find((toast2) => {
|
|
2001
|
+
return toast2.id === id;
|
|
2002
|
+
});
|
|
2003
|
+
const dismissible = data.dismissible === void 0 ? true : data.dismissible;
|
|
2004
|
+
if (this.dismissedToasts.has(id)) {
|
|
2005
|
+
this.dismissedToasts.delete(id);
|
|
2006
|
+
}
|
|
2007
|
+
if (alreadyExists) {
|
|
2008
|
+
this.toasts = this.toasts.map((toast2) => {
|
|
2009
|
+
if (toast2.id === id) {
|
|
2010
|
+
this.publish({
|
|
2011
|
+
...toast2,
|
|
2012
|
+
...data,
|
|
2013
|
+
id,
|
|
2014
|
+
title: message
|
|
2015
|
+
});
|
|
2016
|
+
return {
|
|
2017
|
+
...toast2,
|
|
2018
|
+
...data,
|
|
2019
|
+
id,
|
|
2020
|
+
dismissible,
|
|
2021
|
+
title: message
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
return toast2;
|
|
2025
|
+
});
|
|
2026
|
+
} else {
|
|
2027
|
+
this.addToast({
|
|
2028
|
+
title: message,
|
|
2029
|
+
...rest,
|
|
2030
|
+
dismissible,
|
|
2031
|
+
id
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
return id;
|
|
2035
|
+
};
|
|
2036
|
+
this.dismiss = (id) => {
|
|
2037
|
+
if (id) {
|
|
2038
|
+
this.dismissedToasts.add(id);
|
|
2039
|
+
requestAnimationFrame(() => this.subscribers.forEach((subscriber) => subscriber({
|
|
2040
|
+
id,
|
|
2041
|
+
dismiss: true
|
|
2042
|
+
})));
|
|
2043
|
+
} else {
|
|
2044
|
+
this.toasts.forEach((toast2) => {
|
|
2045
|
+
this.subscribers.forEach((subscriber) => subscriber({
|
|
2046
|
+
id: toast2.id,
|
|
2047
|
+
dismiss: true
|
|
2048
|
+
}));
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
return id;
|
|
2052
|
+
};
|
|
2053
|
+
this.message = (message, data) => {
|
|
2054
|
+
return this.create({
|
|
2055
|
+
...data,
|
|
2056
|
+
message
|
|
2057
|
+
});
|
|
2058
|
+
};
|
|
2059
|
+
this.error = (message, data) => {
|
|
2060
|
+
return this.create({
|
|
2061
|
+
...data,
|
|
2062
|
+
message,
|
|
2063
|
+
type: "error"
|
|
2064
|
+
});
|
|
2065
|
+
};
|
|
2066
|
+
this.success = (message, data) => {
|
|
2067
|
+
return this.create({
|
|
2068
|
+
...data,
|
|
2069
|
+
type: "success",
|
|
2070
|
+
message
|
|
2071
|
+
});
|
|
2072
|
+
};
|
|
2073
|
+
this.info = (message, data) => {
|
|
2074
|
+
return this.create({
|
|
2075
|
+
...data,
|
|
2076
|
+
type: "info",
|
|
2077
|
+
message
|
|
2078
|
+
});
|
|
2079
|
+
};
|
|
2080
|
+
this.warning = (message, data) => {
|
|
2081
|
+
return this.create({
|
|
2082
|
+
...data,
|
|
2083
|
+
type: "warning",
|
|
2084
|
+
message
|
|
2085
|
+
});
|
|
2086
|
+
};
|
|
2087
|
+
this.loading = (message, data) => {
|
|
2088
|
+
return this.create({
|
|
2089
|
+
...data,
|
|
2090
|
+
type: "loading",
|
|
2091
|
+
message
|
|
2092
|
+
});
|
|
2093
|
+
};
|
|
2094
|
+
this.promise = (promise, data) => {
|
|
2095
|
+
if (!data) {
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
let id = void 0;
|
|
2099
|
+
if (data.loading !== void 0) {
|
|
2100
|
+
id = this.create({
|
|
2101
|
+
...data,
|
|
2102
|
+
promise,
|
|
2103
|
+
type: "loading",
|
|
2104
|
+
message: data.loading,
|
|
2105
|
+
description: typeof data.description !== "function" ? data.description : void 0
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
const p = Promise.resolve(promise instanceof Function ? promise() : promise);
|
|
2109
|
+
let shouldDismiss = id !== void 0;
|
|
2110
|
+
let result;
|
|
2111
|
+
const originalPromise = p.then(async (response) => {
|
|
2112
|
+
result = [
|
|
2113
|
+
"resolve",
|
|
2114
|
+
response
|
|
2115
|
+
];
|
|
2116
|
+
const isReactElementResponse = React.isValidElement(response);
|
|
2117
|
+
if (isReactElementResponse) {
|
|
2118
|
+
shouldDismiss = false;
|
|
2119
|
+
this.create({
|
|
2120
|
+
id,
|
|
2121
|
+
type: "default",
|
|
2122
|
+
message: response
|
|
2123
|
+
});
|
|
2124
|
+
} else if (isHttpResponse(response) && !response.ok) {
|
|
2125
|
+
shouldDismiss = false;
|
|
2126
|
+
const promiseData = typeof data.error === "function" ? await data.error(`HTTP error! status: ${response.status}`) : data.error;
|
|
2127
|
+
const description = typeof data.description === "function" ? await data.description(`HTTP error! status: ${response.status}`) : data.description;
|
|
2128
|
+
const isExtendedResult = typeof promiseData === "object" && !React.isValidElement(promiseData);
|
|
2129
|
+
const toastSettings = isExtendedResult ? promiseData : {
|
|
2130
|
+
message: promiseData
|
|
2131
|
+
};
|
|
2132
|
+
this.create({
|
|
2133
|
+
id,
|
|
2134
|
+
type: "error",
|
|
2135
|
+
description,
|
|
2136
|
+
...toastSettings
|
|
2137
|
+
});
|
|
2138
|
+
} else if (response instanceof Error) {
|
|
2139
|
+
shouldDismiss = false;
|
|
2140
|
+
const promiseData = typeof data.error === "function" ? await data.error(response) : data.error;
|
|
2141
|
+
const description = typeof data.description === "function" ? await data.description(response) : data.description;
|
|
2142
|
+
const isExtendedResult = typeof promiseData === "object" && !React.isValidElement(promiseData);
|
|
2143
|
+
const toastSettings = isExtendedResult ? promiseData : {
|
|
2144
|
+
message: promiseData
|
|
2145
|
+
};
|
|
2146
|
+
this.create({
|
|
2147
|
+
id,
|
|
2148
|
+
type: "error",
|
|
2149
|
+
description,
|
|
2150
|
+
...toastSettings
|
|
2151
|
+
});
|
|
2152
|
+
} else if (data.success !== void 0) {
|
|
2153
|
+
shouldDismiss = false;
|
|
2154
|
+
const promiseData = typeof data.success === "function" ? await data.success(response) : data.success;
|
|
2155
|
+
const description = typeof data.description === "function" ? await data.description(response) : data.description;
|
|
2156
|
+
const isExtendedResult = typeof promiseData === "object" && !React.isValidElement(promiseData);
|
|
2157
|
+
const toastSettings = isExtendedResult ? promiseData : {
|
|
2158
|
+
message: promiseData
|
|
2159
|
+
};
|
|
2160
|
+
this.create({
|
|
2161
|
+
id,
|
|
2162
|
+
type: "success",
|
|
2163
|
+
description,
|
|
2164
|
+
...toastSettings
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
}).catch(async (error) => {
|
|
2168
|
+
result = [
|
|
2169
|
+
"reject",
|
|
2170
|
+
error
|
|
2171
|
+
];
|
|
2172
|
+
if (data.error !== void 0) {
|
|
2173
|
+
shouldDismiss = false;
|
|
2174
|
+
const promiseData = typeof data.error === "function" ? await data.error(error) : data.error;
|
|
2175
|
+
const description = typeof data.description === "function" ? await data.description(error) : data.description;
|
|
2176
|
+
const isExtendedResult = typeof promiseData === "object" && !React.isValidElement(promiseData);
|
|
2177
|
+
const toastSettings = isExtendedResult ? promiseData : {
|
|
2178
|
+
message: promiseData
|
|
2179
|
+
};
|
|
2180
|
+
this.create({
|
|
2181
|
+
id,
|
|
2182
|
+
type: "error",
|
|
2183
|
+
description,
|
|
2184
|
+
...toastSettings
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
}).finally(() => {
|
|
2188
|
+
if (shouldDismiss) {
|
|
2189
|
+
this.dismiss(id);
|
|
2190
|
+
id = void 0;
|
|
2191
|
+
}
|
|
2192
|
+
data.finally == null ? void 0 : data.finally.call(data);
|
|
2193
|
+
});
|
|
2194
|
+
const unwrap = () => new Promise((resolve, reject) => originalPromise.then(() => result[0] === "reject" ? reject(result[1]) : resolve(result[1])).catch(reject));
|
|
2195
|
+
if (typeof id !== "string" && typeof id !== "number") {
|
|
2196
|
+
return {
|
|
2197
|
+
unwrap
|
|
2198
|
+
};
|
|
2199
|
+
} else {
|
|
2200
|
+
return Object.assign(id, {
|
|
2201
|
+
unwrap
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
this.custom = (jsx2, data) => {
|
|
2206
|
+
const id = (data == null ? void 0 : data.id) || toastsCounter++;
|
|
2207
|
+
this.create({
|
|
2208
|
+
jsx: jsx2(id),
|
|
2209
|
+
id,
|
|
2210
|
+
...data
|
|
2211
|
+
});
|
|
2212
|
+
return id;
|
|
2213
|
+
};
|
|
2214
|
+
this.getActiveToasts = () => {
|
|
2215
|
+
return this.toasts.filter((toast2) => !this.dismissedToasts.has(toast2.id));
|
|
2216
|
+
};
|
|
2217
|
+
this.subscribers = [];
|
|
2218
|
+
this.toasts = [];
|
|
2219
|
+
this.dismissedToasts = /* @__PURE__ */ new Set();
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
var ToastState = new Observer();
|
|
2223
|
+
var toastFunction = (message, data) => {
|
|
2224
|
+
const id = (data == null ? void 0 : data.id) || toastsCounter++;
|
|
2225
|
+
ToastState.addToast({
|
|
2226
|
+
title: message,
|
|
2227
|
+
...data,
|
|
2228
|
+
id
|
|
2229
|
+
});
|
|
2230
|
+
return id;
|
|
2231
|
+
};
|
|
2232
|
+
var isHttpResponse = (data) => {
|
|
2233
|
+
return data && typeof data === "object" && "ok" in data && typeof data.ok === "boolean" && "status" in data && typeof data.status === "number";
|
|
2234
|
+
};
|
|
2235
|
+
var basicToast = toastFunction;
|
|
2236
|
+
var getHistory = () => ToastState.toasts;
|
|
2237
|
+
var getToasts = () => ToastState.getActiveToasts();
|
|
2238
|
+
var toast = Object.assign(basicToast, {
|
|
2239
|
+
success: ToastState.success,
|
|
2240
|
+
info: ToastState.info,
|
|
2241
|
+
warning: ToastState.warning,
|
|
2242
|
+
error: ToastState.error,
|
|
2243
|
+
custom: ToastState.custom,
|
|
2244
|
+
message: ToastState.message,
|
|
2245
|
+
promise: ToastState.promise,
|
|
2246
|
+
dismiss: ToastState.dismiss,
|
|
2247
|
+
loading: ToastState.loading
|
|
2248
|
+
}, {
|
|
2249
|
+
getHistory,
|
|
2250
|
+
getToasts
|
|
2251
|
+
});
|
|
2252
|
+
__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%)}");
|
|
2253
|
+
function isAction(action) {
|
|
2254
|
+
return action.label !== void 0;
|
|
2255
|
+
}
|
|
2256
|
+
var VISIBLE_TOASTS_AMOUNT = 3;
|
|
2257
|
+
var VIEWPORT_OFFSET = "24px";
|
|
2258
|
+
var MOBILE_VIEWPORT_OFFSET = "16px";
|
|
2259
|
+
var TOAST_LIFETIME = 4e3;
|
|
2260
|
+
var TOAST_WIDTH = 356;
|
|
2261
|
+
var GAP = 14;
|
|
2262
|
+
var SWIPE_THRESHOLD = 45;
|
|
2263
|
+
var TIME_BEFORE_UNMOUNT = 200;
|
|
2264
|
+
function cn(...classes) {
|
|
2265
|
+
return classes.filter(Boolean).join(" ");
|
|
2266
|
+
}
|
|
2267
|
+
function getDefaultSwipeDirections(position) {
|
|
2268
|
+
const [y, x] = position.split("-");
|
|
2269
|
+
const directions = [];
|
|
2270
|
+
if (y) {
|
|
2271
|
+
directions.push(y);
|
|
2272
|
+
}
|
|
2273
|
+
if (x) {
|
|
2274
|
+
directions.push(x);
|
|
2275
|
+
}
|
|
2276
|
+
return directions;
|
|
2277
|
+
}
|
|
2278
|
+
var Toast = (props) => {
|
|
2279
|
+
var _toast_classNames, _toast_classNames1, _toast_classNames2, _toast_classNames3, _toast_classNames4, _toast_classNames5, _toast_classNames6, _toast_classNames7, _toast_classNames8;
|
|
2280
|
+
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;
|
|
2281
|
+
const [swipeDirection, setSwipeDirection] = React.useState(null);
|
|
2282
|
+
const [swipeOutDirection, setSwipeOutDirection] = React.useState(null);
|
|
2283
|
+
const [mounted, setMounted] = React.useState(false);
|
|
2284
|
+
const [removed, setRemoved] = React.useState(false);
|
|
2285
|
+
const [swiping, setSwiping] = React.useState(false);
|
|
2286
|
+
const [swipeOut, setSwipeOut] = React.useState(false);
|
|
2287
|
+
const [isSwiped, setIsSwiped] = React.useState(false);
|
|
2288
|
+
const [offsetBeforeRemove, setOffsetBeforeRemove] = React.useState(0);
|
|
2289
|
+
const [initialHeight, setInitialHeight] = React.useState(0);
|
|
2290
|
+
const remainingTime = React.useRef(toast2.duration || durationFromToaster || TOAST_LIFETIME);
|
|
2291
|
+
const dragStartTime = React.useRef(null);
|
|
2292
|
+
const toastRef = React.useRef(null);
|
|
2293
|
+
const isFront = index === 0;
|
|
2294
|
+
const isVisible = index + 1 <= visibleToasts;
|
|
2295
|
+
const toastType = toast2.type;
|
|
2296
|
+
const dismissible = toast2.dismissible !== false;
|
|
2297
|
+
const toastClassname = toast2.className || "";
|
|
2298
|
+
const toastDescriptionClassname = toast2.descriptionClassName || "";
|
|
2299
|
+
const heightIndex = React.useMemo(() => heights.findIndex((height) => height.toastId === toast2.id) || 0, [
|
|
2300
|
+
heights,
|
|
2301
|
+
toast2.id
|
|
2302
|
+
]);
|
|
2303
|
+
const closeButton = React.useMemo(() => {
|
|
2304
|
+
var _toast_closeButton;
|
|
2305
|
+
return (_toast_closeButton = toast2.closeButton) != null ? _toast_closeButton : closeButtonFromToaster;
|
|
2306
|
+
}, [
|
|
2307
|
+
toast2.closeButton,
|
|
2308
|
+
closeButtonFromToaster
|
|
2309
|
+
]);
|
|
2310
|
+
const duration = React.useMemo(() => toast2.duration || durationFromToaster || TOAST_LIFETIME, [
|
|
2311
|
+
toast2.duration,
|
|
2312
|
+
durationFromToaster
|
|
2313
|
+
]);
|
|
2314
|
+
const closeTimerStartTimeRef = React.useRef(0);
|
|
2315
|
+
const offset = React.useRef(0);
|
|
2316
|
+
const lastCloseTimerStartTimeRef = React.useRef(0);
|
|
2317
|
+
const pointerStartRef = React.useRef(null);
|
|
2318
|
+
const [y, x] = position.split("-");
|
|
2319
|
+
const toastsHeightBefore = React.useMemo(() => {
|
|
2320
|
+
return heights.reduce((prev, curr, reducerIndex) => {
|
|
2321
|
+
if (reducerIndex >= heightIndex) {
|
|
2322
|
+
return prev;
|
|
2323
|
+
}
|
|
2324
|
+
return prev + curr.height;
|
|
2325
|
+
}, 0);
|
|
2326
|
+
}, [
|
|
2327
|
+
heights,
|
|
2328
|
+
heightIndex
|
|
2329
|
+
]);
|
|
2330
|
+
const isDocumentHidden = useIsDocumentHidden();
|
|
2331
|
+
const invert = toast2.invert || ToasterInvert;
|
|
2332
|
+
const disabled = toastType === "loading";
|
|
2333
|
+
offset.current = React.useMemo(() => heightIndex * gap + toastsHeightBefore, [
|
|
2334
|
+
heightIndex,
|
|
2335
|
+
toastsHeightBefore
|
|
2336
|
+
]);
|
|
2337
|
+
React.useEffect(() => {
|
|
2338
|
+
remainingTime.current = duration;
|
|
2339
|
+
}, [
|
|
2340
|
+
duration
|
|
2341
|
+
]);
|
|
2342
|
+
React.useEffect(() => {
|
|
2343
|
+
setMounted(true);
|
|
2344
|
+
}, []);
|
|
2345
|
+
React.useEffect(() => {
|
|
2346
|
+
const toastNode = toastRef.current;
|
|
2347
|
+
if (toastNode) {
|
|
2348
|
+
const height = toastNode.getBoundingClientRect().height;
|
|
2349
|
+
setInitialHeight(height);
|
|
2350
|
+
setHeights((h) => [
|
|
2351
|
+
{
|
|
2352
|
+
toastId: toast2.id,
|
|
2353
|
+
height,
|
|
2354
|
+
position: toast2.position
|
|
2355
|
+
},
|
|
2356
|
+
...h
|
|
2357
|
+
]);
|
|
2358
|
+
return () => setHeights((h) => h.filter((height2) => height2.toastId !== toast2.id));
|
|
2359
|
+
}
|
|
2360
|
+
}, [
|
|
2361
|
+
setHeights,
|
|
2362
|
+
toast2.id
|
|
2363
|
+
]);
|
|
2364
|
+
React.useLayoutEffect(() => {
|
|
2365
|
+
if (!mounted) return;
|
|
2366
|
+
const toastNode = toastRef.current;
|
|
2367
|
+
const originalHeight = toastNode.style.height;
|
|
2368
|
+
toastNode.style.height = "auto";
|
|
2369
|
+
const newHeight = toastNode.getBoundingClientRect().height;
|
|
2370
|
+
toastNode.style.height = originalHeight;
|
|
2371
|
+
setInitialHeight(newHeight);
|
|
2372
|
+
setHeights((heights2) => {
|
|
2373
|
+
const alreadyExists = heights2.find((height) => height.toastId === toast2.id);
|
|
2374
|
+
if (!alreadyExists) {
|
|
2375
|
+
return [
|
|
2376
|
+
{
|
|
2377
|
+
toastId: toast2.id,
|
|
2378
|
+
height: newHeight,
|
|
2379
|
+
position: toast2.position
|
|
2380
|
+
},
|
|
2381
|
+
...heights2
|
|
2382
|
+
];
|
|
2383
|
+
} else {
|
|
2384
|
+
return heights2.map((height) => height.toastId === toast2.id ? {
|
|
2385
|
+
...height,
|
|
2386
|
+
height: newHeight
|
|
2387
|
+
} : height);
|
|
2388
|
+
}
|
|
2389
|
+
});
|
|
2390
|
+
}, [
|
|
2391
|
+
mounted,
|
|
2392
|
+
toast2.title,
|
|
2393
|
+
toast2.description,
|
|
2394
|
+
setHeights,
|
|
2395
|
+
toast2.id,
|
|
2396
|
+
toast2.jsx,
|
|
2397
|
+
toast2.action,
|
|
2398
|
+
toast2.cancel
|
|
2399
|
+
]);
|
|
2400
|
+
const deleteToast = React.useCallback(() => {
|
|
2401
|
+
setRemoved(true);
|
|
2402
|
+
setOffsetBeforeRemove(offset.current);
|
|
2403
|
+
setHeights((h) => h.filter((height) => height.toastId !== toast2.id));
|
|
2404
|
+
setTimeout(() => {
|
|
2405
|
+
removeToast(toast2);
|
|
2406
|
+
}, TIME_BEFORE_UNMOUNT);
|
|
2407
|
+
}, [
|
|
2408
|
+
toast2,
|
|
2409
|
+
removeToast,
|
|
2410
|
+
setHeights,
|
|
2411
|
+
offset
|
|
2412
|
+
]);
|
|
2413
|
+
React.useEffect(() => {
|
|
2414
|
+
if (toast2.promise && toastType === "loading" || toast2.duration === Infinity || toast2.type === "loading") return;
|
|
2415
|
+
let timeoutId;
|
|
2416
|
+
const pauseTimer = () => {
|
|
2417
|
+
if (lastCloseTimerStartTimeRef.current < closeTimerStartTimeRef.current) {
|
|
2418
|
+
const elapsedTime = (/* @__PURE__ */ new Date()).getTime() - closeTimerStartTimeRef.current;
|
|
2419
|
+
remainingTime.current = remainingTime.current - elapsedTime;
|
|
2420
|
+
}
|
|
2421
|
+
lastCloseTimerStartTimeRef.current = (/* @__PURE__ */ new Date()).getTime();
|
|
2422
|
+
};
|
|
2423
|
+
const startTimer = () => {
|
|
2424
|
+
if (remainingTime.current === Infinity) return;
|
|
2425
|
+
closeTimerStartTimeRef.current = (/* @__PURE__ */ new Date()).getTime();
|
|
2426
|
+
timeoutId = setTimeout(() => {
|
|
2427
|
+
toast2.onAutoClose == null ? void 0 : toast2.onAutoClose.call(toast2, toast2);
|
|
2428
|
+
deleteToast();
|
|
2429
|
+
}, remainingTime.current);
|
|
2430
|
+
};
|
|
2431
|
+
if (expanded || interacting || isDocumentHidden) {
|
|
2432
|
+
pauseTimer();
|
|
1573
2433
|
} else {
|
|
1574
|
-
|
|
2434
|
+
startTimer();
|
|
2435
|
+
}
|
|
2436
|
+
return () => clearTimeout(timeoutId);
|
|
2437
|
+
}, [
|
|
2438
|
+
expanded,
|
|
2439
|
+
interacting,
|
|
2440
|
+
toast2,
|
|
2441
|
+
toastType,
|
|
2442
|
+
isDocumentHidden,
|
|
2443
|
+
deleteToast
|
|
2444
|
+
]);
|
|
2445
|
+
React.useEffect(() => {
|
|
2446
|
+
if (toast2.delete) {
|
|
2447
|
+
deleteToast();
|
|
2448
|
+
toast2.onDismiss == null ? void 0 : toast2.onDismiss.call(toast2, toast2);
|
|
2449
|
+
}
|
|
2450
|
+
}, [
|
|
2451
|
+
deleteToast,
|
|
2452
|
+
toast2.delete
|
|
2453
|
+
]);
|
|
2454
|
+
function getLoadingIcon() {
|
|
2455
|
+
var _toast_classNames9;
|
|
2456
|
+
if (icons == null ? void 0 : icons.loading) {
|
|
2457
|
+
var _toast_classNames12;
|
|
2458
|
+
return /* @__PURE__ */ React.createElement("div", {
|
|
2459
|
+
className: cn(classNames == null ? void 0 : classNames.loader, toast2 == null ? void 0 : (_toast_classNames12 = toast2.classNames) == null ? void 0 : _toast_classNames12.loader, "sonner-loader"),
|
|
2460
|
+
"data-visible": toastType === "loading"
|
|
2461
|
+
}, icons.loading);
|
|
2462
|
+
}
|
|
2463
|
+
return /* @__PURE__ */ React.createElement(Loader, {
|
|
2464
|
+
className: cn(classNames == null ? void 0 : classNames.loader, toast2 == null ? void 0 : (_toast_classNames9 = toast2.classNames) == null ? void 0 : _toast_classNames9.loader),
|
|
2465
|
+
visible: toastType === "loading"
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
const icon = toast2.icon || (icons == null ? void 0 : icons[toastType]) || getAsset(toastType);
|
|
2469
|
+
var _toast_richColors, _icons_close;
|
|
2470
|
+
return /* @__PURE__ */ React.createElement("li", {
|
|
2471
|
+
tabIndex: 0,
|
|
2472
|
+
ref: toastRef,
|
|
2473
|
+
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]),
|
|
2474
|
+
"data-sonner-toast": "",
|
|
2475
|
+
"data-rich-colors": (_toast_richColors = toast2.richColors) != null ? _toast_richColors : defaultRichColors,
|
|
2476
|
+
"data-styled": !Boolean(toast2.jsx || toast2.unstyled || unstyled),
|
|
2477
|
+
"data-mounted": mounted,
|
|
2478
|
+
"data-promise": Boolean(toast2.promise),
|
|
2479
|
+
"data-swiped": isSwiped,
|
|
2480
|
+
"data-removed": removed,
|
|
2481
|
+
"data-visible": isVisible,
|
|
2482
|
+
"data-y-position": y,
|
|
2483
|
+
"data-x-position": x,
|
|
2484
|
+
"data-index": index,
|
|
2485
|
+
"data-front": isFront,
|
|
2486
|
+
"data-swiping": swiping,
|
|
2487
|
+
"data-dismissible": dismissible,
|
|
2488
|
+
"data-type": toastType,
|
|
2489
|
+
"data-invert": invert,
|
|
2490
|
+
"data-swipe-out": swipeOut,
|
|
2491
|
+
"data-swipe-direction": swipeOutDirection,
|
|
2492
|
+
"data-expanded": Boolean(expanded || expandByDefault && mounted),
|
|
2493
|
+
"data-testid": toast2.testId,
|
|
2494
|
+
style: {
|
|
2495
|
+
"--index": index,
|
|
2496
|
+
"--toasts-before": index,
|
|
2497
|
+
"--z-index": toasts.length - index,
|
|
2498
|
+
"--offset": `${removed ? offsetBeforeRemove : offset.current}px`,
|
|
2499
|
+
"--initial-height": expandByDefault ? "auto" : `${initialHeight}px`,
|
|
2500
|
+
...style,
|
|
2501
|
+
...toast2.style
|
|
2502
|
+
},
|
|
2503
|
+
onDragEnd: () => {
|
|
2504
|
+
setSwiping(false);
|
|
2505
|
+
setSwipeDirection(null);
|
|
2506
|
+
pointerStartRef.current = null;
|
|
2507
|
+
},
|
|
2508
|
+
onPointerDown: (event) => {
|
|
2509
|
+
if (event.button === 2) return;
|
|
2510
|
+
if (disabled || !dismissible) return;
|
|
2511
|
+
dragStartTime.current = /* @__PURE__ */ new Date();
|
|
2512
|
+
setOffsetBeforeRemove(offset.current);
|
|
2513
|
+
event.target.setPointerCapture(event.pointerId);
|
|
2514
|
+
if (event.target.tagName === "BUTTON") return;
|
|
2515
|
+
setSwiping(true);
|
|
2516
|
+
pointerStartRef.current = {
|
|
2517
|
+
x: event.clientX,
|
|
2518
|
+
y: event.clientY
|
|
2519
|
+
};
|
|
2520
|
+
},
|
|
2521
|
+
onPointerUp: () => {
|
|
2522
|
+
var _toastRef_current, _toastRef_current1, _dragStartTime_current;
|
|
2523
|
+
if (swipeOut || !dismissible) return;
|
|
2524
|
+
pointerStartRef.current = null;
|
|
2525
|
+
const swipeAmountX = Number(((_toastRef_current = toastRef.current) == null ? void 0 : _toastRef_current.style.getPropertyValue("--swipe-amount-x").replace("px", "")) || 0);
|
|
2526
|
+
const swipeAmountY = Number(((_toastRef_current1 = toastRef.current) == null ? void 0 : _toastRef_current1.style.getPropertyValue("--swipe-amount-y").replace("px", "")) || 0);
|
|
2527
|
+
const timeTaken = (/* @__PURE__ */ new Date()).getTime() - ((_dragStartTime_current = dragStartTime.current) == null ? void 0 : _dragStartTime_current.getTime());
|
|
2528
|
+
const swipeAmount = swipeDirection === "x" ? swipeAmountX : swipeAmountY;
|
|
2529
|
+
const velocity = Math.abs(swipeAmount) / timeTaken;
|
|
2530
|
+
if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
|
|
2531
|
+
setOffsetBeforeRemove(offset.current);
|
|
2532
|
+
toast2.onDismiss == null ? void 0 : toast2.onDismiss.call(toast2, toast2);
|
|
2533
|
+
if (swipeDirection === "x") {
|
|
2534
|
+
setSwipeOutDirection(swipeAmountX > 0 ? "right" : "left");
|
|
2535
|
+
} else {
|
|
2536
|
+
setSwipeOutDirection(swipeAmountY > 0 ? "down" : "up");
|
|
2537
|
+
}
|
|
2538
|
+
deleteToast();
|
|
2539
|
+
setSwipeOut(true);
|
|
2540
|
+
return;
|
|
2541
|
+
} else {
|
|
2542
|
+
var _toastRef_current2, _toastRef_current3;
|
|
2543
|
+
(_toastRef_current2 = toastRef.current) == null ? void 0 : _toastRef_current2.style.setProperty("--swipe-amount-x", `0px`);
|
|
2544
|
+
(_toastRef_current3 = toastRef.current) == null ? void 0 : _toastRef_current3.style.setProperty("--swipe-amount-y", `0px`);
|
|
2545
|
+
}
|
|
2546
|
+
setIsSwiped(false);
|
|
2547
|
+
setSwiping(false);
|
|
2548
|
+
setSwipeDirection(null);
|
|
2549
|
+
},
|
|
2550
|
+
onPointerMove: (event) => {
|
|
2551
|
+
var _window_getSelection, _toastRef_current, _toastRef_current1;
|
|
2552
|
+
if (!pointerStartRef.current || !dismissible) return;
|
|
2553
|
+
const isHighlighted = ((_window_getSelection = window.getSelection()) == null ? void 0 : _window_getSelection.toString().length) > 0;
|
|
2554
|
+
if (isHighlighted) return;
|
|
2555
|
+
const yDelta = event.clientY - pointerStartRef.current.y;
|
|
2556
|
+
const xDelta = event.clientX - pointerStartRef.current.x;
|
|
2557
|
+
var _props_swipeDirections;
|
|
2558
|
+
const swipeDirections = (_props_swipeDirections = props.swipeDirections) != null ? _props_swipeDirections : getDefaultSwipeDirections(position);
|
|
2559
|
+
if (!swipeDirection && (Math.abs(xDelta) > 1 || Math.abs(yDelta) > 1)) {
|
|
2560
|
+
setSwipeDirection(Math.abs(xDelta) > Math.abs(yDelta) ? "x" : "y");
|
|
2561
|
+
}
|
|
2562
|
+
let swipeAmount = {
|
|
2563
|
+
x: 0,
|
|
2564
|
+
y: 0
|
|
2565
|
+
};
|
|
2566
|
+
const getDampening = (delta) => {
|
|
2567
|
+
const factor = Math.abs(delta) / 20;
|
|
2568
|
+
return 1 / (1.5 + factor);
|
|
2569
|
+
};
|
|
2570
|
+
if (swipeDirection === "y") {
|
|
2571
|
+
if (swipeDirections.includes("top") || swipeDirections.includes("bottom")) {
|
|
2572
|
+
if (swipeDirections.includes("top") && yDelta < 0 || swipeDirections.includes("bottom") && yDelta > 0) {
|
|
2573
|
+
swipeAmount.y = yDelta;
|
|
2574
|
+
} else {
|
|
2575
|
+
const dampenedDelta = yDelta * getDampening(yDelta);
|
|
2576
|
+
swipeAmount.y = Math.abs(dampenedDelta) < Math.abs(yDelta) ? dampenedDelta : yDelta;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
} else if (swipeDirection === "x") {
|
|
2580
|
+
if (swipeDirections.includes("left") || swipeDirections.includes("right")) {
|
|
2581
|
+
if (swipeDirections.includes("left") && xDelta < 0 || swipeDirections.includes("right") && xDelta > 0) {
|
|
2582
|
+
swipeAmount.x = xDelta;
|
|
2583
|
+
} else {
|
|
2584
|
+
const dampenedDelta = xDelta * getDampening(xDelta);
|
|
2585
|
+
swipeAmount.x = Math.abs(dampenedDelta) < Math.abs(xDelta) ? dampenedDelta : xDelta;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
if (Math.abs(swipeAmount.x) > 0 || Math.abs(swipeAmount.y) > 0) {
|
|
2590
|
+
setIsSwiped(true);
|
|
2591
|
+
}
|
|
2592
|
+
(_toastRef_current = toastRef.current) == null ? void 0 : _toastRef_current.style.setProperty("--swipe-amount-x", `${swipeAmount.x}px`);
|
|
2593
|
+
(_toastRef_current1 = toastRef.current) == null ? void 0 : _toastRef_current1.style.setProperty("--swipe-amount-y", `${swipeAmount.y}px`);
|
|
2594
|
+
}
|
|
2595
|
+
}, closeButton && !toast2.jsx && toastType !== "loading" ? /* @__PURE__ */ React.createElement("button", {
|
|
2596
|
+
"aria-label": closeButtonAriaLabel,
|
|
2597
|
+
"data-disabled": disabled,
|
|
2598
|
+
"data-close-button": true,
|
|
2599
|
+
onClick: disabled || !dismissible ? () => {
|
|
2600
|
+
} : () => {
|
|
2601
|
+
deleteToast();
|
|
2602
|
+
toast2.onDismiss == null ? void 0 : toast2.onDismiss.call(toast2, toast2);
|
|
2603
|
+
},
|
|
2604
|
+
className: cn(classNames == null ? void 0 : classNames.closeButton, toast2 == null ? void 0 : (_toast_classNames2 = toast2.classNames) == null ? void 0 : _toast_classNames2.closeButton)
|
|
2605
|
+
}, (_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", {
|
|
2606
|
+
"data-icon": "",
|
|
2607
|
+
className: cn(classNames == null ? void 0 : classNames.icon, toast2 == null ? void 0 : (_toast_classNames3 = toast2.classNames) == null ? void 0 : _toast_classNames3.icon)
|
|
2608
|
+
}, toast2.promise || toast2.type === "loading" && !toast2.icon ? toast2.icon || getLoadingIcon() : null, toast2.type !== "loading" ? icon : null) : null, /* @__PURE__ */ React.createElement("div", {
|
|
2609
|
+
"data-content": "",
|
|
2610
|
+
className: cn(classNames == null ? void 0 : classNames.content, toast2 == null ? void 0 : (_toast_classNames4 = toast2.classNames) == null ? void 0 : _toast_classNames4.content)
|
|
2611
|
+
}, /* @__PURE__ */ React.createElement("div", {
|
|
2612
|
+
"data-title": "",
|
|
2613
|
+
className: cn(classNames == null ? void 0 : classNames.title, toast2 == null ? void 0 : (_toast_classNames5 = toast2.classNames) == null ? void 0 : _toast_classNames5.title)
|
|
2614
|
+
}, toast2.jsx ? toast2.jsx : typeof toast2.title === "function" ? toast2.title() : toast2.title), toast2.description ? /* @__PURE__ */ React.createElement("div", {
|
|
2615
|
+
"data-description": "",
|
|
2616
|
+
className: cn(descriptionClassName, toastDescriptionClassname, classNames == null ? void 0 : classNames.description, toast2 == null ? void 0 : (_toast_classNames6 = toast2.classNames) == null ? void 0 : _toast_classNames6.description)
|
|
2617
|
+
}, 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", {
|
|
2618
|
+
"data-button": true,
|
|
2619
|
+
"data-cancel": true,
|
|
2620
|
+
style: toast2.cancelButtonStyle || cancelButtonStyle,
|
|
2621
|
+
onClick: (event) => {
|
|
2622
|
+
if (!isAction(toast2.cancel)) return;
|
|
2623
|
+
if (!dismissible) return;
|
|
2624
|
+
toast2.cancel.onClick == null ? void 0 : toast2.cancel.onClick.call(toast2.cancel, event);
|
|
2625
|
+
deleteToast();
|
|
2626
|
+
},
|
|
2627
|
+
className: cn(classNames == null ? void 0 : classNames.cancelButton, toast2 == null ? void 0 : (_toast_classNames7 = toast2.classNames) == null ? void 0 : _toast_classNames7.cancelButton)
|
|
2628
|
+
}, toast2.cancel.label) : null, /* @__PURE__ */ React.isValidElement(toast2.action) ? toast2.action : toast2.action && isAction(toast2.action) ? /* @__PURE__ */ React.createElement("button", {
|
|
2629
|
+
"data-button": true,
|
|
2630
|
+
"data-action": true,
|
|
2631
|
+
style: toast2.actionButtonStyle || actionButtonStyle,
|
|
2632
|
+
onClick: (event) => {
|
|
2633
|
+
if (!isAction(toast2.action)) return;
|
|
2634
|
+
toast2.action.onClick == null ? void 0 : toast2.action.onClick.call(toast2.action, event);
|
|
2635
|
+
if (event.defaultPrevented) return;
|
|
2636
|
+
deleteToast();
|
|
2637
|
+
},
|
|
2638
|
+
className: cn(classNames == null ? void 0 : classNames.actionButton, toast2 == null ? void 0 : (_toast_classNames8 = toast2.classNames) == null ? void 0 : _toast_classNames8.actionButton)
|
|
2639
|
+
}, toast2.action.label) : null);
|
|
2640
|
+
};
|
|
2641
|
+
function getDocumentDirection() {
|
|
2642
|
+
if (typeof window === "undefined") return "ltr";
|
|
2643
|
+
if (typeof document === "undefined") return "ltr";
|
|
2644
|
+
const dirAttribute = document.documentElement.getAttribute("dir");
|
|
2645
|
+
if (dirAttribute === "auto" || !dirAttribute) {
|
|
2646
|
+
return window.getComputedStyle(document.documentElement).direction;
|
|
2647
|
+
}
|
|
2648
|
+
return dirAttribute;
|
|
2649
|
+
}
|
|
2650
|
+
function assignOffset(defaultOffset, mobileOffset) {
|
|
2651
|
+
const styles = {};
|
|
2652
|
+
[
|
|
2653
|
+
defaultOffset,
|
|
2654
|
+
mobileOffset
|
|
2655
|
+
].forEach((offset, index) => {
|
|
2656
|
+
const isMobile = index === 1;
|
|
2657
|
+
const prefix = isMobile ? "--mobile-offset" : "--offset";
|
|
2658
|
+
const defaultValue = isMobile ? MOBILE_VIEWPORT_OFFSET : VIEWPORT_OFFSET;
|
|
2659
|
+
function assignAll(offset2) {
|
|
2660
|
+
[
|
|
2661
|
+
"top",
|
|
2662
|
+
"right",
|
|
2663
|
+
"bottom",
|
|
2664
|
+
"left"
|
|
2665
|
+
].forEach((key) => {
|
|
2666
|
+
styles[`${prefix}-${key}`] = typeof offset2 === "number" ? `${offset2}px` : offset2;
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
if (typeof offset === "number" || typeof offset === "string") {
|
|
2670
|
+
assignAll(offset);
|
|
2671
|
+
} else if (typeof offset === "object") {
|
|
2672
|
+
[
|
|
2673
|
+
"top",
|
|
2674
|
+
"right",
|
|
2675
|
+
"bottom",
|
|
2676
|
+
"left"
|
|
2677
|
+
].forEach((key) => {
|
|
2678
|
+
if (offset[key] === void 0) {
|
|
2679
|
+
styles[`${prefix}-${key}`] = defaultValue;
|
|
2680
|
+
} else {
|
|
2681
|
+
styles[`${prefix}-${key}`] = typeof offset[key] === "number" ? `${offset[key]}px` : offset[key];
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
2684
|
+
} else {
|
|
2685
|
+
assignAll(defaultValue);
|
|
1575
2686
|
}
|
|
1576
|
-
};
|
|
1577
|
-
return trigger(start, {
|
|
1578
|
-
asChild,
|
|
1579
|
-
children: children ?? "Add to my plan",
|
|
1580
|
-
className,
|
|
1581
|
-
disabled: checkout.isLoading
|
|
1582
2687
|
});
|
|
1583
|
-
|
|
2688
|
+
return styles;
|
|
2689
|
+
}
|
|
2690
|
+
var Toaster = /* @__PURE__ */ React.forwardRef(function Toaster2(props, ref) {
|
|
2691
|
+
const { id, invert, position = "bottom-right", hotkey = [
|
|
2692
|
+
"altKey",
|
|
2693
|
+
"KeyT"
|
|
2694
|
+
], expand, closeButton, className, offset, mobileOffset, theme = "light", richColors, duration, style, visibleToasts = VISIBLE_TOASTS_AMOUNT, toastOptions, dir = getDocumentDirection(), gap = GAP, icons, containerAriaLabel = "Notifications" } = props;
|
|
2695
|
+
const [toasts, setToasts] = React.useState([]);
|
|
2696
|
+
const filteredToasts = React.useMemo(() => {
|
|
2697
|
+
if (id) {
|
|
2698
|
+
return toasts.filter((toast2) => toast2.toasterId === id);
|
|
2699
|
+
}
|
|
2700
|
+
return toasts.filter((toast2) => !toast2.toasterId);
|
|
2701
|
+
}, [
|
|
2702
|
+
toasts,
|
|
2703
|
+
id
|
|
2704
|
+
]);
|
|
2705
|
+
const possiblePositions = React.useMemo(() => {
|
|
2706
|
+
return Array.from(new Set([
|
|
2707
|
+
position
|
|
2708
|
+
].concat(filteredToasts.filter((toast2) => toast2.position).map((toast2) => toast2.position))));
|
|
2709
|
+
}, [
|
|
2710
|
+
filteredToasts,
|
|
2711
|
+
position
|
|
2712
|
+
]);
|
|
2713
|
+
const [heights, setHeights] = React.useState([]);
|
|
2714
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
2715
|
+
const [interacting, setInteracting] = React.useState(false);
|
|
2716
|
+
const [actualTheme, setActualTheme] = React.useState(theme !== "system" ? theme : typeof window !== "undefined" ? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : "light");
|
|
2717
|
+
const listRef = React.useRef(null);
|
|
2718
|
+
const hotkeyLabel = hotkey.join("+").replace(/Key/g, "").replace(/Digit/g, "");
|
|
2719
|
+
const lastFocusedElementRef = React.useRef(null);
|
|
2720
|
+
const isFocusWithinRef = React.useRef(false);
|
|
2721
|
+
const removeToast = React.useCallback((toastToRemove) => {
|
|
2722
|
+
setToasts((toasts2) => {
|
|
2723
|
+
var _toasts_find;
|
|
2724
|
+
if (!((_toasts_find = toasts2.find((toast2) => toast2.id === toastToRemove.id)) == null ? void 0 : _toasts_find.delete)) {
|
|
2725
|
+
ToastState.dismiss(toastToRemove.id);
|
|
2726
|
+
}
|
|
2727
|
+
return toasts2.filter(({ id: id2 }) => id2 !== toastToRemove.id);
|
|
2728
|
+
});
|
|
2729
|
+
}, []);
|
|
2730
|
+
React.useEffect(() => {
|
|
2731
|
+
return ToastState.subscribe((toast2) => {
|
|
2732
|
+
if (toast2.dismiss) {
|
|
2733
|
+
requestAnimationFrame(() => {
|
|
2734
|
+
setToasts((toasts2) => toasts2.map((t) => t.id === toast2.id ? {
|
|
2735
|
+
...t,
|
|
2736
|
+
delete: true
|
|
2737
|
+
} : t));
|
|
2738
|
+
});
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
setTimeout(() => {
|
|
2742
|
+
ReactDOM.flushSync(() => {
|
|
2743
|
+
setToasts((toasts2) => {
|
|
2744
|
+
const indexOfExistingToast = toasts2.findIndex((t) => t.id === toast2.id);
|
|
2745
|
+
if (indexOfExistingToast !== -1) {
|
|
2746
|
+
return [
|
|
2747
|
+
...toasts2.slice(0, indexOfExistingToast),
|
|
2748
|
+
{
|
|
2749
|
+
...toasts2[indexOfExistingToast],
|
|
2750
|
+
...toast2
|
|
2751
|
+
},
|
|
2752
|
+
...toasts2.slice(indexOfExistingToast + 1)
|
|
2753
|
+
];
|
|
2754
|
+
}
|
|
2755
|
+
return [
|
|
2756
|
+
toast2,
|
|
2757
|
+
...toasts2
|
|
2758
|
+
];
|
|
2759
|
+
});
|
|
2760
|
+
});
|
|
2761
|
+
});
|
|
2762
|
+
});
|
|
2763
|
+
}, [
|
|
2764
|
+
toasts
|
|
2765
|
+
]);
|
|
2766
|
+
React.useEffect(() => {
|
|
2767
|
+
if (theme !== "system") {
|
|
2768
|
+
setActualTheme(theme);
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
if (theme === "system") {
|
|
2772
|
+
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
2773
|
+
setActualTheme("dark");
|
|
2774
|
+
} else {
|
|
2775
|
+
setActualTheme("light");
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
if (typeof window === "undefined") return;
|
|
2779
|
+
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
2780
|
+
try {
|
|
2781
|
+
darkMediaQuery.addEventListener("change", ({ matches }) => {
|
|
2782
|
+
if (matches) {
|
|
2783
|
+
setActualTheme("dark");
|
|
2784
|
+
} else {
|
|
2785
|
+
setActualTheme("light");
|
|
2786
|
+
}
|
|
2787
|
+
});
|
|
2788
|
+
} catch (error) {
|
|
2789
|
+
darkMediaQuery.addListener(({ matches }) => {
|
|
2790
|
+
try {
|
|
2791
|
+
if (matches) {
|
|
2792
|
+
setActualTheme("dark");
|
|
2793
|
+
} else {
|
|
2794
|
+
setActualTheme("light");
|
|
2795
|
+
}
|
|
2796
|
+
} catch (e) {
|
|
2797
|
+
console.error(e);
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
}, [
|
|
2802
|
+
theme
|
|
2803
|
+
]);
|
|
2804
|
+
React.useEffect(() => {
|
|
2805
|
+
if (toasts.length <= 1) {
|
|
2806
|
+
setExpanded(false);
|
|
2807
|
+
}
|
|
2808
|
+
}, [
|
|
2809
|
+
toasts
|
|
2810
|
+
]);
|
|
2811
|
+
React.useEffect(() => {
|
|
2812
|
+
const handleKeyDown = (event) => {
|
|
2813
|
+
var _listRef_current;
|
|
2814
|
+
const isHotkeyPressed = hotkey.every((key) => event[key] || event.code === key);
|
|
2815
|
+
if (isHotkeyPressed) {
|
|
2816
|
+
var _listRef_current1;
|
|
2817
|
+
setExpanded(true);
|
|
2818
|
+
(_listRef_current1 = listRef.current) == null ? void 0 : _listRef_current1.focus();
|
|
2819
|
+
}
|
|
2820
|
+
if (event.code === "Escape" && (document.activeElement === listRef.current || ((_listRef_current = listRef.current) == null ? void 0 : _listRef_current.contains(document.activeElement)))) {
|
|
2821
|
+
setExpanded(false);
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
2825
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
2826
|
+
}, [
|
|
2827
|
+
hotkey
|
|
2828
|
+
]);
|
|
2829
|
+
React.useEffect(() => {
|
|
2830
|
+
if (listRef.current) {
|
|
2831
|
+
return () => {
|
|
2832
|
+
if (lastFocusedElementRef.current) {
|
|
2833
|
+
lastFocusedElementRef.current.focus({
|
|
2834
|
+
preventScroll: true
|
|
2835
|
+
});
|
|
2836
|
+
lastFocusedElementRef.current = null;
|
|
2837
|
+
isFocusWithinRef.current = false;
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
}, [
|
|
2842
|
+
listRef.current
|
|
2843
|
+
]);
|
|
2844
|
+
return (
|
|
2845
|
+
// Remove item from normal navigation flow, only available via hotkey
|
|
2846
|
+
/* @__PURE__ */ React.createElement("section", {
|
|
2847
|
+
ref,
|
|
2848
|
+
"aria-label": `${containerAriaLabel} ${hotkeyLabel}`,
|
|
2849
|
+
tabIndex: -1,
|
|
2850
|
+
"aria-live": "polite",
|
|
2851
|
+
"aria-relevant": "additions text",
|
|
2852
|
+
"aria-atomic": "false",
|
|
2853
|
+
suppressHydrationWarning: true
|
|
2854
|
+
}, possiblePositions.map((position2, index) => {
|
|
2855
|
+
var _heights_;
|
|
2856
|
+
const [y, x] = position2.split("-");
|
|
2857
|
+
if (!filteredToasts.length) return null;
|
|
2858
|
+
return /* @__PURE__ */ React.createElement("ol", {
|
|
2859
|
+
key: position2,
|
|
2860
|
+
dir: dir === "auto" ? getDocumentDirection() : dir,
|
|
2861
|
+
tabIndex: -1,
|
|
2862
|
+
ref: listRef,
|
|
2863
|
+
className,
|
|
2864
|
+
"data-sonner-toaster": true,
|
|
2865
|
+
"data-sonner-theme": actualTheme,
|
|
2866
|
+
"data-y-position": y,
|
|
2867
|
+
"data-x-position": x,
|
|
2868
|
+
style: {
|
|
2869
|
+
"--front-toast-height": `${((_heights_ = heights[0]) == null ? void 0 : _heights_.height) || 0}px`,
|
|
2870
|
+
"--width": `${TOAST_WIDTH}px`,
|
|
2871
|
+
"--gap": `${gap}px`,
|
|
2872
|
+
...style,
|
|
2873
|
+
...assignOffset(offset, mobileOffset)
|
|
2874
|
+
},
|
|
2875
|
+
onBlur: (event) => {
|
|
2876
|
+
if (isFocusWithinRef.current && !event.currentTarget.contains(event.relatedTarget)) {
|
|
2877
|
+
isFocusWithinRef.current = false;
|
|
2878
|
+
if (lastFocusedElementRef.current) {
|
|
2879
|
+
lastFocusedElementRef.current.focus({
|
|
2880
|
+
preventScroll: true
|
|
2881
|
+
});
|
|
2882
|
+
lastFocusedElementRef.current = null;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
},
|
|
2886
|
+
onFocus: (event) => {
|
|
2887
|
+
const isNotDismissible = event.target instanceof HTMLElement && event.target.dataset.dismissible === "false";
|
|
2888
|
+
if (isNotDismissible) return;
|
|
2889
|
+
if (!isFocusWithinRef.current) {
|
|
2890
|
+
isFocusWithinRef.current = true;
|
|
2891
|
+
lastFocusedElementRef.current = event.relatedTarget;
|
|
2892
|
+
}
|
|
2893
|
+
},
|
|
2894
|
+
onMouseEnter: () => setExpanded(true),
|
|
2895
|
+
onMouseMove: () => setExpanded(true),
|
|
2896
|
+
onMouseLeave: () => {
|
|
2897
|
+
if (!interacting) {
|
|
2898
|
+
setExpanded(false);
|
|
2899
|
+
}
|
|
2900
|
+
},
|
|
2901
|
+
onDragEnd: () => setExpanded(false),
|
|
2902
|
+
onPointerDown: (event) => {
|
|
2903
|
+
const isNotDismissible = event.target instanceof HTMLElement && event.target.dataset.dismissible === "false";
|
|
2904
|
+
if (isNotDismissible) return;
|
|
2905
|
+
setInteracting(true);
|
|
2906
|
+
},
|
|
2907
|
+
onPointerUp: () => setInteracting(false)
|
|
2908
|
+
}, filteredToasts.filter((toast2) => !toast2.position && index === 0 || toast2.position === position2).map((toast2, index2) => {
|
|
2909
|
+
var _toastOptions_duration, _toastOptions_closeButton;
|
|
2910
|
+
return /* @__PURE__ */ React.createElement(Toast, {
|
|
2911
|
+
key: toast2.id,
|
|
2912
|
+
icons,
|
|
2913
|
+
index: index2,
|
|
2914
|
+
toast: toast2,
|
|
2915
|
+
defaultRichColors: richColors,
|
|
2916
|
+
duration: (_toastOptions_duration = toastOptions == null ? void 0 : toastOptions.duration) != null ? _toastOptions_duration : duration,
|
|
2917
|
+
className: toastOptions == null ? void 0 : toastOptions.className,
|
|
2918
|
+
descriptionClassName: toastOptions == null ? void 0 : toastOptions.descriptionClassName,
|
|
2919
|
+
invert,
|
|
2920
|
+
visibleToasts,
|
|
2921
|
+
closeButton: (_toastOptions_closeButton = toastOptions == null ? void 0 : toastOptions.closeButton) != null ? _toastOptions_closeButton : closeButton,
|
|
2922
|
+
interacting,
|
|
2923
|
+
position: position2,
|
|
2924
|
+
style: toastOptions == null ? void 0 : toastOptions.style,
|
|
2925
|
+
unstyled: toastOptions == null ? void 0 : toastOptions.unstyled,
|
|
2926
|
+
classNames: toastOptions == null ? void 0 : toastOptions.classNames,
|
|
2927
|
+
cancelButtonStyle: toastOptions == null ? void 0 : toastOptions.cancelButtonStyle,
|
|
2928
|
+
actionButtonStyle: toastOptions == null ? void 0 : toastOptions.actionButtonStyle,
|
|
2929
|
+
closeButtonAriaLabel: toastOptions == null ? void 0 : toastOptions.closeButtonAriaLabel,
|
|
2930
|
+
removeToast,
|
|
2931
|
+
toasts: filteredToasts.filter((t) => t.position == toast2.position),
|
|
2932
|
+
heights: heights.filter((h) => h.position == toast2.position),
|
|
2933
|
+
setHeights,
|
|
2934
|
+
expandByDefault: expand,
|
|
2935
|
+
gap,
|
|
2936
|
+
expanded,
|
|
2937
|
+
swipeDirections: props.swipeDirections
|
|
2938
|
+
});
|
|
2939
|
+
}));
|
|
2940
|
+
}))
|
|
2941
|
+
);
|
|
2942
|
+
});
|
|
1584
2943
|
function FunnelToaster(props = {}) {
|
|
1585
2944
|
return createElement(Toaster, { position: "top-center", ...props });
|
|
1586
2945
|
}
|
|
1587
2946
|
function renderToaster(toaster) {
|
|
1588
2947
|
if (toaster === false) return null;
|
|
1589
|
-
return
|
|
2948
|
+
return /* @__PURE__ */ jsx(FunnelToaster, { ...toaster === true ? {} : toaster });
|
|
1590
2949
|
}
|
|
1591
2950
|
|
|
1592
2951
|
// src/state/namespaces.ts
|
|
@@ -1610,7 +2969,8 @@ var Namespace = class {
|
|
|
1610
2969
|
const out = {};
|
|
1611
2970
|
const all = this.store.getState();
|
|
1612
2971
|
for (const key in all) {
|
|
1613
|
-
if (key.startsWith(this.dot))
|
|
2972
|
+
if (key.startsWith(this.dot))
|
|
2973
|
+
out[key.slice(this.dot.length)] = all[key];
|
|
1614
2974
|
}
|
|
1615
2975
|
return out;
|
|
1616
2976
|
}
|
|
@@ -1656,9 +3016,10 @@ var FunnelCtx = createContext(null);
|
|
|
1656
3016
|
function FunnelProvider({
|
|
1657
3017
|
config,
|
|
1658
3018
|
sessionValues,
|
|
3019
|
+
deferSessionValues = false,
|
|
1659
3020
|
tracker,
|
|
1660
3021
|
context: contextOpts,
|
|
1661
|
-
|
|
3022
|
+
offerings,
|
|
1662
3023
|
experiments,
|
|
1663
3024
|
checkout,
|
|
1664
3025
|
messages,
|
|
@@ -1671,7 +3032,12 @@ function FunnelProvider({
|
|
|
1671
3032
|
const [value] = useState(() => {
|
|
1672
3033
|
const s = store ?? createFunnelStore(
|
|
1673
3034
|
{ responses: config?.responses, data: config?.data },
|
|
1674
|
-
|
|
3035
|
+
// FLASH-FREE HYDRATION: with deferSessionValues the initial render is seeded with
|
|
3036
|
+
// DEFAULTS ONLY, so the client's first render is byte-identical to the build-time
|
|
3037
|
+
// DEFAULT-state prerender it hydrates against (clean match). The visitor's saved
|
|
3038
|
+
// answers arrive as a post-hydration setMany below. Without the flag, sessionValues
|
|
3039
|
+
// seed the store at construction (SSR + non-prerendered paths).
|
|
3040
|
+
deferSessionValues ? void 0 : sessionValues
|
|
1675
3041
|
);
|
|
1676
3042
|
const ctx = buildContext({
|
|
1677
3043
|
funnelId: config?.id,
|
|
@@ -1685,17 +3051,37 @@ function FunnelProvider({
|
|
|
1685
3051
|
getVisitorId: () => ctx.identity.visitorId
|
|
1686
3052
|
});
|
|
1687
3053
|
const busTracker = withBus(resolvedTracker, bus);
|
|
3054
|
+
const emailForCheckout = () => {
|
|
3055
|
+
const value2 = s.get("user.email");
|
|
3056
|
+
return typeof value2 === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value2) ? value2 : void 0;
|
|
3057
|
+
};
|
|
3058
|
+
const catalog = buildCatalog(offerings ?? []);
|
|
1688
3059
|
return {
|
|
1689
3060
|
funnel: createFunnel(s, ctx, busTracker, experiments ?? []),
|
|
1690
3061
|
store: s,
|
|
1691
3062
|
context: ctx,
|
|
1692
|
-
catalog
|
|
1693
|
-
driver
|
|
3063
|
+
catalog,
|
|
3064
|
+
// Slot → catalog-key resolution wraps the email-enriching driver, so the wire charges the real
|
|
3065
|
+
// catalog key while pages + analytics keep the durable SLOT identity (req.product).
|
|
3066
|
+
driver: driverWithCatalog(
|
|
3067
|
+
driverWithEmail(
|
|
3068
|
+
checkout ?? createMockDriver(),
|
|
3069
|
+
emailForCheckout
|
|
3070
|
+
),
|
|
3071
|
+
catalog
|
|
3072
|
+
),
|
|
1694
3073
|
tracker: busTracker,
|
|
1695
3074
|
bus
|
|
1696
3075
|
};
|
|
1697
3076
|
});
|
|
1698
3077
|
useEffect(() => attachBus(value.bus), [value.bus]);
|
|
3078
|
+
useEffect(() => {
|
|
3079
|
+
if (!deferSessionValues || !sessionValues) return;
|
|
3080
|
+
const updates = {};
|
|
3081
|
+
for (const [key, v] of Object.entries(sessionValues))
|
|
3082
|
+
if (v !== void 0) updates[key] = v;
|
|
3083
|
+
if (Object.keys(updates).length > 0) value.store.setMany(updates);
|
|
3084
|
+
}, []);
|
|
1699
3085
|
useEffect(() => {
|
|
1700
3086
|
value.tracker.setAcquisition?.(buildAcquisition(value.context));
|
|
1701
3087
|
}, [value]);
|
|
@@ -1724,13 +3110,18 @@ var FALLBACK_TRACKER = createConsoleTracker({ silent: true });
|
|
|
1724
3110
|
function useFunnelCtx() {
|
|
1725
3111
|
const ctx = useContext(FunnelCtx);
|
|
1726
3112
|
if (!ctx) {
|
|
1727
|
-
throw new Error(
|
|
3113
|
+
throw new Error(
|
|
3114
|
+
"useFunnel/useResponse/\u2026 must be used inside <FunnelProvider>"
|
|
3115
|
+
);
|
|
1728
3116
|
}
|
|
1729
3117
|
return ctx;
|
|
1730
3118
|
}
|
|
1731
3119
|
function useFunnel() {
|
|
1732
3120
|
return useFunnelCtx().funnel;
|
|
1733
3121
|
}
|
|
3122
|
+
function useBusInternal() {
|
|
3123
|
+
return useContext(FunnelCtx)?.bus ?? null;
|
|
3124
|
+
}
|
|
1734
3125
|
function useStoreKey(store, key) {
|
|
1735
3126
|
const subscribe = useCallback(
|
|
1736
3127
|
(cb) => store.subscribe(cb, { keys: [key] }),
|
|
@@ -1759,7 +3150,9 @@ function useField(path) {
|
|
|
1759
3150
|
const key = path.slice(dot + 1);
|
|
1760
3151
|
const { funnel, store } = useFunnelCtx();
|
|
1761
3152
|
if (!(ns in NAMESPACE_PREFIX)) {
|
|
1762
|
-
throw new Error(
|
|
3153
|
+
throw new Error(
|
|
3154
|
+
`useField: "${path}" is not a writable namespace (user/responses/data)`
|
|
3155
|
+
);
|
|
1763
3156
|
}
|
|
1764
3157
|
const value = useStoreKey(store, `${NAMESPACE_PREFIX[ns]}.${key}`);
|
|
1765
3158
|
const set = useCallback(
|
|
@@ -1789,10 +3182,403 @@ function useSystem() {
|
|
|
1789
3182
|
function useContextValue(path) {
|
|
1790
3183
|
const ctx = useContextObject();
|
|
1791
3184
|
return useMemo(
|
|
1792
|
-
() => path.split(".").reduce(
|
|
3185
|
+
() => path.split(".").reduce(
|
|
3186
|
+
(acc, k) => acc?.[k],
|
|
3187
|
+
ctx
|
|
3188
|
+
),
|
|
1793
3189
|
[ctx, path]
|
|
1794
3190
|
);
|
|
1795
3191
|
}
|
|
3192
|
+
|
|
3193
|
+
// src/tracking/snapshotPersistence.ts
|
|
3194
|
+
function createSnapshotPersistence(opts) {
|
|
3195
|
+
const baseDelay = opts.baseDelayMs ?? 1e3;
|
|
3196
|
+
const maxDelay = opts.maxDelayMs ?? 3e4;
|
|
3197
|
+
const setT = opts.timers?.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
3198
|
+
const clearT = opts.timers?.clearTimeout ?? ((h) => clearTimeout(h));
|
|
3199
|
+
const rand = opts.random ?? Math.random;
|
|
3200
|
+
let pending = null;
|
|
3201
|
+
let seq = 0;
|
|
3202
|
+
let pendingSeq = 0;
|
|
3203
|
+
let inFlight = false;
|
|
3204
|
+
let attempt = 0;
|
|
3205
|
+
let retryTimer = null;
|
|
3206
|
+
let disposed = false;
|
|
3207
|
+
let fatalSeq = -1;
|
|
3208
|
+
const clearRetry = () => {
|
|
3209
|
+
if (retryTimer !== null) {
|
|
3210
|
+
clearT(retryTimer);
|
|
3211
|
+
retryTimer = null;
|
|
3212
|
+
}
|
|
3213
|
+
};
|
|
3214
|
+
const buildBody = (snapshot, s) => JSON.stringify({ sessionId: opts.getSessionId(), userData: snapshot, seq: s });
|
|
3215
|
+
const trySend = () => {
|
|
3216
|
+
if (disposed || inFlight || pending === null) return;
|
|
3217
|
+
if (retryTimer !== null) return;
|
|
3218
|
+
const sessionId = opts.getSessionId();
|
|
3219
|
+
if (!sessionId) return;
|
|
3220
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) return;
|
|
3221
|
+
if (pendingSeq === fatalSeq) return;
|
|
3222
|
+
const snapshot = pending;
|
|
3223
|
+
const sentSeq = pendingSeq;
|
|
3224
|
+
const body = buildBody(snapshot, sentSeq);
|
|
3225
|
+
inFlight = true;
|
|
3226
|
+
void fetch(opts.url, {
|
|
3227
|
+
method: "POST",
|
|
3228
|
+
headers: { "content-type": "application/json" },
|
|
3229
|
+
body,
|
|
3230
|
+
keepalive: true
|
|
3231
|
+
}).then((res) => {
|
|
3232
|
+
inFlight = false;
|
|
3233
|
+
if (disposed) return;
|
|
3234
|
+
if (res.ok) {
|
|
3235
|
+
attempt = 0;
|
|
3236
|
+
if (pendingSeq === sentSeq) pending = null;
|
|
3237
|
+
else trySend();
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
if (res.status >= 400 && res.status < 500) {
|
|
3241
|
+
if (process.env.NODE_ENV !== "production") {
|
|
3242
|
+
console.warn(
|
|
3243
|
+
`[appfunnel] snapshot persist rejected ${res.status} (dropping snapshot; check session/payload)`
|
|
3244
|
+
);
|
|
3245
|
+
}
|
|
3246
|
+
fatalSeq = sentSeq;
|
|
3247
|
+
if (pendingSeq === sentSeq) pending = null;
|
|
3248
|
+
else trySend();
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
scheduleRetry();
|
|
3252
|
+
}).catch(() => {
|
|
3253
|
+
inFlight = false;
|
|
3254
|
+
if (disposed) return;
|
|
3255
|
+
scheduleRetry();
|
|
3256
|
+
});
|
|
3257
|
+
};
|
|
3258
|
+
const scheduleRetry = () => {
|
|
3259
|
+
if (disposed || pending === null) return;
|
|
3260
|
+
const exp = Math.min(maxDelay, baseDelay * 2 ** attempt);
|
|
3261
|
+
const delay2 = Math.round(exp * (0.5 + rand() * 0.5));
|
|
3262
|
+
attempt++;
|
|
3263
|
+
clearRetry();
|
|
3264
|
+
retryTimer = setT(() => {
|
|
3265
|
+
retryTimer = null;
|
|
3266
|
+
trySend();
|
|
3267
|
+
}, delay2);
|
|
3268
|
+
};
|
|
3269
|
+
const push = (snapshot) => {
|
|
3270
|
+
if (disposed) return;
|
|
3271
|
+
pending = snapshot;
|
|
3272
|
+
pendingSeq = ++seq;
|
|
3273
|
+
if (fatalSeq !== -1 && pendingSeq !== fatalSeq) fatalSeq = -1;
|
|
3274
|
+
if (retryTimer === null) trySend();
|
|
3275
|
+
};
|
|
3276
|
+
const flushBeacon = () => {
|
|
3277
|
+
if (disposed || pending === null) return;
|
|
3278
|
+
const sessionId = opts.getSessionId();
|
|
3279
|
+
if (!sessionId) return;
|
|
3280
|
+
const body = buildBody(pending, pendingSeq);
|
|
3281
|
+
let delivered = false;
|
|
3282
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
3283
|
+
try {
|
|
3284
|
+
delivered = navigator.sendBeacon(
|
|
3285
|
+
opts.url,
|
|
3286
|
+
new Blob([body], { type: "application/json" })
|
|
3287
|
+
);
|
|
3288
|
+
} catch {
|
|
3289
|
+
delivered = false;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
if (!delivered) {
|
|
3293
|
+
try {
|
|
3294
|
+
void fetch(opts.url, {
|
|
3295
|
+
method: "POST",
|
|
3296
|
+
headers: { "content-type": "application/json" },
|
|
3297
|
+
body,
|
|
3298
|
+
keepalive: true
|
|
3299
|
+
}).catch(() => {
|
|
3300
|
+
});
|
|
3301
|
+
delivered = true;
|
|
3302
|
+
} catch {
|
|
3303
|
+
delivered = false;
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
if (delivered) pending = null;
|
|
3307
|
+
};
|
|
3308
|
+
const onHidden = () => {
|
|
3309
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
3310
|
+
flushBeacon();
|
|
3311
|
+
}
|
|
3312
|
+
};
|
|
3313
|
+
const onPageHide = () => flushBeacon();
|
|
3314
|
+
const onOnline = () => trySend();
|
|
3315
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
3316
|
+
try {
|
|
3317
|
+
document.addEventListener("visibilitychange", onHidden);
|
|
3318
|
+
window.addEventListener("pagehide", onPageHide);
|
|
3319
|
+
window.addEventListener("online", onOnline);
|
|
3320
|
+
} catch {
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
const dispose = () => {
|
|
3324
|
+
disposed = true;
|
|
3325
|
+
clearRetry();
|
|
3326
|
+
pending = null;
|
|
3327
|
+
if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
|
|
3328
|
+
try {
|
|
3329
|
+
document.removeEventListener("visibilitychange", onHidden);
|
|
3330
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
3331
|
+
window.removeEventListener("online", onOnline);
|
|
3332
|
+
} catch {
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
return { push, flushBeacon, dispose };
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
// src/tracking/platformTracker.ts
|
|
3340
|
+
function validClickIds(acq) {
|
|
3341
|
+
return Object.entries(acq.clickIds ?? {}).filter(
|
|
3342
|
+
(e) => typeof e[1] === "string" && e[1] !== ""
|
|
3343
|
+
);
|
|
3344
|
+
}
|
|
3345
|
+
function acquisitionQueryBag(acq) {
|
|
3346
|
+
const q = {};
|
|
3347
|
+
const utm = acq.utm ?? {};
|
|
3348
|
+
for (const [k, v] of Object.entries({
|
|
3349
|
+
utm_source: utm.source,
|
|
3350
|
+
utm_medium: utm.medium,
|
|
3351
|
+
utm_campaign: utm.campaign,
|
|
3352
|
+
utm_term: utm.term,
|
|
3353
|
+
utm_content: utm.content
|
|
3354
|
+
})) if (v) q[k] = v;
|
|
3355
|
+
for (const [k, v] of validClickIds(acq)) q[k] = v;
|
|
3356
|
+
if (acq.cookies?.fbp) q._fbp = acq.cookies.fbp;
|
|
3357
|
+
if (acq.cookies?.fbc) q._fbc = acq.cookies.fbc;
|
|
3358
|
+
return q;
|
|
3359
|
+
}
|
|
3360
|
+
function acquisitionAdAttribution(acq) {
|
|
3361
|
+
const a = {};
|
|
3362
|
+
for (const [k, v] of validClickIds(acq)) a[k] = v;
|
|
3363
|
+
if (acq.cookies?.fbp) a.fbp = acq.cookies.fbp;
|
|
3364
|
+
if (acq.cookies?.fbc) a.fbc = acq.cookies.fbc;
|
|
3365
|
+
else if (acq.clickIds?.fbclid) a.fbc = `fb.1.${Date.now()}.${acq.clickIds.fbclid}`;
|
|
3366
|
+
return a;
|
|
3367
|
+
}
|
|
3368
|
+
function createPlatformTracker(cfg) {
|
|
3369
|
+
if (typeof window === "undefined" || cfg.mode === "test") return createConsoleTracker();
|
|
3370
|
+
const base = cfg.apiBase.replace(/\/+$/, "");
|
|
3371
|
+
const url = `${base}/campaign/${encodeURIComponent(cfg.campaignId)}/event`;
|
|
3372
|
+
const landingUrl = window.location.href;
|
|
3373
|
+
const sidKey = `af_sid:${cfg.campaignId}`;
|
|
3374
|
+
let sessionId = null;
|
|
3375
|
+
try {
|
|
3376
|
+
sessionId = window.sessionStorage.getItem(sidKey);
|
|
3377
|
+
} catch {
|
|
3378
|
+
}
|
|
3379
|
+
if (!sessionId && cfg.sessionId) sessionId = cfg.sessionId;
|
|
3380
|
+
let acquisition = null;
|
|
3381
|
+
let acquisitionSent = false;
|
|
3382
|
+
let chain = Promise.resolve();
|
|
3383
|
+
const send = async (event, data, userData) => {
|
|
3384
|
+
let mergedUserData = landingUrl ? { landingUrl, ...userData } : userData;
|
|
3385
|
+
let adAttribution;
|
|
3386
|
+
let attachingAcquisition = false;
|
|
3387
|
+
if (acquisition && !acquisitionSent) {
|
|
3388
|
+
attachingAcquisition = true;
|
|
3389
|
+
const query = acquisitionQueryBag(acquisition);
|
|
3390
|
+
if (Object.keys(query).length) mergedUserData = { ...mergedUserData, query };
|
|
3391
|
+
const ad = acquisitionAdAttribution(acquisition);
|
|
3392
|
+
if (Object.keys(ad).length) adAttribution = ad;
|
|
3393
|
+
}
|
|
3394
|
+
const body = {
|
|
3395
|
+
campaignId: cfg.campaignId,
|
|
3396
|
+
funnelId: cfg.funnelId,
|
|
3397
|
+
event,
|
|
3398
|
+
sessionId: sessionId ?? void 0,
|
|
3399
|
+
visitorId: cfg.visitorId ?? void 0,
|
|
3400
|
+
// Campaign route stamp (campaign-routing pivot): ingestion reads this ONLY at session create
|
|
3401
|
+
// (WebFunnelSession.campaignRouteId, snapshot semantics) — carried on every event but ignored
|
|
3402
|
+
// once the session exists. Undefined ⇒ default funnel / no route matched.
|
|
3403
|
+
campaignRouteId: cfg.route?.id ?? void 0,
|
|
3404
|
+
eventId: typeof data?.eventId === "string" ? data.eventId : void 0,
|
|
3405
|
+
data: data ?? {},
|
|
3406
|
+
metadata: {
|
|
3407
|
+
language: typeof navigator !== "undefined" ? navigator.language : void 0,
|
|
3408
|
+
screen: typeof window.screen !== "undefined" ? `${window.screen.width}x${window.screen.height}` : void 0,
|
|
3409
|
+
timezone: (() => {
|
|
3410
|
+
try {
|
|
3411
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
3412
|
+
} catch {
|
|
3413
|
+
return void 0;
|
|
3414
|
+
}
|
|
3415
|
+
})(),
|
|
3416
|
+
referrer: typeof document !== "undefined" && document.referrer ? document.referrer : void 0
|
|
3417
|
+
},
|
|
3418
|
+
...mergedUserData ? { userData: mergedUserData } : {},
|
|
3419
|
+
...adAttribution ? { adAttribution } : {}
|
|
3420
|
+
};
|
|
3421
|
+
const res = await fetch(url, {
|
|
3422
|
+
method: "POST",
|
|
3423
|
+
headers: { "content-type": "application/json" },
|
|
3424
|
+
body: JSON.stringify(body),
|
|
3425
|
+
keepalive: true
|
|
3426
|
+
// survives page.exit / unload
|
|
3427
|
+
});
|
|
3428
|
+
if (attachingAcquisition) acquisitionSent = true;
|
|
3429
|
+
const json = await res.json().catch(() => null);
|
|
3430
|
+
if (json?.sessionId && json.sessionId !== sessionId) {
|
|
3431
|
+
sessionId = json.sessionId;
|
|
3432
|
+
try {
|
|
3433
|
+
window.sessionStorage.setItem(sidKey, sessionId);
|
|
3434
|
+
} catch {
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
};
|
|
3438
|
+
const enqueue = (event, data, userData) => {
|
|
3439
|
+
const task = () => send(event, data, userData).catch((e) => {
|
|
3440
|
+
if (process.env.NODE_ENV !== "production") console.warn(`[appfunnel] track failed (${event}):`, e);
|
|
3441
|
+
});
|
|
3442
|
+
if (!sessionId) {
|
|
3443
|
+
chain = chain.then(task);
|
|
3444
|
+
} else {
|
|
3445
|
+
void task();
|
|
3446
|
+
}
|
|
3447
|
+
};
|
|
3448
|
+
const VARS_DEBOUNCE_MS = 800;
|
|
3449
|
+
const dataUrl = `${base}/campaign/${encodeURIComponent(cfg.campaignId)}/event/session/data`;
|
|
3450
|
+
let pendingVars = null;
|
|
3451
|
+
let varsTimer = null;
|
|
3452
|
+
const persistence = createSnapshotPersistence({
|
|
3453
|
+
url: dataUrl,
|
|
3454
|
+
getSessionId: () => sessionId
|
|
3455
|
+
});
|
|
3456
|
+
const flushVars = () => {
|
|
3457
|
+
if (varsTimer) {
|
|
3458
|
+
clearTimeout(varsTimer);
|
|
3459
|
+
varsTimer = null;
|
|
3460
|
+
}
|
|
3461
|
+
if (!pendingVars) return;
|
|
3462
|
+
const snapshot = pendingVars;
|
|
3463
|
+
pendingVars = null;
|
|
3464
|
+
persistence.push(snapshot);
|
|
3465
|
+
if (!sessionId) chain = chain.then(() => {
|
|
3466
|
+
persistence.push(snapshot);
|
|
3467
|
+
});
|
|
3468
|
+
};
|
|
3469
|
+
const drainOnExit = () => {
|
|
3470
|
+
if (pendingVars) {
|
|
3471
|
+
flushVars();
|
|
3472
|
+
persistence.flushBeacon();
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
try {
|
|
3476
|
+
window.addEventListener("visibilitychange", drainOnExit);
|
|
3477
|
+
window.addEventListener("pagehide", drainOnExit);
|
|
3478
|
+
} catch {
|
|
3479
|
+
}
|
|
3480
|
+
if (cfg.experiment?.id && cfg.experiment.variant) {
|
|
3481
|
+
enqueue("experiment.exposure", exposurePayload(cfg.experiment.id, cfg.experiment.variant, cfg.experiment.version));
|
|
3482
|
+
}
|
|
3483
|
+
return {
|
|
3484
|
+
track: (event, data, userData) => enqueue(event, data, userData),
|
|
3485
|
+
identify: (email, eventId) => enqueue("user.registered", { email, ...eventId ? { eventId } : {} }),
|
|
3486
|
+
setVariables: (variables) => {
|
|
3487
|
+
pendingVars = variables;
|
|
3488
|
+
if (varsTimer) clearTimeout(varsTimer);
|
|
3489
|
+
varsTimer = setTimeout(flushVars, VARS_DEBOUNCE_MS);
|
|
3490
|
+
},
|
|
3491
|
+
setAcquisition: (acq) => {
|
|
3492
|
+
acquisition = acq;
|
|
3493
|
+
}
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// src/flow/mount.tsx
|
|
3498
|
+
var AssetsContext = createContext({});
|
|
3499
|
+
function AssetsProvider({
|
|
3500
|
+
assets,
|
|
3501
|
+
children
|
|
3502
|
+
}) {
|
|
3503
|
+
return createElement(AssetsContext.Provider, { value: assets ?? {} }, children);
|
|
3504
|
+
}
|
|
3505
|
+
function useAssets() {
|
|
3506
|
+
return useContext(AssetsContext);
|
|
3507
|
+
}
|
|
3508
|
+
function useAsset(name) {
|
|
3509
|
+
return useContext(AssetsContext)[name];
|
|
3510
|
+
}
|
|
3511
|
+
function createFunnelTree({
|
|
3512
|
+
config,
|
|
3513
|
+
pages,
|
|
3514
|
+
layout,
|
|
3515
|
+
checkoutDriver,
|
|
3516
|
+
messages
|
|
3517
|
+
}) {
|
|
3518
|
+
return function tree(opts) {
|
|
3519
|
+
const mode = opts.mode ?? "live";
|
|
3520
|
+
const embedEnabled = opts.embed ?? false;
|
|
3521
|
+
const tracker = opts.tracking ? createPlatformTracker({
|
|
3522
|
+
...opts.tracking,
|
|
3523
|
+
visitorId: opts.visitorId,
|
|
3524
|
+
sessionId: opts.sessionId,
|
|
3525
|
+
mode
|
|
3526
|
+
}) : void 0;
|
|
3527
|
+
const checkout = checkoutDriver && opts.tracking ? checkoutDriver({
|
|
3528
|
+
apiBase: opts.tracking.apiBase,
|
|
3529
|
+
campaignId: opts.tracking.campaignId,
|
|
3530
|
+
funnelId: opts.tracking.funnelId,
|
|
3531
|
+
mode,
|
|
3532
|
+
sessionId: opts.sessionId,
|
|
3533
|
+
visitorId: opts.visitorId
|
|
3534
|
+
}) : void 0;
|
|
3535
|
+
const layoutWithResume = function LayoutWithCheckoutResume({
|
|
3536
|
+
children
|
|
3537
|
+
}) {
|
|
3538
|
+
return createElement(
|
|
3539
|
+
Fragment,
|
|
3540
|
+
null,
|
|
3541
|
+
createElement(CheckoutResume, null),
|
|
3542
|
+
embedEnabled ? createElement(EmbedBridge, null) : null,
|
|
3543
|
+
layout ? createElement(layout, null, children) : children
|
|
3544
|
+
);
|
|
3545
|
+
};
|
|
3546
|
+
return createElement(AssetsProvider, {
|
|
3547
|
+
assets: opts.assets,
|
|
3548
|
+
children: createElement(FunnelProvider, {
|
|
3549
|
+
config,
|
|
3550
|
+
offerings: opts.offerings ?? [],
|
|
3551
|
+
messages: opts.messages ?? messages,
|
|
3552
|
+
locale: opts.locale,
|
|
3553
|
+
tracker,
|
|
3554
|
+
checkout,
|
|
3555
|
+
sessionValues: opts.sessionValues,
|
|
3556
|
+
deferSessionValues: opts.deferSessionValues,
|
|
3557
|
+
experiments: opts.experiments,
|
|
3558
|
+
context: {
|
|
3559
|
+
mode,
|
|
3560
|
+
visitorId: opts.visitorId ?? null,
|
|
3561
|
+
customerId: opts.customerId ?? null,
|
|
3562
|
+
sessionId: opts.sessionId ?? null
|
|
3563
|
+
},
|
|
3564
|
+
children: createElement(FunnelView, {
|
|
3565
|
+
pages,
|
|
3566
|
+
layout: layoutWithResume,
|
|
3567
|
+
initialKey: opts.initialKey,
|
|
3568
|
+
// FLASH-FREE HYDRATION (Option B, FIX 1): trust the SERVER-resolved initialKey instead of
|
|
3569
|
+
// re-running the entry guard. The CLIENT sets this via deferSessionValues (its deferred
|
|
3570
|
+
// snapshot is empty, so re-running the guard would bounce a qualified deep-link and
|
|
3571
|
+
// mismatch the markup). The build-time PRERENDER sets `trustInitialKey` explicitly so a
|
|
3572
|
+
// guarded page prerenders its OWN default-state markup (the renderer picks target-vs-bounce
|
|
3573
|
+
// at serve time). Live SSR fallback leaves both off → the guard runs against real values.
|
|
3574
|
+
trustInitialKey: opts.trustInitialKey ?? opts.deferSessionValues,
|
|
3575
|
+
prefetch: "off"
|
|
3576
|
+
// renderer background-modulepreloads all chunks
|
|
3577
|
+
})
|
|
3578
|
+
})
|
|
3579
|
+
});
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
1796
3582
|
function Choice({
|
|
1797
3583
|
bind,
|
|
1798
3584
|
options,
|
|
@@ -1932,95 +3718,6 @@ function Script({
|
|
|
1932
3718
|
return null;
|
|
1933
3719
|
}
|
|
1934
3720
|
|
|
1935
|
-
|
|
1936
|
-
function isVariantNode(page) {
|
|
1937
|
-
return isVariantKey(page.key);
|
|
1938
|
-
}
|
|
1939
|
-
function serializeGate(when) {
|
|
1940
|
-
if (!when) return void 0;
|
|
1941
|
-
if (typeof when === "function") return { kind: "predicate" };
|
|
1942
|
-
return { kind: "declarative", condition: when };
|
|
1943
|
-
}
|
|
1944
|
-
function compileManifest(input) {
|
|
1945
|
-
const { funnel, pages } = input;
|
|
1946
|
-
const keys = new Set(pages.map((p) => p.key));
|
|
1947
|
-
const manifestPages = pages.map((p, index) => ({
|
|
1948
|
-
id: p.key,
|
|
1949
|
-
key: p.key,
|
|
1950
|
-
type: p.meta?.type ?? "default",
|
|
1951
|
-
slug: p.meta?.slug ?? p.key,
|
|
1952
|
-
index,
|
|
1953
|
-
variantOf: isVariantKey(p.key) ? parseSlotKey(p.key).slot : void 0
|
|
1954
|
-
}));
|
|
1955
|
-
const edges = [];
|
|
1956
|
-
const badTargets = [];
|
|
1957
|
-
const nextAnchor = (from) => {
|
|
1958
|
-
for (let j = from + 1; j < pages.length; j++) {
|
|
1959
|
-
if (!isVariantNode(pages[j])) return pages[j];
|
|
1960
|
-
}
|
|
1961
|
-
return void 0;
|
|
1962
|
-
};
|
|
1963
|
-
pages.forEach((p, i) => {
|
|
1964
|
-
if (isVariantNode(p)) return;
|
|
1965
|
-
const routes = p.meta?.next ?? [];
|
|
1966
|
-
let hasUnconditional = false;
|
|
1967
|
-
for (const route of routes) {
|
|
1968
|
-
if (!keys.has(route.to)) {
|
|
1969
|
-
badTargets.push({ from: p.key, to: route.to });
|
|
1970
|
-
continue;
|
|
1971
|
-
}
|
|
1972
|
-
edges.push({ from: p.key, to: route.to, kind: "branch", condition: serializeGate(route.when) });
|
|
1973
|
-
if (!route.when) hasUnconditional = true;
|
|
1974
|
-
}
|
|
1975
|
-
const next = nextAnchor(i);
|
|
1976
|
-
if (!hasUnconditional && next && p.meta?.type !== "finish") {
|
|
1977
|
-
edges.push({ from: p.key, to: next.key, kind: "linear" });
|
|
1978
|
-
}
|
|
1979
|
-
});
|
|
1980
|
-
const startPage = pages.find((p) => !isVariantNode(p));
|
|
1981
|
-
const start = startPage?.key ?? null;
|
|
1982
|
-
const variantKeys = new Set(pages.filter(isVariantNode).map((p) => p.key));
|
|
1983
|
-
const validation = validate(manifestPages, edges, badTargets, start, variantKeys);
|
|
1984
|
-
return {
|
|
1985
|
-
id: funnel.id,
|
|
1986
|
-
pages: manifestPages,
|
|
1987
|
-
flow: { start, nodes: manifestPages.map((p) => p.id), edges },
|
|
1988
|
-
products: funnel.products ?? [],
|
|
1989
|
-
locales: funnel.locales,
|
|
1990
|
-
validation
|
|
1991
|
-
};
|
|
1992
|
-
}
|
|
1993
|
-
function validate(pages, edges, badTargets, start, variantKeys) {
|
|
1994
|
-
const outgoing = /* @__PURE__ */ new Map();
|
|
1995
|
-
for (const e of edges) outgoing.set(e.from, (outgoing.get(e.from) ?? 0) + 1);
|
|
1996
|
-
const deadEnds = pages.filter((p) => p.type !== "finish" && !variantKeys.has(p.id) && !(outgoing.get(p.id) > 0)).map((p) => p.id);
|
|
1997
|
-
const reachable = /* @__PURE__ */ new Set();
|
|
1998
|
-
if (start) {
|
|
1999
|
-
const adj = /* @__PURE__ */ new Map();
|
|
2000
|
-
for (const e of edges) {
|
|
2001
|
-
const list = adj.get(e.from);
|
|
2002
|
-
if (list) list.push(e.to);
|
|
2003
|
-
else adj.set(e.from, [e.to]);
|
|
2004
|
-
}
|
|
2005
|
-
const stack = [start];
|
|
2006
|
-
while (stack.length) {
|
|
2007
|
-
const id = stack.pop();
|
|
2008
|
-
if (reachable.has(id)) continue;
|
|
2009
|
-
reachable.add(id);
|
|
2010
|
-
for (const to of adj.get(id) ?? []) stack.push(to);
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
const unreachable = pages.filter((p) => !variantKeys.has(p.id) && !reachable.has(p.id)).map((p) => p.id);
|
|
2014
|
-
const missingEmailCapture = !pages.some((p) => p.type === "email-capture");
|
|
2015
|
-
return {
|
|
2016
|
-
ok: deadEnds.length === 0 && unreachable.length === 0 && badTargets.length === 0 && !missingEmailCapture,
|
|
2017
|
-
deadEnds,
|
|
2018
|
-
unreachable,
|
|
2019
|
-
badTargets,
|
|
2020
|
-
missingEmailCapture
|
|
2021
|
-
};
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
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 };
|
|
3721
|
+
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, useGroupProgress, useLocale, useModal, useNavigation, useOffering, useOfferings, usePage, useResponse, useSystem, useTracker, useTranslation, useUserAttribute, useUtm, withBus };
|
|
2025
3722
|
//# sourceMappingURL=index.js.map
|
|
2026
3723
|
//# sourceMappingURL=index.js.map
|