@sonordev/site-kit 2.6.1 → 2.7.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/{chunk-HSSKX77P.mjs → chunk-FN7ADGWH.mjs} +729 -5
- package/dist/chunk-FN7ADGWH.mjs.map +1 -0
- package/dist/{chunk-D5ES7H7E.js → chunk-KCA5OLS4.js} +729 -4
- package/dist/chunk-KCA5OLS4.js.map +1 -0
- package/dist/commerce/index.d.mts +1 -1
- package/dist/commerce/index.d.ts +1 -1
- package/dist/commerce/index.js +49 -45
- package/dist/commerce/index.mjs +1 -1
- package/dist/forms/index.d.mts +2 -0
- package/dist/forms/index.d.ts +2 -0
- package/dist/forms/index.js +395 -211
- package/dist/forms/index.js.map +1 -1
- package/dist/forms/index.mjs +329 -149
- package/dist/forms/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +29 -25
- package/dist/index.mjs +1 -1
- package/dist/{useEventModal-DvqBRau-.d.mts → useEventModal-4W_y1SYY.d.mts} +36 -3
- package/dist/{useEventModal-C4Dr4Psb.d.ts → useEventModal-CJLCmH5Y.d.ts} +36 -3
- package/package.json +1 -1
- package/dist/chunk-D5ES7H7E.js.map +0 -1
- package/dist/chunk-HSSKX77P.mjs.map +0 -1
package/dist/forms/index.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
require('../chunk-ZSMWDLMK.js');
|
|
5
|
-
var
|
|
5
|
+
var React2 = require('react');
|
|
6
6
|
var jsxRuntime = require('react/jsx-runtime');
|
|
7
7
|
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var React2__default = /*#__PURE__*/_interopDefault(React2);
|
|
11
|
+
|
|
8
12
|
// src/forms/formsApi.ts
|
|
9
13
|
var config = null;
|
|
10
14
|
function configureFormsApi(options) {
|
|
@@ -328,14 +332,14 @@ function useFormTracking({
|
|
|
328
332
|
enabled = true,
|
|
329
333
|
debug = false
|
|
330
334
|
}) {
|
|
331
|
-
const sessionIdRef =
|
|
332
|
-
const startTimeRef =
|
|
333
|
-
const stepStartTimeRef =
|
|
334
|
-
const stepTimesRef =
|
|
335
|
-
const currentStepRef =
|
|
336
|
-
const maxStepRef =
|
|
337
|
-
const analyticsIdRef =
|
|
338
|
-
|
|
335
|
+
const sessionIdRef = React2.useRef("");
|
|
336
|
+
const startTimeRef = React2.useRef(0);
|
|
337
|
+
const stepStartTimeRef = React2.useRef(0);
|
|
338
|
+
const stepTimesRef = React2.useRef({});
|
|
339
|
+
const currentStepRef = React2.useRef(1);
|
|
340
|
+
const maxStepRef = React2.useRef(1);
|
|
341
|
+
const analyticsIdRef = React2.useRef(null);
|
|
342
|
+
React2.useEffect(() => {
|
|
339
343
|
if (!enabled) return;
|
|
340
344
|
if (!formId) {
|
|
341
345
|
if (debug) console.warn("[Forms] Tracking skipped: formId is empty");
|
|
@@ -406,7 +410,7 @@ function useFormTracking({
|
|
|
406
410
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
407
411
|
};
|
|
408
412
|
}, [formId, enabled, debug]);
|
|
409
|
-
const trackStepChange =
|
|
413
|
+
const trackStepChange = React2.useCallback(async (step) => {
|
|
410
414
|
if (!enabled || !analyticsIdRef.current) return;
|
|
411
415
|
const apiUrl = window.__SITE_KIT_API_URL__;
|
|
412
416
|
const apiKey = window.__SITE_KIT_API_KEY__;
|
|
@@ -442,7 +446,7 @@ function useFormTracking({
|
|
|
442
446
|
if (debug) console.error("[Forms] Error updating step:", error);
|
|
443
447
|
}
|
|
444
448
|
}, [enabled, debug]);
|
|
445
|
-
const trackComplete =
|
|
449
|
+
const trackComplete = React2.useCallback(async () => {
|
|
446
450
|
if (!enabled || !analyticsIdRef.current) return;
|
|
447
451
|
const apiUrl = window.__SITE_KIT_API_URL__;
|
|
448
452
|
const apiKey = window.__SITE_KIT_API_KEY__;
|
|
@@ -508,6 +512,42 @@ function getDeviceType() {
|
|
|
508
512
|
return "desktop";
|
|
509
513
|
}
|
|
510
514
|
|
|
515
|
+
// src/forms/fetchWithRetry.ts
|
|
516
|
+
function backoffDelay(attempt, response) {
|
|
517
|
+
if (response?.status === 429) {
|
|
518
|
+
const header = response.headers.get("retry-after");
|
|
519
|
+
const seconds = header ? parseInt(header, 10) : NaN;
|
|
520
|
+
if (!Number.isNaN(seconds) && seconds > 0) return Math.min(seconds * 1e3, 1e4);
|
|
521
|
+
}
|
|
522
|
+
return Math.min(8e3, 500 * 2 ** attempt);
|
|
523
|
+
}
|
|
524
|
+
async function fetchWithRetry(url, init, options = {}) {
|
|
525
|
+
const retries = options.retries ?? 2;
|
|
526
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
527
|
+
let lastError;
|
|
528
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
529
|
+
const controller = new AbortController();
|
|
530
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
531
|
+
try {
|
|
532
|
+
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
533
|
+
clearTimeout(timer);
|
|
534
|
+
if ((response.status === 429 || response.status >= 500) && attempt < retries) {
|
|
535
|
+
await new Promise((resolve) => setTimeout(resolve, backoffDelay(attempt, response)));
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
return response;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
clearTimeout(timer);
|
|
541
|
+
lastError = error;
|
|
542
|
+
if (attempt < retries) {
|
|
543
|
+
await new Promise((resolve) => setTimeout(resolve, backoffDelay(attempt)));
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
throw lastError instanceof Error ? lastError : new Error("Network request failed after multiple attempts. Please try again.");
|
|
549
|
+
}
|
|
550
|
+
|
|
511
551
|
// src/forms/recaptcha.ts
|
|
512
552
|
function getSiteKey(explicitSiteKey) {
|
|
513
553
|
if (typeof window === "undefined") return void 0;
|
|
@@ -515,8 +555,9 @@ function getSiteKey(explicitSiteKey) {
|
|
|
515
555
|
const win = window;
|
|
516
556
|
return win.__SITE_KIT_RECAPTCHA_SITE_KEY__ ?? process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
|
|
517
557
|
}
|
|
558
|
+
var RECAPTCHA_LOAD_TIMEOUT_MS = 1e4;
|
|
518
559
|
function loadScript(siteKey) {
|
|
519
|
-
return new Promise((resolve
|
|
560
|
+
return new Promise((resolve) => {
|
|
520
561
|
if (typeof window === "undefined") {
|
|
521
562
|
resolve(null);
|
|
522
563
|
return;
|
|
@@ -526,20 +567,35 @@ function loadScript(siteKey) {
|
|
|
526
567
|
resolve(win.grecaptcha);
|
|
527
568
|
return;
|
|
528
569
|
}
|
|
570
|
+
let settled = false;
|
|
571
|
+
const finish = (value) => {
|
|
572
|
+
if (settled) return;
|
|
573
|
+
settled = true;
|
|
574
|
+
resolve(value);
|
|
575
|
+
};
|
|
576
|
+
const giveUp = setTimeout(() => finish(null), RECAPTCHA_LOAD_TIMEOUT_MS);
|
|
577
|
+
const waitForReady = () => {
|
|
578
|
+
if (settled) return;
|
|
579
|
+
if (win.grecaptcha?.enterprise) {
|
|
580
|
+
clearTimeout(giveUp);
|
|
581
|
+
finish(win.grecaptcha);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
setTimeout(waitForReady, 50);
|
|
585
|
+
};
|
|
529
586
|
const existing = document.querySelector('script[src*="recaptcha/enterprise"]');
|
|
530
587
|
if (existing) {
|
|
531
|
-
|
|
532
|
-
if (win.grecaptcha?.enterprise) resolve(win.grecaptcha);
|
|
533
|
-
else setTimeout(check, 50);
|
|
534
|
-
};
|
|
535
|
-
check();
|
|
588
|
+
waitForReady();
|
|
536
589
|
return;
|
|
537
590
|
}
|
|
538
591
|
const script = document.createElement("script");
|
|
539
592
|
script.src = `https://www.google.com/recaptcha/enterprise.js?render=${siteKey}`;
|
|
540
593
|
script.async = true;
|
|
541
|
-
script.onload = () =>
|
|
542
|
-
script.onerror = () =>
|
|
594
|
+
script.onload = () => waitForReady();
|
|
595
|
+
script.onerror = () => {
|
|
596
|
+
clearTimeout(giveUp);
|
|
597
|
+
finish(null);
|
|
598
|
+
};
|
|
543
599
|
document.head.appendChild(script);
|
|
544
600
|
});
|
|
545
601
|
}
|
|
@@ -557,21 +613,83 @@ async function getRecaptchaToken(explicitSiteKey) {
|
|
|
557
613
|
}
|
|
558
614
|
}
|
|
559
615
|
|
|
560
|
-
// src/forms/
|
|
561
|
-
var
|
|
562
|
-
|
|
563
|
-
|
|
616
|
+
// src/forms/submitForm.ts
|
|
617
|
+
var DEFAULT_HONEYPOT_FIELD = "_sk_hp_url";
|
|
618
|
+
function getApiConfig() {
|
|
619
|
+
const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.sonor.io" : "https://api.sonor.io";
|
|
620
|
+
const apiKey = typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ : void 0;
|
|
621
|
+
return { apiUrl, apiKey };
|
|
622
|
+
}
|
|
564
623
|
function getUTMParams() {
|
|
565
624
|
if (typeof window === "undefined") return {};
|
|
566
625
|
const params = new URLSearchParams(window.location.search);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
626
|
+
const out = {};
|
|
627
|
+
for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"]) {
|
|
628
|
+
out[key] = params.get(key);
|
|
629
|
+
}
|
|
630
|
+
return out;
|
|
631
|
+
}
|
|
632
|
+
async function submitForm({
|
|
633
|
+
config: config2,
|
|
634
|
+
values,
|
|
635
|
+
honeypotValue,
|
|
636
|
+
formLoadedAt
|
|
637
|
+
}) {
|
|
638
|
+
const { apiUrl, apiKey } = getApiConfig();
|
|
639
|
+
if (!apiKey) {
|
|
640
|
+
throw new Error("API key is required. Set SONOR_API_KEY in your environment.");
|
|
641
|
+
}
|
|
642
|
+
const recaptchaToken = config2.recaptcha_enabled ? await getRecaptchaToken(config2.recaptcha_site_key) : null;
|
|
643
|
+
if (config2.recaptcha_enabled && !recaptchaToken) {
|
|
644
|
+
throw new Error("Verification could not be completed. Please try again.");
|
|
645
|
+
}
|
|
646
|
+
const honeypotFieldName = config2.honeypot_field || DEFAULT_HONEYPOT_FIELD;
|
|
647
|
+
const utm = getUTMParams();
|
|
648
|
+
const payload = {
|
|
649
|
+
formId: config2.id,
|
|
650
|
+
form_id: config2.id,
|
|
651
|
+
data: {
|
|
652
|
+
...values,
|
|
653
|
+
...config2.honeypot_enabled ? { [honeypotFieldName]: honeypotValue } : {}
|
|
654
|
+
},
|
|
655
|
+
metadata: {
|
|
656
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : null,
|
|
657
|
+
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
658
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
|
659
|
+
sessionId: typeof sessionStorage !== "undefined" ? sessionStorage.getItem("_sk_sid") : null,
|
|
660
|
+
recaptchaToken: recaptchaToken || void 0,
|
|
661
|
+
utmSource: utm.utm_source,
|
|
662
|
+
utmMedium: utm.utm_medium,
|
|
663
|
+
utmCampaign: utm.utm_campaign,
|
|
664
|
+
_formLoadedAt: formLoadedAt
|
|
665
|
+
}
|
|
573
666
|
};
|
|
667
|
+
const response = await fetchWithRetry(
|
|
668
|
+
`${apiUrl}/api/public/forms/submit`,
|
|
669
|
+
{
|
|
670
|
+
method: "POST",
|
|
671
|
+
headers: {
|
|
672
|
+
"Content-Type": "application/json",
|
|
673
|
+
Authorization: `Bearer ${apiKey}`
|
|
674
|
+
},
|
|
675
|
+
body: JSON.stringify(payload)
|
|
676
|
+
},
|
|
677
|
+
{ retries: 2, timeoutMs: 2e4 }
|
|
678
|
+
);
|
|
679
|
+
const data = await response.json().catch(() => null);
|
|
680
|
+
if (!response.ok) {
|
|
681
|
+
const msg = (data?.message ?? data?.error ?? (response.statusText || `HTTP ${response.status}`)).toString().trim();
|
|
682
|
+
throw new Error(
|
|
683
|
+
msg ? `Failed to submit form: ${msg}` : `Failed to submit form (${response.status})`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
return data;
|
|
574
687
|
}
|
|
688
|
+
|
|
689
|
+
// src/forms/defaultSuccessMessage.ts
|
|
690
|
+
var DEFAULT_FORM_SUCCESS_MESSAGE = "Thank you for your submission! We will be in touch soon.";
|
|
691
|
+
|
|
692
|
+
// src/forms/useForm.ts
|
|
575
693
|
function useForm(formIdOrSlug, options = {}) {
|
|
576
694
|
const {
|
|
577
695
|
projectId: optionsProjectId,
|
|
@@ -581,32 +699,38 @@ function useForm(formIdOrSlug, options = {}) {
|
|
|
581
699
|
redirectUrl
|
|
582
700
|
} = options;
|
|
583
701
|
const projectId = optionsProjectId || (typeof window !== "undefined" ? window.__SITE_KIT_PROJECT_ID__ : void 0);
|
|
584
|
-
const [form, setForm] =
|
|
585
|
-
const [isLoading, setIsLoading] =
|
|
586
|
-
const [fetchError, setFetchError] =
|
|
587
|
-
const [values, setValuesState] =
|
|
588
|
-
const [errors, setErrors] =
|
|
589
|
-
const [step, setStep] =
|
|
590
|
-
const [isSubmitting, setIsSubmitting] =
|
|
591
|
-
const [isComplete, setIsComplete] =
|
|
702
|
+
const [form, setForm] = React2.useState(null);
|
|
703
|
+
const [isLoading, setIsLoading] = React2.useState(true);
|
|
704
|
+
const [fetchError, setFetchError] = React2.useState(null);
|
|
705
|
+
const [values, setValuesState] = React2.useState(initialValues);
|
|
706
|
+
const [errors, setErrors] = React2.useState({});
|
|
707
|
+
const [step, setStep] = React2.useState(1);
|
|
708
|
+
const [isSubmitting, setIsSubmitting] = React2.useState(false);
|
|
709
|
+
const [isComplete, setIsComplete] = React2.useState(false);
|
|
710
|
+
const [submitError, setSubmitError] = React2.useState(null);
|
|
711
|
+
const isSubmittingRef = React2.useRef(false);
|
|
712
|
+
const formLoadedAt = React2.useRef(Date.now());
|
|
592
713
|
const totalSteps = form?.total_steps || 1;
|
|
593
714
|
const isMultiStep = form?.is_multi_step || false;
|
|
594
715
|
const { trackStepChange, trackComplete } = useFormTracking({
|
|
595
716
|
formId: form?.id || "",
|
|
596
717
|
totalSteps
|
|
597
718
|
});
|
|
598
|
-
|
|
719
|
+
React2.useEffect(() => {
|
|
599
720
|
const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.sonor.io" : "https://api.sonor.io";
|
|
600
|
-
const apiKey = typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ : void 0;
|
|
601
|
-
if (!apiKey) {
|
|
602
|
-
setFetchError(new Error("API key is required. Provide it via SiteKitProvider (apiKey) or set __SITE_KIT_API_KEY__."));
|
|
603
|
-
setIsLoading(false);
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
721
|
async function fetchForm() {
|
|
607
722
|
try {
|
|
608
723
|
setIsLoading(true);
|
|
609
724
|
setFetchError(null);
|
|
725
|
+
const readApiKey = () => typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ : void 0;
|
|
726
|
+
let apiKey = readApiKey();
|
|
727
|
+
for (let i = 0; i < 15 && !apiKey; i++) {
|
|
728
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
729
|
+
apiKey = readApiKey();
|
|
730
|
+
}
|
|
731
|
+
if (!apiKey) {
|
|
732
|
+
throw new Error("API key is required. Provide it via SiteKitLayout/SiteKitProvider or set SONOR_API_KEY.");
|
|
733
|
+
}
|
|
610
734
|
const url = `${apiUrl}/api/public/forms/config`;
|
|
611
735
|
const init = {
|
|
612
736
|
method: "POST",
|
|
@@ -618,15 +742,7 @@ function useForm(formIdOrSlug, options = {}) {
|
|
|
618
742
|
formIdOrSlug
|
|
619
743
|
})
|
|
620
744
|
};
|
|
621
|
-
const
|
|
622
|
-
let response = await fetch(url, init);
|
|
623
|
-
for (let attempt = 1; attempt < maxAttempts && response.status === 429; attempt++) {
|
|
624
|
-
const ra = response.headers.get("retry-after");
|
|
625
|
-
const retryAfterSec = ra ? parseInt(ra, 10) : NaN;
|
|
626
|
-
const delayMs = !Number.isNaN(retryAfterSec) && retryAfterSec > 0 ? retryAfterSec * 1e3 : Math.min(8e3, 400 * 2 ** (attempt - 1));
|
|
627
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
628
|
-
response = await fetch(url, init);
|
|
629
|
-
}
|
|
745
|
+
const response = await fetchWithRetry(url, init, { retries: 3, timeoutMs: 15e3 });
|
|
630
746
|
if (!response.ok) {
|
|
631
747
|
throw new Error(`Failed to fetch form: ${response.statusText}`);
|
|
632
748
|
}
|
|
@@ -667,14 +783,14 @@ function useForm(formIdOrSlug, options = {}) {
|
|
|
667
783
|
}
|
|
668
784
|
fetchForm();
|
|
669
785
|
}, [formIdOrSlug, projectId]);
|
|
670
|
-
const allFields =
|
|
671
|
-
const fields =
|
|
786
|
+
const allFields = React2.useMemo(() => form?.fields || [], [form]);
|
|
787
|
+
const fields = React2.useMemo(() => {
|
|
672
788
|
if (!isMultiStep) return allFields;
|
|
673
789
|
const currentStepConfig = form?.steps?.find((s) => s.step_number === step);
|
|
674
790
|
if (!currentStepConfig) return allFields;
|
|
675
791
|
return allFields.filter((f) => f.step_id === currentStepConfig.id);
|
|
676
792
|
}, [form, step, allFields, isMultiStep]);
|
|
677
|
-
const isFieldVisible =
|
|
793
|
+
const isFieldVisible = React2.useCallback((field2) => {
|
|
678
794
|
if (!field2.conditional?.show_when) return true;
|
|
679
795
|
const { field: condField, operator, value } = field2.conditional.show_when;
|
|
680
796
|
const fieldValue = values[condField];
|
|
@@ -699,11 +815,12 @@ function useForm(formIdOrSlug, options = {}) {
|
|
|
699
815
|
return true;
|
|
700
816
|
}
|
|
701
817
|
}, [values]);
|
|
702
|
-
const visibleFields =
|
|
818
|
+
const visibleFields = React2.useMemo(() => {
|
|
703
819
|
return fields.filter(isFieldVisible);
|
|
704
820
|
}, [fields, isFieldVisible]);
|
|
705
|
-
const validateField =
|
|
821
|
+
const validateField = React2.useCallback((field2) => {
|
|
706
822
|
if (!isFieldVisible(field2)) return null;
|
|
823
|
+
if (["heading", "section_header", "paragraph", "hidden"].includes(field2.field_type)) return null;
|
|
707
824
|
const value = values[field2.slug];
|
|
708
825
|
const rules = field2.validation || {};
|
|
709
826
|
if (field2.is_required && (!value || value === "")) {
|
|
@@ -723,8 +840,13 @@ function useForm(formIdOrSlug, options = {}) {
|
|
|
723
840
|
if (rules.max !== void 0 && Number(value) > rules.max) {
|
|
724
841
|
return `${field2.label} must be no more than ${rules.max}`;
|
|
725
842
|
}
|
|
726
|
-
if (rules.pattern
|
|
727
|
-
|
|
843
|
+
if (rules.pattern) {
|
|
844
|
+
try {
|
|
845
|
+
if (!new RegExp(rules.pattern).test(strValue)) {
|
|
846
|
+
return rules.custom_error || `${field2.label} is invalid`;
|
|
847
|
+
}
|
|
848
|
+
} catch {
|
|
849
|
+
}
|
|
728
850
|
}
|
|
729
851
|
if (field2.field_type === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(strValue)) {
|
|
730
852
|
return "Please enter a valid email address";
|
|
@@ -734,7 +856,7 @@ function useForm(formIdOrSlug, options = {}) {
|
|
|
734
856
|
}
|
|
735
857
|
return null;
|
|
736
858
|
}, [values, isFieldVisible]);
|
|
737
|
-
const validate =
|
|
859
|
+
const validate = React2.useCallback(() => {
|
|
738
860
|
const newErrors = {};
|
|
739
861
|
for (const field2 of fields) {
|
|
740
862
|
const error = validateField(field2);
|
|
@@ -745,7 +867,24 @@ function useForm(formIdOrSlug, options = {}) {
|
|
|
745
867
|
setErrors(newErrors);
|
|
746
868
|
return Object.keys(newErrors).length === 0;
|
|
747
869
|
}, [fields, validateField]);
|
|
748
|
-
const
|
|
870
|
+
const validateAll = React2.useCallback(() => {
|
|
871
|
+
const newErrors = {};
|
|
872
|
+
for (const field2 of allFields) {
|
|
873
|
+
const error = validateField(field2);
|
|
874
|
+
if (error) newErrors[field2.slug] = error;
|
|
875
|
+
}
|
|
876
|
+
setErrors(newErrors);
|
|
877
|
+
return Object.keys(newErrors).length === 0;
|
|
878
|
+
}, [allFields, validateField]);
|
|
879
|
+
const buildVisibleValues = React2.useCallback(() => {
|
|
880
|
+
const out = {};
|
|
881
|
+
for (const field2 of allFields) {
|
|
882
|
+
if (!isFieldVisible(field2)) continue;
|
|
883
|
+
if (field2.slug in values) out[field2.slug] = values[field2.slug];
|
|
884
|
+
}
|
|
885
|
+
return out;
|
|
886
|
+
}, [allFields, values, isFieldVisible]);
|
|
887
|
+
const setFieldValue = React2.useCallback((key, value) => {
|
|
749
888
|
setValuesState((prev) => ({ ...prev, [key]: value }));
|
|
750
889
|
setErrors((prev) => {
|
|
751
890
|
if (prev[key]) {
|
|
@@ -756,17 +895,17 @@ function useForm(formIdOrSlug, options = {}) {
|
|
|
756
895
|
return prev;
|
|
757
896
|
});
|
|
758
897
|
}, []);
|
|
759
|
-
const setValues =
|
|
898
|
+
const setValues = React2.useCallback((newValues) => {
|
|
760
899
|
setValuesState((prev) => ({ ...prev, ...newValues }));
|
|
761
900
|
}, []);
|
|
762
|
-
const clearErrors =
|
|
901
|
+
const clearErrors = React2.useCallback(() => {
|
|
763
902
|
setErrors({});
|
|
764
903
|
}, []);
|
|
765
904
|
const canGoNext = step < totalSteps;
|
|
766
905
|
const canGoPrev = step > 1;
|
|
767
906
|
const isLastStep = step === totalSteps;
|
|
768
907
|
const progress = Math.round(step / totalSteps * 100);
|
|
769
|
-
const nextStep =
|
|
908
|
+
const nextStep = React2.useCallback(() => {
|
|
770
909
|
if (!validate()) return false;
|
|
771
910
|
if (step < totalSteps) {
|
|
772
911
|
const newStep = step + 1;
|
|
@@ -776,64 +915,33 @@ function useForm(formIdOrSlug, options = {}) {
|
|
|
776
915
|
}
|
|
777
916
|
return false;
|
|
778
917
|
}, [step, totalSteps, validate, trackStepChange]);
|
|
779
|
-
const prevStep =
|
|
918
|
+
const prevStep = React2.useCallback(() => {
|
|
780
919
|
if (step > 1) {
|
|
781
920
|
const newStep = step - 1;
|
|
782
921
|
setStep(newStep);
|
|
783
922
|
trackStepChange(newStep);
|
|
784
923
|
}
|
|
785
924
|
}, [step, trackStepChange]);
|
|
786
|
-
const goToStep =
|
|
925
|
+
const goToStep = React2.useCallback((targetStep) => {
|
|
787
926
|
if (targetStep >= 1 && targetStep <= totalSteps) {
|
|
788
927
|
setStep(targetStep);
|
|
789
928
|
trackStepChange(targetStep);
|
|
790
929
|
}
|
|
791
930
|
}, [totalSteps, trackStepChange]);
|
|
792
|
-
const submit =
|
|
931
|
+
const submit = React2.useCallback(async () => {
|
|
793
932
|
if (!form) return;
|
|
794
|
-
if (
|
|
933
|
+
if (isSubmittingRef.current) return;
|
|
934
|
+
if (!validateAll()) return;
|
|
935
|
+
isSubmittingRef.current = true;
|
|
795
936
|
setIsSubmitting(true);
|
|
937
|
+
setSubmitError(null);
|
|
796
938
|
try {
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const honeypotFieldName = form.honeypot_field || "website";
|
|
803
|
-
const recaptchaToken = form.recaptcha_enabled ? await getRecaptchaToken(form.recaptcha_site_key) : null;
|
|
804
|
-
if (form.recaptcha_enabled && !recaptchaToken) {
|
|
805
|
-
throw new Error("reCAPTCHA is required but could not be initialized.");
|
|
806
|
-
}
|
|
807
|
-
const utm = getUTMParams();
|
|
808
|
-
const submission = {
|
|
809
|
-
formId: form.id,
|
|
810
|
-
data: {
|
|
811
|
-
...values,
|
|
812
|
-
...form.honeypot_enabled ? { [honeypotFieldName]: "" } : {}
|
|
813
|
-
},
|
|
814
|
-
metadata: {
|
|
815
|
-
pageUrl: typeof window !== "undefined" ? window.location.href : null,
|
|
816
|
-
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
817
|
-
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
|
818
|
-
sessionId: typeof sessionStorage !== "undefined" ? sessionStorage.getItem("_sk_sid") : null,
|
|
819
|
-
recaptchaToken: recaptchaToken || void 0,
|
|
820
|
-
utmSource: utm.utm_source,
|
|
821
|
-
utmMedium: utm.utm_medium,
|
|
822
|
-
utmCampaign: utm.utm_campaign
|
|
823
|
-
}
|
|
824
|
-
};
|
|
825
|
-
const response = await fetch(`${apiUrl}/api/public/forms/submit`, {
|
|
826
|
-
method: "POST",
|
|
827
|
-
headers: {
|
|
828
|
-
"Content-Type": "application/json",
|
|
829
|
-
"Authorization": `Bearer ${apiKey}`
|
|
830
|
-
},
|
|
831
|
-
body: JSON.stringify(submission)
|
|
939
|
+
const data = await submitForm({
|
|
940
|
+
config: form,
|
|
941
|
+
values: buildVisibleValues(),
|
|
942
|
+
honeypotValue: "",
|
|
943
|
+
formLoadedAt: formLoadedAt.current
|
|
832
944
|
});
|
|
833
|
-
if (!response.ok) {
|
|
834
|
-
throw new Error(`Form submission failed: ${response.statusText}`);
|
|
835
|
-
}
|
|
836
|
-
const data = await response.json();
|
|
837
945
|
trackComplete();
|
|
838
946
|
setIsComplete(true);
|
|
839
947
|
onSuccess?.(data);
|
|
@@ -843,21 +951,25 @@ function useForm(formIdOrSlug, options = {}) {
|
|
|
843
951
|
}
|
|
844
952
|
} catch (error) {
|
|
845
953
|
console.error("[useForm] Submission error:", error);
|
|
954
|
+
setSubmitError("Something went wrong sending your submission. Please try again.");
|
|
846
955
|
onError?.(error);
|
|
847
956
|
} finally {
|
|
957
|
+
isSubmittingRef.current = false;
|
|
848
958
|
setIsSubmitting(false);
|
|
849
959
|
}
|
|
850
|
-
}, [form,
|
|
851
|
-
const reset =
|
|
960
|
+
}, [form, validateAll, buildVisibleValues, trackComplete, onSuccess, onError, redirectUrl]);
|
|
961
|
+
const reset = React2.useCallback(() => {
|
|
852
962
|
setValuesState(initialValues);
|
|
853
963
|
setErrors({});
|
|
854
964
|
setStep(1);
|
|
855
965
|
setIsComplete(false);
|
|
966
|
+
setSubmitError(null);
|
|
856
967
|
}, [initialValues]);
|
|
857
968
|
return {
|
|
858
969
|
form,
|
|
859
970
|
isLoading,
|
|
860
971
|
fetchError,
|
|
972
|
+
submitError,
|
|
861
973
|
allFields,
|
|
862
974
|
fields,
|
|
863
975
|
visibleFields,
|
|
@@ -923,13 +1035,13 @@ function DatePicker({
|
|
|
923
1035
|
}) {
|
|
924
1036
|
const today = /* @__PURE__ */ new Date();
|
|
925
1037
|
const todayStr = toDateStr(today.getFullYear(), today.getMonth(), today.getDate());
|
|
926
|
-
const effectiveMin = minDate ||
|
|
1038
|
+
const effectiveMin = minDate || "";
|
|
927
1039
|
const parsed = parseDate(value);
|
|
928
|
-
const [viewYear, setViewYear] =
|
|
929
|
-
const [viewMonth, setViewMonth] =
|
|
930
|
-
const [open, setOpen] =
|
|
931
|
-
const containerRef =
|
|
932
|
-
|
|
1040
|
+
const [viewYear, setViewYear] = React2.useState(parsed?.year || today.getFullYear());
|
|
1041
|
+
const [viewMonth, setViewMonth] = React2.useState(parsed?.month ?? today.getMonth());
|
|
1042
|
+
const [open, setOpen] = React2.useState(false);
|
|
1043
|
+
const containerRef = React2.useRef(null);
|
|
1044
|
+
React2.useEffect(() => {
|
|
933
1045
|
if (!open) return;
|
|
934
1046
|
const handler = (e) => {
|
|
935
1047
|
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
@@ -939,7 +1051,7 @@ function DatePicker({
|
|
|
939
1051
|
document.addEventListener("mousedown", handler);
|
|
940
1052
|
return () => document.removeEventListener("mousedown", handler);
|
|
941
1053
|
}, [open]);
|
|
942
|
-
|
|
1054
|
+
React2.useEffect(() => {
|
|
943
1055
|
if (!open) return;
|
|
944
1056
|
const handler = (e) => {
|
|
945
1057
|
if (e.key === "Escape") setOpen(false);
|
|
@@ -947,32 +1059,32 @@ function DatePicker({
|
|
|
947
1059
|
window.addEventListener("keydown", handler);
|
|
948
1060
|
return () => window.removeEventListener("keydown", handler);
|
|
949
1061
|
}, [open]);
|
|
950
|
-
const prevMonth =
|
|
1062
|
+
const prevMonth = React2.useCallback(() => {
|
|
951
1063
|
if (viewMonth === 0) {
|
|
952
1064
|
setViewMonth(11);
|
|
953
1065
|
setViewYear((y) => y - 1);
|
|
954
1066
|
} else setViewMonth((m) => m - 1);
|
|
955
1067
|
}, [viewMonth]);
|
|
956
|
-
const nextMonth =
|
|
1068
|
+
const nextMonth = React2.useCallback(() => {
|
|
957
1069
|
if (viewMonth === 11) {
|
|
958
1070
|
setViewMonth(0);
|
|
959
1071
|
setViewYear((y) => y + 1);
|
|
960
1072
|
} else setViewMonth((m) => m + 1);
|
|
961
1073
|
}, [viewMonth]);
|
|
962
|
-
const selectDay =
|
|
1074
|
+
const selectDay = React2.useCallback((day) => {
|
|
963
1075
|
const dateStr = toDateStr(viewYear, viewMonth, day);
|
|
964
1076
|
onChange(dateStr);
|
|
965
1077
|
setOpen(false);
|
|
966
1078
|
}, [viewYear, viewMonth, onChange]);
|
|
967
|
-
const isDisabled =
|
|
1079
|
+
const isDisabled = React2.useCallback((day) => {
|
|
968
1080
|
const dateStr = toDateStr(viewYear, viewMonth, day);
|
|
969
1081
|
return dateStr < effectiveMin;
|
|
970
1082
|
}, [viewYear, viewMonth, effectiveMin]);
|
|
971
|
-
const isSelected =
|
|
1083
|
+
const isSelected = React2.useCallback((day) => {
|
|
972
1084
|
if (!value) return false;
|
|
973
1085
|
return toDateStr(viewYear, viewMonth, day) === value;
|
|
974
1086
|
}, [viewYear, viewMonth, value]);
|
|
975
|
-
const isToday =
|
|
1087
|
+
const isToday = React2.useCallback((day) => {
|
|
976
1088
|
return toDateStr(viewYear, viewMonth, day) === todayStr;
|
|
977
1089
|
}, [viewYear, viewMonth, todayStr]);
|
|
978
1090
|
const daysInMonth = getDaysInMonth(viewYear, viewMonth);
|
|
@@ -1349,20 +1461,13 @@ function FormField({ field: field2, value, error, onChange, classPrefix = "sk-fo
|
|
|
1349
1461
|
}
|
|
1350
1462
|
),
|
|
1351
1463
|
field2.field_type === "file" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1352
|
-
|
|
1464
|
+
FileUploadField,
|
|
1353
1465
|
{
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
onChange: (e) => {
|
|
1360
|
-
const file = e.target.files?.[0];
|
|
1361
|
-
if (file) {
|
|
1362
|
-
onChange(file.name);
|
|
1363
|
-
}
|
|
1364
|
-
},
|
|
1365
|
-
style: baseInputStyle
|
|
1466
|
+
field: field2,
|
|
1467
|
+
inputId,
|
|
1468
|
+
classPrefix,
|
|
1469
|
+
baseInputStyle,
|
|
1470
|
+
onChange
|
|
1366
1471
|
}
|
|
1367
1472
|
),
|
|
1368
1473
|
field2.field_type === "rating" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${classPrefix}__rating`, style: { display: "flex", gap: 4 }, children: [1, 2, 3, 4, 5].map((star) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1402,6 +1507,79 @@ function FormField({ field: field2, value, error, onChange, classPrefix = "sk-fo
|
|
|
1402
1507
|
field2.help_text && !error && field2.field_type !== "checkbox" && /* @__PURE__ */ jsxRuntime.jsx("p", { className: `${classPrefix}__help`, style: helpStyle, children: field2.help_text })
|
|
1403
1508
|
] });
|
|
1404
1509
|
}
|
|
1510
|
+
function FileUploadField({
|
|
1511
|
+
field: field2,
|
|
1512
|
+
inputId,
|
|
1513
|
+
classPrefix,
|
|
1514
|
+
baseInputStyle,
|
|
1515
|
+
onChange
|
|
1516
|
+
}) {
|
|
1517
|
+
const [status, setStatus] = React2__default.default.useState("idle");
|
|
1518
|
+
const [fileName, setFileName] = React2__default.default.useState("");
|
|
1519
|
+
const handleFile = async (e) => {
|
|
1520
|
+
const file = e.target.files?.[0];
|
|
1521
|
+
if (!file) return;
|
|
1522
|
+
setFileName(file.name);
|
|
1523
|
+
setStatus("uploading");
|
|
1524
|
+
onChange("");
|
|
1525
|
+
try {
|
|
1526
|
+
const url = await uploadFormFile(file);
|
|
1527
|
+
onChange(url);
|
|
1528
|
+
setStatus("idle");
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
console.error("[Forms] File upload failed:", err);
|
|
1531
|
+
onChange("");
|
|
1532
|
+
setStatus("error");
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1536
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1537
|
+
"input",
|
|
1538
|
+
{
|
|
1539
|
+
id: inputId,
|
|
1540
|
+
className: `${classPrefix}__input ${classPrefix}__input--file`,
|
|
1541
|
+
type: "file",
|
|
1542
|
+
name: field2.slug,
|
|
1543
|
+
required: field2.is_required,
|
|
1544
|
+
disabled: status === "uploading",
|
|
1545
|
+
onChange: handleFile,
|
|
1546
|
+
style: baseInputStyle
|
|
1547
|
+
}
|
|
1548
|
+
),
|
|
1549
|
+
status === "uploading" && /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { color: "var(--sk-text-tertiary, #6b7280)", fontSize: 14, marginTop: 4 }, children: [
|
|
1550
|
+
"Uploading ",
|
|
1551
|
+
fileName,
|
|
1552
|
+
"\u2026"
|
|
1553
|
+
] }),
|
|
1554
|
+
status === "error" && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", style: { color: "var(--sk-error, #ef4444)", fontSize: 14, marginTop: 4 }, children: "Upload failed. Please choose the file again." })
|
|
1555
|
+
] });
|
|
1556
|
+
}
|
|
1557
|
+
async function uploadFormFile(file) {
|
|
1558
|
+
const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.sonor.io" : "https://api.sonor.io";
|
|
1559
|
+
const apiKey = typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ : void 0;
|
|
1560
|
+
if (!apiKey) {
|
|
1561
|
+
throw new Error("API key is required to upload files.");
|
|
1562
|
+
}
|
|
1563
|
+
const body = new FormData();
|
|
1564
|
+
body.append("file", file);
|
|
1565
|
+
const response = await fetchWithRetry(
|
|
1566
|
+
`${apiUrl}/api/public/forms/upload`,
|
|
1567
|
+
{
|
|
1568
|
+
method: "POST",
|
|
1569
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1570
|
+
body
|
|
1571
|
+
},
|
|
1572
|
+
{ retries: 1, timeoutMs: 6e4 }
|
|
1573
|
+
);
|
|
1574
|
+
if (!response.ok) {
|
|
1575
|
+
throw new Error(`Upload failed (${response.status})`);
|
|
1576
|
+
}
|
|
1577
|
+
const data = await response.json().catch(() => null);
|
|
1578
|
+
if (!data?.url) {
|
|
1579
|
+
throw new Error("Upload did not return a URL.");
|
|
1580
|
+
}
|
|
1581
|
+
return data.url;
|
|
1582
|
+
}
|
|
1405
1583
|
function FormClient({
|
|
1406
1584
|
config: config2,
|
|
1407
1585
|
className,
|
|
@@ -1409,19 +1587,21 @@ function FormClient({
|
|
|
1409
1587
|
onError,
|
|
1410
1588
|
customRender
|
|
1411
1589
|
}) {
|
|
1412
|
-
const [values, setValues] =
|
|
1413
|
-
const [errors, setErrors] =
|
|
1414
|
-
const [step, setStep] =
|
|
1415
|
-
const [isSubmitting, setIsSubmitting] =
|
|
1416
|
-
const [isComplete, setIsComplete] =
|
|
1417
|
-
const [
|
|
1418
|
-
const
|
|
1419
|
-
const
|
|
1590
|
+
const [values, setValues] = React2.useState({});
|
|
1591
|
+
const [errors, setErrors] = React2.useState({});
|
|
1592
|
+
const [step, setStep] = React2.useState(1);
|
|
1593
|
+
const [isSubmitting, setIsSubmitting] = React2.useState(false);
|
|
1594
|
+
const [isComplete, setIsComplete] = React2.useState(false);
|
|
1595
|
+
const [submitError, setSubmitError] = React2.useState(null);
|
|
1596
|
+
const [honeypotValue, setHoneypotValue] = React2.useState("");
|
|
1597
|
+
const honeypotFieldName = config2.honeypot_field || DEFAULT_HONEYPOT_FIELD;
|
|
1598
|
+
const formLoadedAt = React2.useRef(Date.now());
|
|
1599
|
+
const isSubmittingRef = React2.useRef(false);
|
|
1420
1600
|
const { trackStepChange, trackComplete } = useFormTracking({
|
|
1421
1601
|
formId: config2.id,
|
|
1422
1602
|
totalSteps: config2.total_steps
|
|
1423
1603
|
});
|
|
1424
|
-
const currentFields =
|
|
1604
|
+
const currentFields = React2.useMemo(() => {
|
|
1425
1605
|
if (!config2.is_multi_step) {
|
|
1426
1606
|
return config2.fields || [];
|
|
1427
1607
|
}
|
|
@@ -1429,7 +1609,7 @@ function FormClient({
|
|
|
1429
1609
|
if (!currentStepConfig) return config2.fields || [];
|
|
1430
1610
|
return (config2.fields || []).filter((f) => f.step_id === currentStepConfig.id);
|
|
1431
1611
|
}, [config2, step]);
|
|
1432
|
-
const isFieldVisible =
|
|
1612
|
+
const isFieldVisible = React2.useCallback((field2) => {
|
|
1433
1613
|
if (!field2.conditional?.show_when) return true;
|
|
1434
1614
|
const { field: condField, operator, value } = field2.conditional.show_when;
|
|
1435
1615
|
const fieldValue = values[condField];
|
|
@@ -1454,7 +1634,7 @@ function FormClient({
|
|
|
1454
1634
|
return true;
|
|
1455
1635
|
}
|
|
1456
1636
|
}, [values]);
|
|
1457
|
-
const validateField =
|
|
1637
|
+
const validateField = React2.useCallback((field2) => {
|
|
1458
1638
|
if (!isFieldVisible(field2)) return null;
|
|
1459
1639
|
if (["heading", "section_header", "paragraph", "hidden"].includes(field2.field_type)) return null;
|
|
1460
1640
|
const value = values[field2.slug];
|
|
@@ -1476,8 +1656,13 @@ function FormClient({
|
|
|
1476
1656
|
if (rules.max !== void 0 && Number(value) > rules.max) {
|
|
1477
1657
|
return `${field2.label} must be no more than ${rules.max}`;
|
|
1478
1658
|
}
|
|
1479
|
-
if (rules.pattern
|
|
1480
|
-
|
|
1659
|
+
if (rules.pattern) {
|
|
1660
|
+
try {
|
|
1661
|
+
if (!new RegExp(rules.pattern).test(strValue)) {
|
|
1662
|
+
return rules.custom_error || `${field2.label} is invalid`;
|
|
1663
|
+
}
|
|
1664
|
+
} catch {
|
|
1665
|
+
}
|
|
1481
1666
|
}
|
|
1482
1667
|
if (field2.field_type === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(strValue)) {
|
|
1483
1668
|
return "Please enter a valid email address";
|
|
@@ -1487,7 +1672,7 @@ function FormClient({
|
|
|
1487
1672
|
}
|
|
1488
1673
|
return null;
|
|
1489
1674
|
}, [values, isFieldVisible]);
|
|
1490
|
-
const validateStep =
|
|
1675
|
+
const validateStep = React2.useCallback(() => {
|
|
1491
1676
|
const newErrors = {};
|
|
1492
1677
|
for (const field2 of currentFields) {
|
|
1493
1678
|
const error = validateField(field2);
|
|
@@ -1498,7 +1683,36 @@ function FormClient({
|
|
|
1498
1683
|
setErrors(newErrors);
|
|
1499
1684
|
return Object.keys(newErrors).length === 0;
|
|
1500
1685
|
}, [currentFields, validateField]);
|
|
1501
|
-
const
|
|
1686
|
+
const validateAll = React2.useCallback(() => {
|
|
1687
|
+
const allFields = config2.fields || [];
|
|
1688
|
+
const newErrors = {};
|
|
1689
|
+
for (const field2 of allFields) {
|
|
1690
|
+
const error = validateField(field2);
|
|
1691
|
+
if (error) newErrors[field2.slug] = error;
|
|
1692
|
+
}
|
|
1693
|
+
setErrors(newErrors);
|
|
1694
|
+
if (Object.keys(newErrors).length === 0) return { ok: true, firstErrorStep: null };
|
|
1695
|
+
let firstErrorStep = null;
|
|
1696
|
+
if (config2.is_multi_step && config2.steps) {
|
|
1697
|
+
const ordered = [...config2.steps].sort((a, b) => a.step_number - b.step_number);
|
|
1698
|
+
for (const s of ordered) {
|
|
1699
|
+
if (allFields.some((f) => f.step_id === s.id && newErrors[f.slug])) {
|
|
1700
|
+
firstErrorStep = s.step_number;
|
|
1701
|
+
break;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return { ok: false, firstErrorStep };
|
|
1706
|
+
}, [config2, validateField]);
|
|
1707
|
+
const buildVisibleValues = React2.useCallback(() => {
|
|
1708
|
+
const out = {};
|
|
1709
|
+
for (const field2 of config2.fields || []) {
|
|
1710
|
+
if (!isFieldVisible(field2)) continue;
|
|
1711
|
+
if (field2.slug in values) out[field2.slug] = values[field2.slug];
|
|
1712
|
+
}
|
|
1713
|
+
return out;
|
|
1714
|
+
}, [config2, values, isFieldVisible]);
|
|
1715
|
+
const setFieldValue = React2.useCallback((key, value) => {
|
|
1502
1716
|
setValues((prev) => ({ ...prev, [key]: value }));
|
|
1503
1717
|
if (errors[key]) {
|
|
1504
1718
|
setErrors((prev) => {
|
|
@@ -1508,7 +1722,7 @@ function FormClient({
|
|
|
1508
1722
|
});
|
|
1509
1723
|
}
|
|
1510
1724
|
}, [errors]);
|
|
1511
|
-
const nextStep =
|
|
1725
|
+
const nextStep = React2.useCallback(() => {
|
|
1512
1726
|
if (!validateStep()) return false;
|
|
1513
1727
|
if (step < config2.total_steps) {
|
|
1514
1728
|
const newStep = step + 1;
|
|
@@ -1518,67 +1732,36 @@ function FormClient({
|
|
|
1518
1732
|
}
|
|
1519
1733
|
return false;
|
|
1520
1734
|
}, [step, config2.total_steps, validateStep, trackStepChange]);
|
|
1521
|
-
const prevStep =
|
|
1735
|
+
const prevStep = React2.useCallback(() => {
|
|
1522
1736
|
if (step > 1) {
|
|
1523
1737
|
const newStep = step - 1;
|
|
1524
1738
|
setStep(newStep);
|
|
1525
1739
|
trackStepChange(newStep);
|
|
1526
1740
|
}
|
|
1527
1741
|
}, [step, trackStepChange]);
|
|
1528
|
-
const goToStep =
|
|
1742
|
+
const goToStep = React2.useCallback((targetStep) => {
|
|
1529
1743
|
if (targetStep >= 1 && targetStep <= config2.total_steps) {
|
|
1530
1744
|
setStep(targetStep);
|
|
1531
1745
|
trackStepChange(targetStep);
|
|
1532
1746
|
}
|
|
1533
1747
|
}, [config2.total_steps, trackStepChange]);
|
|
1534
|
-
const submit =
|
|
1535
|
-
if (
|
|
1748
|
+
const submit = React2.useCallback(async () => {
|
|
1749
|
+
if (isSubmittingRef.current) return;
|
|
1750
|
+
const { ok, firstErrorStep } = validateAll();
|
|
1751
|
+
if (!ok) {
|
|
1752
|
+
if (firstErrorStep != null) setStep(firstErrorStep);
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
isSubmittingRef.current = true;
|
|
1536
1756
|
setIsSubmitting(true);
|
|
1757
|
+
setSubmitError(null);
|
|
1537
1758
|
try {
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
const recaptchaToken = config2.recaptcha_enabled ? await getRecaptchaToken(config2.recaptcha_site_key) : null;
|
|
1544
|
-
if (config2.recaptcha_enabled && !recaptchaToken) {
|
|
1545
|
-
throw new Error("reCAPTCHA is required but could not be initialized.");
|
|
1546
|
-
}
|
|
1547
|
-
const utm = getUTMParams2();
|
|
1548
|
-
const submission = {
|
|
1549
|
-
form_id: config2.id,
|
|
1550
|
-
project_id: config2.project_id,
|
|
1551
|
-
data: {
|
|
1552
|
-
...values,
|
|
1553
|
-
...config2.honeypot_enabled ? { [honeypotFieldName]: honeypotValue } : {}
|
|
1554
|
-
},
|
|
1555
|
-
routing_type: config2.form_type,
|
|
1556
|
-
status: "new",
|
|
1557
|
-
metadata: {
|
|
1558
|
-
pageUrl: typeof window !== "undefined" ? window.location.href : null,
|
|
1559
|
-
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
1560
|
-
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
|
1561
|
-
sessionId: typeof sessionStorage !== "undefined" ? sessionStorage.getItem("_sk_sid") : null,
|
|
1562
|
-
recaptchaToken: recaptchaToken || void 0,
|
|
1563
|
-
utmSource: utm.utm_source,
|
|
1564
|
-
utmMedium: utm.utm_medium,
|
|
1565
|
-
utmCampaign: utm.utm_campaign,
|
|
1566
|
-
_formLoadedAt: formLoadedAt.current
|
|
1567
|
-
}
|
|
1568
|
-
};
|
|
1569
|
-
const response = await fetch(`${apiUrl}/api/public/forms/submit`, {
|
|
1570
|
-
method: "POST",
|
|
1571
|
-
headers: {
|
|
1572
|
-
"Content-Type": "application/json",
|
|
1573
|
-
"Authorization": `Bearer ${apiKey}`
|
|
1574
|
-
},
|
|
1575
|
-
body: JSON.stringify(submission)
|
|
1759
|
+
const data = await submitForm({
|
|
1760
|
+
config: config2,
|
|
1761
|
+
values: buildVisibleValues(),
|
|
1762
|
+
honeypotValue: config2.honeypot_enabled ? honeypotValue : "",
|
|
1763
|
+
formLoadedAt: formLoadedAt.current
|
|
1576
1764
|
});
|
|
1577
|
-
const data = await response.json().catch(() => null);
|
|
1578
|
-
if (!response.ok) {
|
|
1579
|
-
const msg = (data?.message ?? data?.error ?? (response.statusText || `HTTP ${response.status}`)).trim();
|
|
1580
|
-
throw new Error(msg ? `Failed to submit form: ${msg}` : `Failed to submit form (${response.status})`);
|
|
1581
|
-
}
|
|
1582
1765
|
trackComplete();
|
|
1583
1766
|
setIsComplete(true);
|
|
1584
1767
|
onSuccess?.(data);
|
|
@@ -1587,12 +1770,14 @@ function FormClient({
|
|
|
1587
1770
|
}
|
|
1588
1771
|
} catch (error) {
|
|
1589
1772
|
console.error("[Forms] Submission error:", error);
|
|
1773
|
+
setSubmitError("Something went wrong sending your submission. Please try again.");
|
|
1590
1774
|
onError?.(error);
|
|
1591
1775
|
} finally {
|
|
1776
|
+
isSubmittingRef.current = false;
|
|
1592
1777
|
setIsSubmitting(false);
|
|
1593
1778
|
}
|
|
1594
|
-
}, [config2,
|
|
1595
|
-
const progress =
|
|
1779
|
+
}, [config2, honeypotValue, validateAll, buildVisibleValues, trackComplete, onSuccess, onError]);
|
|
1780
|
+
const progress = React2.useMemo(() => {
|
|
1596
1781
|
return Math.round(step / config2.total_steps * 100);
|
|
1597
1782
|
}, [step, config2.total_steps]);
|
|
1598
1783
|
const renderProps = {
|
|
@@ -1689,6 +1874,15 @@ function FormClient({
|
|
|
1689
1874
|
step > 1 && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "sk-form__btn sk-form__btn--back", onClick: prevStep, children: "Back" }),
|
|
1690
1875
|
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "sk-form__btn sk-form__btn--submit", disabled: isSubmitting, children: isSubmitting ? "Submitting..." : step < config2.total_steps ? "Next" : config2.submit_button_text })
|
|
1691
1876
|
] }),
|
|
1877
|
+
submitError && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1878
|
+
"p",
|
|
1879
|
+
{
|
|
1880
|
+
className: "sk-form__submit-error",
|
|
1881
|
+
role: "alert",
|
|
1882
|
+
style: { color: "var(--sk-error, #ef4444)", fontSize: 14, marginTop: 12 },
|
|
1883
|
+
children: submitError
|
|
1884
|
+
}
|
|
1885
|
+
),
|
|
1692
1886
|
config2.honeypot_enabled && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1693
1887
|
"input",
|
|
1694
1888
|
{
|
|
@@ -1715,16 +1909,6 @@ function FormClient({
|
|
|
1715
1909
|
}
|
|
1716
1910
|
);
|
|
1717
1911
|
}
|
|
1718
|
-
function getUTMParams2() {
|
|
1719
|
-
if (typeof window === "undefined") return {};
|
|
1720
|
-
const params = new URLSearchParams(window.location.search);
|
|
1721
|
-
const utmParams = {};
|
|
1722
|
-
for (const key of ["utm_source", "utm_medium", "utm_campaign"]) {
|
|
1723
|
-
const value = params.get(key);
|
|
1724
|
-
if (value) utmParams[key] = value;
|
|
1725
|
-
}
|
|
1726
|
-
return utmParams;
|
|
1727
|
-
}
|
|
1728
1912
|
function ManagedForm({
|
|
1729
1913
|
formId,
|
|
1730
1914
|
projectId,
|