@sonordev/site-kit 2.9.0 → 3.0.0
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/dist/EngageWidget-LRE4VD7O.js +14 -0
- package/dist/{EngageWidget-JIXUBG47.js.map → EngageWidget-LRE4VD7O.js.map} +1 -1
- package/dist/EngageWidget-OAOLB6ED.mjs +5 -0
- package/dist/{EngageWidget-4W5EM4UO.mjs.map → EngageWidget-OAOLB6ED.mjs.map} +1 -1
- package/dist/chunk-2FEQ6TYD.mjs +37 -0
- package/dist/chunk-2FEQ6TYD.mjs.map +1 -0
- package/dist/{chunk-5QLM6LZ2.mjs → chunk-APRAGA5G.mjs} +3 -3
- package/dist/{chunk-5QLM6LZ2.mjs.map → chunk-APRAGA5G.mjs.map} +1 -1
- package/dist/{chunk-HANRPNKG.js → chunk-FMI65E5Y.js} +3 -3
- package/dist/{chunk-HANRPNKG.js.map → chunk-FMI65E5Y.js.map} +1 -1
- package/dist/{chunk-SLJJJWMV.js → chunk-FPU2QPO4.js} +338 -33
- package/dist/chunk-FPU2QPO4.js.map +1 -0
- package/dist/chunk-MLJXELPA.js +693 -0
- package/dist/chunk-MLJXELPA.js.map +1 -0
- package/dist/{chunk-IAV32YHL.mjs → chunk-RV45YMBJ.mjs} +338 -33
- package/dist/chunk-RV45YMBJ.mjs.map +1 -0
- package/dist/chunk-TCVHI3JP.mjs +686 -0
- package/dist/chunk-TCVHI3JP.mjs.map +1 -0
- package/dist/chunk-YBKALLBH.js +46 -0
- package/dist/chunk-YBKALLBH.js.map +1 -0
- package/dist/engage/index.d.mts +2 -2
- package/dist/engage/index.d.ts +2 -2
- package/dist/engage/index.js +5 -4
- package/dist/engage/index.js.map +1 -1
- package/dist/engage/index.mjs +2 -1
- package/dist/engage/index.mjs.map +1 -1
- package/dist/forms/index.js +19 -683
- package/dist/forms/index.js.map +1 -1
- package/dist/forms/index.mjs +4 -676
- package/dist/forms/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/layout/client.js +2 -2
- package/dist/layout/client.mjs +1 -1
- package/dist/layout/index.js +2 -2
- package/dist/layout/index.mjs +1 -1
- package/dist/middleware/index.d.mts +62 -1
- package/dist/middleware/index.d.ts +62 -1
- package/dist/middleware/index.js +105 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +103 -1
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/slots/contract.d.mts +83 -0
- package/dist/slots/contract.d.ts +83 -0
- package/dist/slots/contract.js +41 -0
- package/dist/slots/contract.js.map +1 -0
- package/dist/slots/contract.mjs +4 -0
- package/dist/slots/contract.mjs.map +1 -0
- package/dist/slots/index.d.mts +104 -0
- package/dist/slots/index.d.ts +104 -0
- package/dist/slots/index.js +186 -0
- package/dist/slots/index.js.map +1 -0
- package/dist/slots/index.mjs +146 -0
- package/dist/slots/index.mjs.map +1 -0
- package/dist/{types-BnCwwUX3.d.ts → types-D_1iUxOQ.d.mts} +18 -1
- package/dist/{types-BnCwwUX3.d.mts → types-D_1iUxOQ.d.ts} +18 -1
- package/package.json +11 -1
- package/dist/EngageWidget-4W5EM4UO.mjs +0 -4
- package/dist/EngageWidget-JIXUBG47.js +0 -13
- package/dist/chunk-IAV32YHL.mjs.map +0 -1
- package/dist/chunk-SLJJJWMV.js.map +0 -1
package/dist/forms/index.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
import { DEFAULT_HONEYPOT_FIELD, useFormTracking, submitForm, DEFAULT_FORM_SUCCESS_MESSAGE, useForm, fetchWithRetry } from '../chunk-TCVHI3JP.mjs';
|
|
3
|
+
export { DEFAULT_FORM_SUCCESS_MESSAGE, useForm, useFormTracking } from '../chunk-TCVHI3JP.mjs';
|
|
2
4
|
import '../chunk-4XPGGLVP.mjs';
|
|
3
|
-
import React2, { useRef, useEffect, useCallback,
|
|
5
|
+
import React2, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
4
6
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
5
7
|
|
|
6
8
|
// src/forms/formsApi.ts
|
|
@@ -320,680 +322,6 @@ async function initializeForms(forms) {
|
|
|
320
322
|
console.error("[Site-Kit] Failed to sync forms:", error);
|
|
321
323
|
}
|
|
322
324
|
}
|
|
323
|
-
function useFormTracking({
|
|
324
|
-
formId,
|
|
325
|
-
totalSteps,
|
|
326
|
-
enabled = true,
|
|
327
|
-
debug = false
|
|
328
|
-
}) {
|
|
329
|
-
const sessionIdRef = useRef("");
|
|
330
|
-
const startTimeRef = useRef(0);
|
|
331
|
-
const stepStartTimeRef = useRef(0);
|
|
332
|
-
const stepTimesRef = useRef({});
|
|
333
|
-
const currentStepRef = useRef(1);
|
|
334
|
-
const maxStepRef = useRef(1);
|
|
335
|
-
const analyticsIdRef = useRef(null);
|
|
336
|
-
useEffect(() => {
|
|
337
|
-
if (!enabled) return;
|
|
338
|
-
if (!formId) {
|
|
339
|
-
if (debug) console.warn("[Forms] Tracking skipped: formId is empty");
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
sessionIdRef.current = crypto.randomUUID();
|
|
343
|
-
startTimeRef.current = Date.now();
|
|
344
|
-
stepStartTimeRef.current = Date.now();
|
|
345
|
-
const startTracking = async () => {
|
|
346
|
-
const apiUrl = window.__SITE_KIT_API_URL__;
|
|
347
|
-
const apiKey = window.__SITE_KIT_API_KEY__;
|
|
348
|
-
if (!apiUrl || !apiKey) {
|
|
349
|
-
if (debug) console.error("[Forms] Missing API URL or API key");
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
try {
|
|
353
|
-
const response = await fetch(`${apiUrl}/api/public/forms/analytics/start`, {
|
|
354
|
-
method: "POST",
|
|
355
|
-
headers: {
|
|
356
|
-
"Content-Type": "application/json",
|
|
357
|
-
"x-api-key": apiKey
|
|
358
|
-
},
|
|
359
|
-
body: JSON.stringify({
|
|
360
|
-
formId,
|
|
361
|
-
sessionId: sessionIdRef.current,
|
|
362
|
-
deviceType: getDeviceType()
|
|
363
|
-
})
|
|
364
|
-
});
|
|
365
|
-
if (!response.ok) {
|
|
366
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
367
|
-
}
|
|
368
|
-
const data = await response.json();
|
|
369
|
-
analyticsIdRef.current = data.id;
|
|
370
|
-
if (debug) console.log("[Forms] Started tracking:", data.id);
|
|
371
|
-
} catch (error) {
|
|
372
|
-
if (debug) console.error("[Forms] Error starting tracking:", error);
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
startTracking();
|
|
376
|
-
const handleBeforeUnload = () => {
|
|
377
|
-
if (!analyticsIdRef.current) return;
|
|
378
|
-
const apiUrl = window.__SITE_KIT_API_URL__;
|
|
379
|
-
const apiKey = window.__SITE_KIT_API_KEY__;
|
|
380
|
-
if (!apiUrl || !apiKey) return;
|
|
381
|
-
const now = Date.now();
|
|
382
|
-
const currentStepTime = Math.floor((now - stepStartTimeRef.current) / 1e3);
|
|
383
|
-
stepTimesRef.current[currentStepRef.current] = currentStepTime;
|
|
384
|
-
const payload = JSON.stringify({
|
|
385
|
-
analyticsId: analyticsIdRef.current,
|
|
386
|
-
step: currentStepRef.current,
|
|
387
|
-
maxStep: maxStepRef.current,
|
|
388
|
-
stepTimes: stepTimesRef.current,
|
|
389
|
-
totalTimeSeconds: Math.floor((now - startTimeRef.current) / 1e3)
|
|
390
|
-
});
|
|
391
|
-
const blob = new Blob([payload], { type: "application/json" });
|
|
392
|
-
new Headers({
|
|
393
|
-
"Content-Type": "application/json",
|
|
394
|
-
"x-api-key": apiKey
|
|
395
|
-
});
|
|
396
|
-
const sent = navigator.sendBeacon(
|
|
397
|
-
`${apiUrl}/api/public/forms/analytics/abandon`,
|
|
398
|
-
blob
|
|
399
|
-
);
|
|
400
|
-
if (debug) console.log("[Forms] Abandonment tracked:", sent ? "success" : "failed");
|
|
401
|
-
};
|
|
402
|
-
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
403
|
-
return () => {
|
|
404
|
-
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
405
|
-
};
|
|
406
|
-
}, [formId, enabled, debug]);
|
|
407
|
-
const trackStepChange = useCallback(async (step) => {
|
|
408
|
-
if (!enabled || !analyticsIdRef.current) return;
|
|
409
|
-
const apiUrl = window.__SITE_KIT_API_URL__;
|
|
410
|
-
const apiKey = window.__SITE_KIT_API_KEY__;
|
|
411
|
-
if (!apiUrl || !apiKey) {
|
|
412
|
-
if (debug) console.error("[Forms] Missing API URL or API key");
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
const now = Date.now();
|
|
416
|
-
const prevStepTime = Math.floor((now - stepStartTimeRef.current) / 1e3);
|
|
417
|
-
stepTimesRef.current[currentStepRef.current] = prevStepTime;
|
|
418
|
-
currentStepRef.current = step;
|
|
419
|
-
maxStepRef.current = Math.max(maxStepRef.current, step);
|
|
420
|
-
stepStartTimeRef.current = now;
|
|
421
|
-
try {
|
|
422
|
-
const response = await fetch(`${apiUrl}/api/public/forms/analytics/step`, {
|
|
423
|
-
method: "POST",
|
|
424
|
-
headers: {
|
|
425
|
-
"Content-Type": "application/json",
|
|
426
|
-
"x-api-key": apiKey
|
|
427
|
-
},
|
|
428
|
-
body: JSON.stringify({
|
|
429
|
-
analyticsId: analyticsIdRef.current,
|
|
430
|
-
step,
|
|
431
|
-
maxStep: maxStepRef.current,
|
|
432
|
-
stepTimes: stepTimesRef.current
|
|
433
|
-
})
|
|
434
|
-
});
|
|
435
|
-
if (!response.ok) {
|
|
436
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
437
|
-
}
|
|
438
|
-
if (debug) console.log("[Forms] Step changed to:", step);
|
|
439
|
-
} catch (error) {
|
|
440
|
-
if (debug) console.error("[Forms] Error updating step:", error);
|
|
441
|
-
}
|
|
442
|
-
}, [enabled, debug]);
|
|
443
|
-
const trackComplete = useCallback(async () => {
|
|
444
|
-
if (!enabled || !analyticsIdRef.current) return;
|
|
445
|
-
const apiUrl = window.__SITE_KIT_API_URL__;
|
|
446
|
-
const apiKey = window.__SITE_KIT_API_KEY__;
|
|
447
|
-
if (!apiUrl || !apiKey) {
|
|
448
|
-
if (debug) console.error("[Forms] Missing API URL or API key");
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
const now = Date.now();
|
|
452
|
-
const finalStepTime = Math.floor((now - stepStartTimeRef.current) / 1e3);
|
|
453
|
-
stepTimesRef.current[currentStepRef.current] = finalStepTime;
|
|
454
|
-
try {
|
|
455
|
-
const response = await fetch(`${apiUrl}/api/public/forms/analytics/complete`, {
|
|
456
|
-
method: "POST",
|
|
457
|
-
headers: {
|
|
458
|
-
"Content-Type": "application/json",
|
|
459
|
-
"x-api-key": apiKey
|
|
460
|
-
},
|
|
461
|
-
body: JSON.stringify({
|
|
462
|
-
analyticsId: analyticsIdRef.current,
|
|
463
|
-
totalSteps,
|
|
464
|
-
stepTimes: stepTimesRef.current,
|
|
465
|
-
totalTimeSeconds: Math.floor((now - startTimeRef.current) / 1e3)
|
|
466
|
-
})
|
|
467
|
-
});
|
|
468
|
-
if (!response.ok) {
|
|
469
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
470
|
-
}
|
|
471
|
-
if (debug) console.log("[Forms] Form completed, recording conversion");
|
|
472
|
-
try {
|
|
473
|
-
await fetch(`${apiUrl}/api/public/analytics/conversion`, {
|
|
474
|
-
method: "POST",
|
|
475
|
-
headers: {
|
|
476
|
-
"Content-Type": "application/json",
|
|
477
|
-
"x-api-key": apiKey
|
|
478
|
-
},
|
|
479
|
-
body: JSON.stringify({
|
|
480
|
-
sessionId: sessionIdRef.current,
|
|
481
|
-
conversionType: "form_submission",
|
|
482
|
-
metadata: { formId, analyticsId: analyticsIdRef.current },
|
|
483
|
-
pagePath: typeof window !== "undefined" ? window.location.pathname : "/",
|
|
484
|
-
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
485
|
-
deviceType: getDeviceType()
|
|
486
|
-
})
|
|
487
|
-
});
|
|
488
|
-
} catch (convErr) {
|
|
489
|
-
if (debug) console.error("[Forms] Error recording conversion:", convErr);
|
|
490
|
-
}
|
|
491
|
-
} catch (error) {
|
|
492
|
-
if (debug) console.error("[Forms] Error completing tracking:", error);
|
|
493
|
-
}
|
|
494
|
-
}, [enabled, totalSteps, formId, debug]);
|
|
495
|
-
return {
|
|
496
|
-
trackStepChange,
|
|
497
|
-
trackComplete,
|
|
498
|
-
sessionId: sessionIdRef.current
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
function getDeviceType() {
|
|
502
|
-
if (typeof window === "undefined") return "desktop";
|
|
503
|
-
const ua = navigator.userAgent;
|
|
504
|
-
if (/tablet|ipad|playbook|silk/i.test(ua)) return "tablet";
|
|
505
|
-
if (/mobile|iphone|ipod|android|blackberry|opera mini|iemobile/i.test(ua)) return "mobile";
|
|
506
|
-
return "desktop";
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// src/forms/fetchWithRetry.ts
|
|
510
|
-
function backoffDelay(attempt, response) {
|
|
511
|
-
if (response?.status === 429) {
|
|
512
|
-
const header = response.headers.get("retry-after");
|
|
513
|
-
const seconds = header ? parseInt(header, 10) : NaN;
|
|
514
|
-
if (!Number.isNaN(seconds) && seconds > 0) return Math.min(seconds * 1e3, 1e4);
|
|
515
|
-
}
|
|
516
|
-
return Math.min(8e3, 500 * 2 ** attempt);
|
|
517
|
-
}
|
|
518
|
-
async function fetchWithRetry(url, init, options = {}) {
|
|
519
|
-
const retries = options.retries ?? 2;
|
|
520
|
-
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
521
|
-
let lastError;
|
|
522
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
523
|
-
const controller = new AbortController();
|
|
524
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
525
|
-
try {
|
|
526
|
-
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
527
|
-
clearTimeout(timer);
|
|
528
|
-
if ((response.status === 429 || response.status >= 500) && attempt < retries) {
|
|
529
|
-
await new Promise((resolve) => setTimeout(resolve, backoffDelay(attempt, response)));
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
return response;
|
|
533
|
-
} catch (error) {
|
|
534
|
-
clearTimeout(timer);
|
|
535
|
-
lastError = error;
|
|
536
|
-
if (attempt < retries) {
|
|
537
|
-
await new Promise((resolve) => setTimeout(resolve, backoffDelay(attempt)));
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
throw lastError instanceof Error ? lastError : new Error("Network request failed after multiple attempts. Please try again.");
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// src/forms/recaptcha.ts
|
|
546
|
-
function getSiteKey(explicitSiteKey) {
|
|
547
|
-
if (typeof window === "undefined") return void 0;
|
|
548
|
-
if (explicitSiteKey) return explicitSiteKey;
|
|
549
|
-
const win = window;
|
|
550
|
-
return win.__SITE_KIT_RECAPTCHA_SITE_KEY__ ?? process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
|
|
551
|
-
}
|
|
552
|
-
var RECAPTCHA_LOAD_TIMEOUT_MS = 1e4;
|
|
553
|
-
function loadScript(siteKey) {
|
|
554
|
-
return new Promise((resolve) => {
|
|
555
|
-
if (typeof window === "undefined") {
|
|
556
|
-
resolve(null);
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
const win = window;
|
|
560
|
-
if (win.grecaptcha?.enterprise) {
|
|
561
|
-
resolve(win.grecaptcha);
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
let settled = false;
|
|
565
|
-
const finish = (value) => {
|
|
566
|
-
if (settled) return;
|
|
567
|
-
settled = true;
|
|
568
|
-
resolve(value);
|
|
569
|
-
};
|
|
570
|
-
const giveUp = setTimeout(() => finish(null), RECAPTCHA_LOAD_TIMEOUT_MS);
|
|
571
|
-
const waitForReady = () => {
|
|
572
|
-
if (settled) return;
|
|
573
|
-
if (win.grecaptcha?.enterprise) {
|
|
574
|
-
clearTimeout(giveUp);
|
|
575
|
-
finish(win.grecaptcha);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
setTimeout(waitForReady, 50);
|
|
579
|
-
};
|
|
580
|
-
const existing = document.querySelector('script[src*="recaptcha/enterprise"]');
|
|
581
|
-
if (existing) {
|
|
582
|
-
waitForReady();
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
const script = document.createElement("script");
|
|
586
|
-
script.src = `https://www.google.com/recaptcha/enterprise.js?render=${siteKey}`;
|
|
587
|
-
script.async = true;
|
|
588
|
-
script.onload = () => waitForReady();
|
|
589
|
-
script.onerror = () => {
|
|
590
|
-
clearTimeout(giveUp);
|
|
591
|
-
finish(null);
|
|
592
|
-
};
|
|
593
|
-
document.head.appendChild(script);
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
async function getRecaptchaToken(explicitSiteKey) {
|
|
597
|
-
const siteKey = getSiteKey(explicitSiteKey);
|
|
598
|
-
if (!siteKey || typeof window === "undefined") return null;
|
|
599
|
-
try {
|
|
600
|
-
const grecaptcha = await loadScript(siteKey);
|
|
601
|
-
if (!grecaptcha?.enterprise?.execute) return null;
|
|
602
|
-
const token = await grecaptcha.enterprise.execute(siteKey, { action: "submit" });
|
|
603
|
-
return token || null;
|
|
604
|
-
} catch (err) {
|
|
605
|
-
console.warn("[site-kit reCAPTCHA]", err);
|
|
606
|
-
return null;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// src/forms/submitForm.ts
|
|
611
|
-
var DEFAULT_HONEYPOT_FIELD = "_sk_hp_url";
|
|
612
|
-
function getApiConfig() {
|
|
613
|
-
const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.sonor.io" : "https://api.sonor.io";
|
|
614
|
-
const apiKey = typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ : void 0;
|
|
615
|
-
return { apiUrl, apiKey };
|
|
616
|
-
}
|
|
617
|
-
function getUTMParams() {
|
|
618
|
-
if (typeof window === "undefined") return {};
|
|
619
|
-
const params = new URLSearchParams(window.location.search);
|
|
620
|
-
const out = {};
|
|
621
|
-
for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"]) {
|
|
622
|
-
out[key] = params.get(key);
|
|
623
|
-
}
|
|
624
|
-
return out;
|
|
625
|
-
}
|
|
626
|
-
async function submitForm({
|
|
627
|
-
config: config2,
|
|
628
|
-
values,
|
|
629
|
-
honeypotValue,
|
|
630
|
-
formLoadedAt
|
|
631
|
-
}) {
|
|
632
|
-
const { apiUrl, apiKey } = getApiConfig();
|
|
633
|
-
if (!apiKey) {
|
|
634
|
-
throw new Error("API key is required. Set SONOR_API_KEY in your environment.");
|
|
635
|
-
}
|
|
636
|
-
const recaptchaToken = config2.recaptcha_enabled ? await getRecaptchaToken(config2.recaptcha_site_key) : null;
|
|
637
|
-
if (config2.recaptcha_enabled && !recaptchaToken) {
|
|
638
|
-
throw new Error("Verification could not be completed. Please try again.");
|
|
639
|
-
}
|
|
640
|
-
const honeypotFieldName = config2.honeypot_field || DEFAULT_HONEYPOT_FIELD;
|
|
641
|
-
const utm = getUTMParams();
|
|
642
|
-
const payload = {
|
|
643
|
-
formId: config2.id,
|
|
644
|
-
form_id: config2.id,
|
|
645
|
-
data: {
|
|
646
|
-
...values,
|
|
647
|
-
...config2.honeypot_enabled ? { [honeypotFieldName]: honeypotValue } : {}
|
|
648
|
-
},
|
|
649
|
-
metadata: {
|
|
650
|
-
pageUrl: typeof window !== "undefined" ? window.location.href : null,
|
|
651
|
-
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
652
|
-
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
|
653
|
-
sessionId: typeof sessionStorage !== "undefined" ? sessionStorage.getItem("_sk_sid") : null,
|
|
654
|
-
recaptchaToken: recaptchaToken || void 0,
|
|
655
|
-
utmSource: utm.utm_source,
|
|
656
|
-
utmMedium: utm.utm_medium,
|
|
657
|
-
utmCampaign: utm.utm_campaign,
|
|
658
|
-
// Sub-site host — preserved on form_submissions for lead attribution
|
|
659
|
-
// across multi-site projects. Resolved by SiteKitClientProviders.
|
|
660
|
-
site: typeof window !== "undefined" ? window.__SITE_KIT_SITE__ || window.location.host : void 0,
|
|
661
|
-
_formLoadedAt: formLoadedAt
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
const response = await fetchWithRetry(
|
|
665
|
-
`${apiUrl}/api/public/forms/submit`,
|
|
666
|
-
{
|
|
667
|
-
method: "POST",
|
|
668
|
-
headers: {
|
|
669
|
-
"Content-Type": "application/json",
|
|
670
|
-
Authorization: `Bearer ${apiKey}`
|
|
671
|
-
},
|
|
672
|
-
body: JSON.stringify(payload)
|
|
673
|
-
},
|
|
674
|
-
{ retries: 2, timeoutMs: 2e4 }
|
|
675
|
-
);
|
|
676
|
-
const data = await response.json().catch(() => null);
|
|
677
|
-
if (!response.ok) {
|
|
678
|
-
const msg = (data?.message ?? data?.error ?? (response.statusText || `HTTP ${response.status}`)).toString().trim();
|
|
679
|
-
throw new Error(
|
|
680
|
-
msg ? `Failed to submit form: ${msg}` : `Failed to submit form (${response.status})`
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
return data;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// src/forms/defaultSuccessMessage.ts
|
|
687
|
-
var DEFAULT_FORM_SUCCESS_MESSAGE = "Thank you for your submission! We will be in touch soon.";
|
|
688
|
-
|
|
689
|
-
// src/forms/useForm.ts
|
|
690
|
-
function useForm(formIdOrSlug, options = {}) {
|
|
691
|
-
const {
|
|
692
|
-
projectId: optionsProjectId,
|
|
693
|
-
onSuccess,
|
|
694
|
-
onError,
|
|
695
|
-
initialValues = {},
|
|
696
|
-
redirectUrl
|
|
697
|
-
} = options;
|
|
698
|
-
const projectId = optionsProjectId || (typeof window !== "undefined" ? window.__SITE_KIT_PROJECT_ID__ : void 0);
|
|
699
|
-
const [form, setForm] = useState(null);
|
|
700
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
701
|
-
const [fetchError, setFetchError] = useState(null);
|
|
702
|
-
const [values, setValuesState] = useState(initialValues);
|
|
703
|
-
const [errors, setErrors] = useState({});
|
|
704
|
-
const [step, setStep] = useState(1);
|
|
705
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
706
|
-
const [isComplete, setIsComplete] = useState(false);
|
|
707
|
-
const [submitError, setSubmitError] = useState(null);
|
|
708
|
-
const isSubmittingRef = useRef(false);
|
|
709
|
-
const formLoadedAt = useRef(Date.now());
|
|
710
|
-
const totalSteps = form?.total_steps || 1;
|
|
711
|
-
const isMultiStep = form?.is_multi_step || false;
|
|
712
|
-
const { trackStepChange, trackComplete } = useFormTracking({
|
|
713
|
-
formId: form?.id || "",
|
|
714
|
-
totalSteps
|
|
715
|
-
});
|
|
716
|
-
useEffect(() => {
|
|
717
|
-
const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.sonor.io" : "https://api.sonor.io";
|
|
718
|
-
async function fetchForm() {
|
|
719
|
-
try {
|
|
720
|
-
setIsLoading(true);
|
|
721
|
-
setFetchError(null);
|
|
722
|
-
const readApiKey = () => typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ : void 0;
|
|
723
|
-
let apiKey = readApiKey();
|
|
724
|
-
for (let i = 0; i < 15 && !apiKey; i++) {
|
|
725
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
726
|
-
apiKey = readApiKey();
|
|
727
|
-
}
|
|
728
|
-
if (!apiKey) {
|
|
729
|
-
throw new Error("API key is required. Provide it via SiteKitLayout/SiteKitProvider or set SONOR_API_KEY.");
|
|
730
|
-
}
|
|
731
|
-
const url = `${apiUrl}/api/public/forms/config`;
|
|
732
|
-
const init = {
|
|
733
|
-
method: "POST",
|
|
734
|
-
headers: {
|
|
735
|
-
"Content-Type": "application/json",
|
|
736
|
-
Authorization: `Bearer ${apiKey}`
|
|
737
|
-
},
|
|
738
|
-
body: JSON.stringify({
|
|
739
|
-
formIdOrSlug
|
|
740
|
-
})
|
|
741
|
-
};
|
|
742
|
-
const response = await fetchWithRetry(url, init, { retries: 3, timeoutMs: 15e3 });
|
|
743
|
-
if (!response.ok) {
|
|
744
|
-
throw new Error(`Failed to fetch form: ${response.statusText}`);
|
|
745
|
-
}
|
|
746
|
-
const data = await response.json();
|
|
747
|
-
if (!data) throw new Error("Form not found");
|
|
748
|
-
if (typeof window !== "undefined" && data.recaptcha_site_key) {
|
|
749
|
-
;
|
|
750
|
-
window.__SITE_KIT_RECAPTCHA_SITE_KEY__ = data.recaptcha_site_key;
|
|
751
|
-
}
|
|
752
|
-
if (data.steps) {
|
|
753
|
-
data.steps.sort((a, b) => (a.step_number || 0) - (b.step_number || 0));
|
|
754
|
-
}
|
|
755
|
-
if (data.fields) {
|
|
756
|
-
data.fields.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0));
|
|
757
|
-
}
|
|
758
|
-
const total_steps = data.steps?.length || 1;
|
|
759
|
-
const is_multi_step = total_steps > 1;
|
|
760
|
-
const apiSuccess = typeof data.success_message === "string" ? data.success_message.trim() : "";
|
|
761
|
-
const success_message = apiSuccess || (typeof data.successMessage === "string" ? String(data.successMessage).trim() : "") || DEFAULT_FORM_SUCCESS_MESSAGE;
|
|
762
|
-
setForm({
|
|
763
|
-
...data,
|
|
764
|
-
success_message,
|
|
765
|
-
total_steps,
|
|
766
|
-
is_multi_step
|
|
767
|
-
});
|
|
768
|
-
const defaults = {};
|
|
769
|
-
for (const field2 of data.fields || []) {
|
|
770
|
-
if (field2.default_value !== void 0 && field2.default_value !== null) {
|
|
771
|
-
defaults[field2.slug] = field2.default_value;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
setValuesState({ ...defaults, ...initialValues });
|
|
775
|
-
} catch (err) {
|
|
776
|
-
setFetchError(err);
|
|
777
|
-
} finally {
|
|
778
|
-
setIsLoading(false);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
fetchForm();
|
|
782
|
-
}, [formIdOrSlug, projectId]);
|
|
783
|
-
const allFields = useMemo(() => form?.fields || [], [form]);
|
|
784
|
-
const fields = useMemo(() => {
|
|
785
|
-
if (!isMultiStep) return allFields;
|
|
786
|
-
const currentStepConfig = form?.steps?.find((s) => s.step_number === step);
|
|
787
|
-
if (!currentStepConfig) return allFields;
|
|
788
|
-
return allFields.filter((f) => f.step_id === currentStepConfig.id);
|
|
789
|
-
}, [form, step, allFields, isMultiStep]);
|
|
790
|
-
const isFieldVisible = useCallback((field2) => {
|
|
791
|
-
if (!field2.conditional?.show_when) return true;
|
|
792
|
-
const { field: condField, operator, value } = field2.conditional.show_when;
|
|
793
|
-
const fieldValue = values[condField];
|
|
794
|
-
switch (operator) {
|
|
795
|
-
case "equals":
|
|
796
|
-
return fieldValue === value;
|
|
797
|
-
case "not_equals":
|
|
798
|
-
return fieldValue !== value;
|
|
799
|
-
case "contains":
|
|
800
|
-
return String(fieldValue || "").includes(String(value));
|
|
801
|
-
case "not_contains":
|
|
802
|
-
return !String(fieldValue || "").includes(String(value));
|
|
803
|
-
case "is_empty":
|
|
804
|
-
return !fieldValue || fieldValue === "";
|
|
805
|
-
case "not_empty":
|
|
806
|
-
return !!fieldValue && fieldValue !== "";
|
|
807
|
-
case "greater_than":
|
|
808
|
-
return Number(fieldValue) > Number(value);
|
|
809
|
-
case "less_than":
|
|
810
|
-
return Number(fieldValue) < Number(value);
|
|
811
|
-
default:
|
|
812
|
-
return true;
|
|
813
|
-
}
|
|
814
|
-
}, [values]);
|
|
815
|
-
const visibleFields = useMemo(() => {
|
|
816
|
-
return fields.filter(isFieldVisible);
|
|
817
|
-
}, [fields, isFieldVisible]);
|
|
818
|
-
const validateField = useCallback((field2) => {
|
|
819
|
-
if (!isFieldVisible(field2)) return null;
|
|
820
|
-
if (["heading", "section_header", "paragraph", "hidden"].includes(field2.field_type)) return null;
|
|
821
|
-
const value = values[field2.slug];
|
|
822
|
-
const rules = field2.validation || {};
|
|
823
|
-
if (field2.is_required && (!value || value === "")) {
|
|
824
|
-
return rules.custom_error || `${field2.label} is required`;
|
|
825
|
-
}
|
|
826
|
-
if (!value) return null;
|
|
827
|
-
const strValue = String(value);
|
|
828
|
-
if (rules.min_length && strValue.length < rules.min_length) {
|
|
829
|
-
return `${field2.label} must be at least ${rules.min_length} characters`;
|
|
830
|
-
}
|
|
831
|
-
if (rules.max_length && strValue.length > rules.max_length) {
|
|
832
|
-
return `${field2.label} must be no more than ${rules.max_length} characters`;
|
|
833
|
-
}
|
|
834
|
-
if (rules.min !== void 0 && Number(value) < rules.min) {
|
|
835
|
-
return `${field2.label} must be at least ${rules.min}`;
|
|
836
|
-
}
|
|
837
|
-
if (rules.max !== void 0 && Number(value) > rules.max) {
|
|
838
|
-
return `${field2.label} must be no more than ${rules.max}`;
|
|
839
|
-
}
|
|
840
|
-
if (rules.pattern) {
|
|
841
|
-
try {
|
|
842
|
-
if (!new RegExp(rules.pattern).test(strValue)) {
|
|
843
|
-
return rules.custom_error || `${field2.label} is invalid`;
|
|
844
|
-
}
|
|
845
|
-
} catch {
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
if (field2.field_type === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(strValue)) {
|
|
849
|
-
return "Please enter a valid email address";
|
|
850
|
-
}
|
|
851
|
-
if (field2.field_type === "phone" && !/^[\d\s\-\+\(\)]+$/.test(strValue)) {
|
|
852
|
-
return "Please enter a valid phone number";
|
|
853
|
-
}
|
|
854
|
-
return null;
|
|
855
|
-
}, [values, isFieldVisible]);
|
|
856
|
-
const validate = useCallback(() => {
|
|
857
|
-
const newErrors = {};
|
|
858
|
-
for (const field2 of fields) {
|
|
859
|
-
const error = validateField(field2);
|
|
860
|
-
if (error) {
|
|
861
|
-
newErrors[field2.slug] = error;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
setErrors(newErrors);
|
|
865
|
-
return Object.keys(newErrors).length === 0;
|
|
866
|
-
}, [fields, validateField]);
|
|
867
|
-
const validateAll = useCallback(() => {
|
|
868
|
-
const newErrors = {};
|
|
869
|
-
for (const field2 of allFields) {
|
|
870
|
-
const error = validateField(field2);
|
|
871
|
-
if (error) newErrors[field2.slug] = error;
|
|
872
|
-
}
|
|
873
|
-
setErrors(newErrors);
|
|
874
|
-
return Object.keys(newErrors).length === 0;
|
|
875
|
-
}, [allFields, validateField]);
|
|
876
|
-
const buildVisibleValues = useCallback(() => {
|
|
877
|
-
const out = {};
|
|
878
|
-
for (const field2 of allFields) {
|
|
879
|
-
if (!isFieldVisible(field2)) continue;
|
|
880
|
-
if (field2.slug in values) out[field2.slug] = values[field2.slug];
|
|
881
|
-
}
|
|
882
|
-
return out;
|
|
883
|
-
}, [allFields, values, isFieldVisible]);
|
|
884
|
-
const setFieldValue = useCallback((key, value) => {
|
|
885
|
-
setValuesState((prev) => ({ ...prev, [key]: value }));
|
|
886
|
-
setErrors((prev) => {
|
|
887
|
-
if (prev[key]) {
|
|
888
|
-
const next = { ...prev };
|
|
889
|
-
delete next[key];
|
|
890
|
-
return next;
|
|
891
|
-
}
|
|
892
|
-
return prev;
|
|
893
|
-
});
|
|
894
|
-
}, []);
|
|
895
|
-
const setValues = useCallback((newValues) => {
|
|
896
|
-
setValuesState((prev) => ({ ...prev, ...newValues }));
|
|
897
|
-
}, []);
|
|
898
|
-
const clearErrors = useCallback(() => {
|
|
899
|
-
setErrors({});
|
|
900
|
-
}, []);
|
|
901
|
-
const canGoNext = step < totalSteps;
|
|
902
|
-
const canGoPrev = step > 1;
|
|
903
|
-
const isLastStep = step === totalSteps;
|
|
904
|
-
const progress = Math.round(step / totalSteps * 100);
|
|
905
|
-
const nextStep = useCallback(() => {
|
|
906
|
-
if (!validate()) return false;
|
|
907
|
-
if (step < totalSteps) {
|
|
908
|
-
const newStep = step + 1;
|
|
909
|
-
setStep(newStep);
|
|
910
|
-
trackStepChange(newStep);
|
|
911
|
-
return true;
|
|
912
|
-
}
|
|
913
|
-
return false;
|
|
914
|
-
}, [step, totalSteps, validate, trackStepChange]);
|
|
915
|
-
const prevStep = useCallback(() => {
|
|
916
|
-
if (step > 1) {
|
|
917
|
-
const newStep = step - 1;
|
|
918
|
-
setStep(newStep);
|
|
919
|
-
trackStepChange(newStep);
|
|
920
|
-
}
|
|
921
|
-
}, [step, trackStepChange]);
|
|
922
|
-
const goToStep = useCallback((targetStep) => {
|
|
923
|
-
if (targetStep >= 1 && targetStep <= totalSteps) {
|
|
924
|
-
setStep(targetStep);
|
|
925
|
-
trackStepChange(targetStep);
|
|
926
|
-
}
|
|
927
|
-
}, [totalSteps, trackStepChange]);
|
|
928
|
-
const submit = useCallback(async () => {
|
|
929
|
-
if (!form) return;
|
|
930
|
-
if (isSubmittingRef.current) return;
|
|
931
|
-
if (!validateAll()) return;
|
|
932
|
-
isSubmittingRef.current = true;
|
|
933
|
-
setIsSubmitting(true);
|
|
934
|
-
setSubmitError(null);
|
|
935
|
-
try {
|
|
936
|
-
const data = await submitForm({
|
|
937
|
-
config: form,
|
|
938
|
-
values: buildVisibleValues(),
|
|
939
|
-
honeypotValue: "",
|
|
940
|
-
formLoadedAt: formLoadedAt.current
|
|
941
|
-
});
|
|
942
|
-
trackComplete();
|
|
943
|
-
setIsComplete(true);
|
|
944
|
-
onSuccess?.(data);
|
|
945
|
-
const finalRedirect = redirectUrl !== false ? redirectUrl || form.redirect_url : void 0;
|
|
946
|
-
if (finalRedirect) {
|
|
947
|
-
window.location.href = finalRedirect;
|
|
948
|
-
}
|
|
949
|
-
} catch (error) {
|
|
950
|
-
console.error("[useForm] Submission error:", error);
|
|
951
|
-
setSubmitError("Something went wrong sending your submission. Please try again.");
|
|
952
|
-
onError?.(error);
|
|
953
|
-
} finally {
|
|
954
|
-
isSubmittingRef.current = false;
|
|
955
|
-
setIsSubmitting(false);
|
|
956
|
-
}
|
|
957
|
-
}, [form, validateAll, buildVisibleValues, trackComplete, onSuccess, onError, redirectUrl]);
|
|
958
|
-
const reset = useCallback(() => {
|
|
959
|
-
setValuesState(initialValues);
|
|
960
|
-
setErrors({});
|
|
961
|
-
setStep(1);
|
|
962
|
-
setIsComplete(false);
|
|
963
|
-
setSubmitError(null);
|
|
964
|
-
}, [initialValues]);
|
|
965
|
-
return {
|
|
966
|
-
form,
|
|
967
|
-
isLoading,
|
|
968
|
-
fetchError,
|
|
969
|
-
submitError,
|
|
970
|
-
allFields,
|
|
971
|
-
fields,
|
|
972
|
-
visibleFields,
|
|
973
|
-
values,
|
|
974
|
-
errors,
|
|
975
|
-
setFieldValue,
|
|
976
|
-
setValues,
|
|
977
|
-
clearErrors,
|
|
978
|
-
step,
|
|
979
|
-
totalSteps,
|
|
980
|
-
isMultiStep,
|
|
981
|
-
progress,
|
|
982
|
-
nextStep,
|
|
983
|
-
prevStep,
|
|
984
|
-
goToStep,
|
|
985
|
-
canGoNext,
|
|
986
|
-
canGoPrev,
|
|
987
|
-
isLastStep,
|
|
988
|
-
validate,
|
|
989
|
-
validateField,
|
|
990
|
-
isFieldVisible,
|
|
991
|
-
submit,
|
|
992
|
-
isSubmitting,
|
|
993
|
-
isComplete,
|
|
994
|
-
reset
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
325
|
var DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
998
326
|
var MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
999
327
|
function pad(n) {
|
|
@@ -1941,6 +1269,6 @@ function ManagedForm({
|
|
|
1941
1269
|
);
|
|
1942
1270
|
}
|
|
1943
1271
|
|
|
1944
|
-
export {
|
|
1272
|
+
export { DatePicker, FormClient, FormField as FormFieldComponent, ManagedForm, configureFormsApi, defineForm, field, formsApi, initializeForms };
|
|
1945
1273
|
//# sourceMappingURL=index.mjs.map
|
|
1946
1274
|
//# sourceMappingURL=index.mjs.map
|