@form-engine-ts/react 2.7.0 → 2.9.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/index.js CHANGED
@@ -1,3 +1,102 @@
1
+ // src/attempt.ts
2
+ function attemptKey(namespace, formId, formVersion) {
3
+ return `${namespace}:${formId}:v${formVersion}`;
4
+ }
5
+ function browserStorage() {
6
+ if (typeof window === "undefined") return null;
7
+ try {
8
+ return window.localStorage;
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+ function parseAttempt(serialized) {
14
+ try {
15
+ const value = JSON.parse(serialized);
16
+ if (typeof value !== "object" || value === null || !("attemptId" in value) || typeof value.attemptId !== "string" || value.attemptId.length === 0 || !("formId" in value) || typeof value.formId !== "string" || !("formVersion" in value) || typeof value.formVersion !== "number" || !Number.isSafeInteger(value.formVersion) || !("createdAt" in value) || typeof value.createdAt !== "string" || !Number.isFinite(Date.parse(value.createdAt))) {
17
+ return null;
18
+ }
19
+ return {
20
+ attemptId: value.attemptId,
21
+ formId: value.formId,
22
+ formVersion: value.formVersion,
23
+ createdAt: value.createdAt
24
+ };
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ function defaultAttemptId() {
30
+ const randomUuid = globalThis.crypto?.randomUUID;
31
+ if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
32
+ return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
33
+ }
34
+ function createLocalStorageSubmissionAttemptStore(options = {}) {
35
+ const namespace = options.namespace ?? "form_engine_attempt";
36
+ if (namespace.trim().length === 0) throw new TypeError("Attempt namespace must not be empty.");
37
+ const memory = /* @__PURE__ */ new Map();
38
+ const inFlight = /* @__PURE__ */ new Map();
39
+ const get = async (formId, formVersion) => {
40
+ const key = attemptKey(namespace, formId, formVersion);
41
+ const remembered = memory.get(key);
42
+ if (remembered !== void 0) return remembered;
43
+ const storage = browserStorage();
44
+ if (storage === null) return null;
45
+ try {
46
+ const serialized = storage.getItem(key);
47
+ if (serialized === null) return null;
48
+ const attempt = parseAttempt(serialized);
49
+ if (attempt === null || attempt.formId !== formId || attempt.formVersion !== formVersion) return null;
50
+ memory.set(key, attempt);
51
+ return attempt;
52
+ } catch {
53
+ return null;
54
+ }
55
+ };
56
+ return {
57
+ get,
58
+ async getOrCreate(formId, formVersion, idFactory = defaultAttemptId) {
59
+ const key = attemptKey(namespace, formId, formVersion);
60
+ const pending = inFlight.get(key);
61
+ if (pending !== void 0) return pending;
62
+ const create = (async () => {
63
+ const existing = await get(formId, formVersion);
64
+ if (existing !== null) return existing;
65
+ const attemptId = idFactory();
66
+ if (typeof attemptId !== "string" || attemptId.trim().length === 0) {
67
+ throw new TypeError("Attempt idFactory must return a non-empty string.");
68
+ }
69
+ const attempt = {
70
+ attemptId,
71
+ formId,
72
+ formVersion,
73
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
74
+ };
75
+ memory.set(key, attempt);
76
+ try {
77
+ browserStorage()?.setItem(key, JSON.stringify(attempt));
78
+ } catch {
79
+ }
80
+ return attempt;
81
+ })();
82
+ inFlight.set(key, create);
83
+ try {
84
+ return await create;
85
+ } finally {
86
+ inFlight.delete(key);
87
+ }
88
+ },
89
+ async clear(formId, formVersion) {
90
+ const key = attemptKey(namespace, formId, formVersion);
91
+ memory.delete(key);
92
+ try {
93
+ browserStorage()?.removeItem(key);
94
+ } catch {
95
+ }
96
+ }
97
+ };
98
+ }
99
+
1
100
  // src/builder.tsx
2
101
  import {
3
102
  populateSchemaTranslations
@@ -945,6 +1044,9 @@ var FIELD_TYPES = [
945
1044
  ];
946
1045
  var BUILDER_DEFAULTS = {
947
1046
  "builder.formBuilder": "Form builder",
1047
+ "builder.basicSettings": "Basic settings",
1048
+ "builder.formTitle": "Form title",
1049
+ "builder.formDescription": "Form description",
948
1050
  "builder.moveUp": "Move {{title}} up",
949
1051
  "builder.moveDown": "Move {{title}} down",
950
1052
  "builder.delete": "Delete {{title}}",
@@ -1110,7 +1212,7 @@ function FormBuilder({
1110
1212
  }) {
1111
1213
  const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
1112
1214
  const components = GUARDED_COMPONENTS;
1113
- const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextInput } = components;
1215
+ const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextArea, TextInput } = components;
1114
1216
  const ToolbarSlot = slots?.toolbar;
1115
1217
  const FieldEditorSlot = slots?.fieldEditor;
1116
1218
  const OptionEditorSlot = slots?.optionEditor;
@@ -1413,6 +1515,42 @@ function FormBuilder({
1413
1515
  }
1414
1516
  },
1415
1517
  children: /* @__PURE__ */ jsxs(Fieldset, { className: "form-engine-builder__controls", disabled: readOnly, children: [
1518
+ /* @__PURE__ */ jsx(
1519
+ Section,
1520
+ {
1521
+ className: "form-engine-builder__basic-settings",
1522
+ headingId: "builder-basic-settings-heading",
1523
+ title: translate("builder.basicSettings"),
1524
+ children: /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
1525
+ /* @__PURE__ */ jsxs("label", { children: [
1526
+ translate("builder.formTitle"),
1527
+ /* @__PURE__ */ jsx(
1528
+ TextInput,
1529
+ {
1530
+ name: "title",
1531
+ required: true,
1532
+ error: schema.title.trim().length === 0,
1533
+ helperText: schema.title.trim().length === 0 ? translate("builder.required") : "",
1534
+ value: schema.title,
1535
+ onChange: (value) => setSourceText({ kind: "form" }, "title", value)
1536
+ }
1537
+ )
1538
+ ] }),
1539
+ /* @__PURE__ */ jsxs("label", { children: [
1540
+ translate("builder.formDescription"),
1541
+ /* @__PURE__ */ jsx(
1542
+ TextArea,
1543
+ {
1544
+ name: "description",
1545
+ rows: 3,
1546
+ value: schema.description ?? "",
1547
+ onChange: (value) => setSourceText({ kind: "form" }, "description", value)
1548
+ }
1549
+ )
1550
+ ] })
1551
+ ] })
1552
+ }
1553
+ ),
1416
1554
  pagesEnabled ? PagesSlot === void 0 ? /* @__PURE__ */ jsx(
1417
1555
  Section,
1418
1556
  {
@@ -2260,7 +2398,7 @@ function FormProvider({
2260
2398
  setSubmitError(null);
2261
2399
  }, [initialValues]);
2262
2400
  const submit = useCallback2(
2263
- async (beforeSubmit) => {
2401
+ async (beforeSubmit, prepareSubmission) => {
2264
2402
  if (submissionInFlight.current) return { status: "cancelled" };
2265
2403
  const validation = validateAnswers(validSchema, values);
2266
2404
  if (!validation.valid) {
@@ -2281,10 +2419,11 @@ function FormProvider({
2281
2419
  setSubmitStatus("idle");
2282
2420
  return { status: "cancelled" };
2283
2421
  }
2284
- await onSubmit(visibleValues);
2422
+ const submissionValues = prepareSubmission === void 0 ? visibleValues : await prepareSubmission(visibleValues);
2423
+ const response = await onSubmit(submissionValues);
2285
2424
  if (resetOnSuccess) setValues({ ...initialValues });
2286
2425
  setSubmitStatus("success");
2287
- return { status: "success" };
2426
+ return response === void 0 ? { status: "success" } : { status: "success", response };
2288
2427
  } catch (cause) {
2289
2428
  const error = cause instanceof Error ? cause : new Error(String(cause));
2290
2429
  setSubmitError(error);
@@ -2353,10 +2492,14 @@ function useField(fieldId) {
2353
2492
  }
2354
2493
 
2355
2494
  // src/receipt.ts
2495
+ import { useEffect as useEffect2, useMemo as useMemo3, useState as useState3 } from "react";
2496
+ function submissionReceiptQueryKey(formId, formVersion) {
2497
+ return `${formId}:v${formVersion}`;
2498
+ }
2356
2499
  function receiptKey(namespace, formId, formVersion) {
2357
2500
  return `${namespace}:${formId}:v${formVersion}`;
2358
2501
  }
2359
- function browserStorage() {
2502
+ function browserStorage2() {
2360
2503
  if (typeof window === "undefined") return null;
2361
2504
  try {
2362
2505
  return window.localStorage;
@@ -2384,31 +2527,86 @@ function parseReceipt(serialized) {
2384
2527
  function createLocalStorageSubmissionReceiptStore(options = {}) {
2385
2528
  const namespace = options.namespace ?? "form_engine_receipt";
2386
2529
  if (namespace.trim().length === 0) throw new TypeError("Receipt namespace must not be empty.");
2530
+ const get = async (formId, formVersion) => {
2531
+ const storage = browserStorage2();
2532
+ if (storage === null) return null;
2533
+ try {
2534
+ const serialized = storage.getItem(receiptKey(namespace, formId, formVersion));
2535
+ if (serialized === null) return null;
2536
+ const receipt = parseReceipt(serialized);
2537
+ return receipt?.formId === formId && receipt.formVersion === formVersion ? receipt : null;
2538
+ } catch {
2539
+ return null;
2540
+ }
2541
+ };
2387
2542
  return {
2388
- async get(formId, formVersion) {
2389
- const storage = browserStorage();
2390
- if (storage === null) return null;
2391
- try {
2392
- const serialized = storage.getItem(receiptKey(namespace, formId, formVersion));
2393
- if (serialized === null) return null;
2394
- const receipt = parseReceipt(serialized);
2395
- return receipt?.formId === formId && receipt.formVersion === formVersion ? receipt : null;
2396
- } catch {
2397
- return null;
2398
- }
2543
+ get,
2544
+ async getBatch(queries) {
2545
+ const receipts = await Promise.all(queries.map((query) => get(query.formId, query.formVersion)));
2546
+ return new Map(
2547
+ receipts.flatMap(
2548
+ (receipt) => receipt === null ? [] : [[submissionReceiptQueryKey(receipt.formId, receipt.formVersion), receipt]]
2549
+ )
2550
+ );
2399
2551
  },
2400
2552
  async save(receipt) {
2401
- const storage = browserStorage();
2553
+ const storage = browserStorage2();
2402
2554
  if (storage === null) return;
2403
2555
  storage.setItem(receiptKey(namespace, receipt.formId, receipt.formVersion), JSON.stringify(receipt));
2404
2556
  },
2405
2557
  async remove(formId, formVersion) {
2406
- const storage = browserStorage();
2558
+ const storage = browserStorage2();
2407
2559
  if (storage === null) return;
2408
2560
  storage.removeItem(receiptKey(namespace, formId, formVersion));
2409
2561
  }
2410
2562
  };
2411
2563
  }
2564
+ function useSubmissionReceipts(store, queries) {
2565
+ const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
2566
+ const stableQueries = useMemo3(() => {
2567
+ const parsed = JSON.parse(querySignature);
2568
+ if (!Array.isArray(parsed)) return [];
2569
+ return parsed.flatMap(
2570
+ (entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
2571
+ );
2572
+ }, [querySignature]);
2573
+ const [state, setState] = useState3({
2574
+ receipts: /* @__PURE__ */ new Map(),
2575
+ isLoading: stableQueries.length > 0,
2576
+ error: null
2577
+ });
2578
+ useEffect2(() => {
2579
+ let active = true;
2580
+ if (stableQueries.length === 0) {
2581
+ setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
2582
+ return () => {
2583
+ active = false;
2584
+ };
2585
+ }
2586
+ setState((current) => ({ ...current, isLoading: true, error: null }));
2587
+ const load = store.getBatch?.(stableQueries) ?? Promise.all(stableQueries.map((query) => store.get(query.formId, query.formVersion))).then(
2588
+ (receipts) => new Map(
2589
+ receipts.flatMap(
2590
+ (receipt) => receipt === null ? [] : [[submissionReceiptQueryKey(receipt.formId, receipt.formVersion), receipt]]
2591
+ )
2592
+ )
2593
+ );
2594
+ void load.then((receipts) => {
2595
+ if (active) setState({ receipts, isLoading: false, error: null });
2596
+ }).catch((cause) => {
2597
+ if (!active) return;
2598
+ setState({
2599
+ receipts: /* @__PURE__ */ new Map(),
2600
+ isLoading: false,
2601
+ error: cause instanceof Error ? cause : new Error(String(cause))
2602
+ });
2603
+ });
2604
+ return () => {
2605
+ active = false;
2606
+ };
2607
+ }, [stableQueries, store]);
2608
+ return state;
2609
+ }
2412
2610
 
2413
2611
  // src/renderer.tsx
2414
2612
  import {
@@ -2417,11 +2615,11 @@ import {
2417
2615
  } from "@form-engine-ts/core";
2418
2616
  import {
2419
2617
  Fragment as Fragment2,
2420
- useEffect as useEffect2,
2618
+ useEffect as useEffect3,
2421
2619
  useId,
2422
- useMemo as useMemo3,
2620
+ useMemo as useMemo4,
2423
2621
  useRef as useRef2,
2424
- useState as useState3
2622
+ useState as useState4
2425
2623
  } from "react";
2426
2624
  import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2427
2625
  function describedBy(field, error, helpId, errorId) {
@@ -2647,31 +2845,34 @@ function ContextFormRenderer({
2647
2845
  onDraftSave,
2648
2846
  submissionGuards = [],
2649
2847
  receiptStore,
2848
+ attemptStore,
2849
+ onReceiptError,
2650
2850
  slots = {}
2651
2851
  }) {
2652
2852
  const form = useForm();
2653
2853
  const prefix = useId().replace(/:/g, "");
2654
2854
  const formRef = useRef2(null);
2655
2855
  const loadedDraftKey = useRef2(null);
2656
- const [draftRestored, setDraftRestored] = useState3(false);
2657
- const [currentPageIndex, setCurrentPageIndex] = useState3(0);
2658
- const [focusFieldId, setFocusFieldId] = useState3(null);
2659
- const [confirmation, setConfirmation] = useState3(null);
2660
- const [guardMessage, setGuardMessage] = useState3(null);
2661
- const [receipt, setReceipt] = useState3(null);
2662
- const [receiptLoaded, setReceiptLoaded] = useState3(receiptStore === void 0);
2856
+ const [draftRestored, setDraftRestored] = useState4(false);
2857
+ const [currentPageIndex, setCurrentPageIndex] = useState4(0);
2858
+ const [focusFieldId, setFocusFieldId] = useState4(null);
2859
+ const [confirmation, setConfirmation] = useState4(null);
2860
+ const [guardMessage, setGuardMessage] = useState4(null);
2861
+ const [receipt, setReceipt] = useState4(null);
2862
+ const [receiptLoaded, setReceiptLoaded] = useState4(receiptStore === void 0);
2663
2863
  const rendererSubmissionInFlight = useRef2(false);
2664
2864
  const pages = form.schema.pages;
2665
- const visiblePageIndexes = useMemo3(
2865
+ const visiblePageIndexes = useMemo4(
2666
2866
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
2667
2867
  [form.pageVisibility, pages]
2668
2868
  );
2669
2869
  const activePage = pages?.[currentPageIndex];
2670
2870
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
2671
2871
  const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
2872
+ const visibleValues = useMemo4(() => selectVisibleAnswers2(form.schema, form.values), [form.schema, form.values]);
2672
2873
  const submitState = confirmation === null ? form.submitStatus : "confirming";
2673
2874
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
2674
- useEffect2(() => {
2875
+ useEffect3(() => {
2675
2876
  let active = true;
2676
2877
  if (receiptStore === void 0) {
2677
2878
  setReceipt(null);
@@ -2692,14 +2893,14 @@ function ContextFormRenderer({
2692
2893
  active = false;
2693
2894
  };
2694
2895
  }, [form.schema.id, form.schema.version, receiptStore]);
2695
- useEffect2(() => {
2896
+ useEffect3(() => {
2696
2897
  if (pages === void 0 || visiblePageIndexes.length === 0) {
2697
2898
  setCurrentPageIndex(0);
2698
2899
  return;
2699
2900
  }
2700
2901
  if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
2701
2902
  }, [currentPageIndex, pages, visiblePageIndexes]);
2702
- useEffect2(() => {
2903
+ useEffect3(() => {
2703
2904
  if (focusFieldId === null) return;
2704
2905
  const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
2705
2906
  (element) => element.dataset.fieldId === focusFieldId
@@ -2710,7 +2911,7 @@ function ContextFormRenderer({
2710
2911
  setFocusFieldId(null);
2711
2912
  }
2712
2913
  }, [focusFieldId]);
2713
- useEffect2(() => {
2914
+ useEffect3(() => {
2714
2915
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
2715
2916
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
2716
2917
  if (loadedDraftKey.current === loadIdentity) return;
@@ -2722,7 +2923,7 @@ function ContextFormRenderer({
2722
2923
  form.restoreValues(draft.values);
2723
2924
  setDraftRestored(true);
2724
2925
  }, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
2725
- useEffect2(() => {
2926
+ useEffect3(() => {
2726
2927
  if (form.submitStatus === "success") return;
2727
2928
  const timeout = globalThis.setTimeout(() => {
2728
2929
  onDraftSave?.(form.values);
@@ -2755,7 +2956,7 @@ function ContextFormRenderer({
2755
2956
  let confirmationMessage;
2756
2957
  let requiresConfirmation = false;
2757
2958
  for (const guard of guards) {
2758
- const result = await guard(form.schema, selectVisibleAnswers2(form.schema, form.values));
2959
+ const result = await guard(form.schema, visibleValues);
2759
2960
  if (result.status === "allow") continue;
2760
2961
  findings.push(...result.findings);
2761
2962
  if (result.status === "block") {
@@ -2785,7 +2986,7 @@ function ContextFormRenderer({
2785
2986
  try {
2786
2987
  const guardResult = await runSubmissionGuards(submissionGuards);
2787
2988
  if (guardResult.status === "block") {
2788
- setGuardMessage(guardResult.message ?? "Submission blocked because sensitive data was detected.");
2989
+ setGuardMessage(guardResult.message ?? form.translate("form.submissionBlocked"));
2789
2990
  return { status: "cancelled" };
2790
2991
  }
2791
2992
  if (guardResult.status === "confirm") {
@@ -2803,7 +3004,18 @@ function ContextFormRenderer({
2803
3004
  setGuardMessage(null);
2804
3005
  rendererSubmissionInFlight.current = true;
2805
3006
  try {
2806
- const result = await form.submit(beforeSubmit);
3007
+ let submissionAttempt;
3008
+ const result = await form.submit(
3009
+ beforeSubmit,
3010
+ attemptStore === void 0 ? void 0 : async (values) => {
3011
+ submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version);
3012
+ return {
3013
+ ...values,
3014
+ attemptId: submissionAttempt.attemptId,
3015
+ submissionId: submissionAttempt.attemptId
3016
+ };
3017
+ }
3018
+ );
2807
3019
  if (result.status === "invalid") {
2808
3020
  const invalidPageIndex = pages?.findIndex(
2809
3021
  (page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
@@ -2814,13 +3026,30 @@ function ContextFormRenderer({
2814
3026
  }
2815
3027
  if (result.status !== "success") return result;
2816
3028
  if (receiptStore !== void 0) {
3029
+ const response = result.response;
3030
+ const submissionId = response?.submissionId ?? submissionAttempt?.attemptId;
2817
3031
  const storedReceipt = {
2818
3032
  formId: form.schema.id,
2819
3033
  formVersion: form.schema.version,
2820
- submittedAt: (/* @__PURE__ */ new Date()).toISOString()
3034
+ submittedAt: response?.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3035
+ ...submissionId === void 0 ? {} : { submissionId }
2821
3036
  };
2822
- await receiptStore.save(storedReceipt);
2823
- setReceipt(storedReceipt);
3037
+ try {
3038
+ await receiptStore.save(storedReceipt);
3039
+ setReceipt(storedReceipt);
3040
+ } catch (cause) {
3041
+ const error = cause instanceof Error ? cause : new Error(String(cause));
3042
+ try {
3043
+ onReceiptError?.(error, storedReceipt);
3044
+ } catch {
3045
+ }
3046
+ }
3047
+ }
3048
+ if (attemptStore !== void 0 && submissionAttempt !== void 0) {
3049
+ try {
3050
+ await attemptStore.clear(form.schema.id, form.schema.version);
3051
+ } catch {
3052
+ }
2824
3053
  }
2825
3054
  if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
2826
3055
  globalThis.localStorage.removeItem(autoSaveKey);
@@ -2860,8 +3089,8 @@ function ContextFormRenderer({
2860
3089
  receipt,
2861
3090
  ...receiptStore === void 0 ? {} : { onReset: () => void resetReceipt() }
2862
3091
  }) ?? /* @__PURE__ */ jsxs2("div", { role: "status", children: [
2863
- "Already submitted.",
2864
- receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: "Submit another response" })
3092
+ form.translate("form.alreadySubmitted"),
3093
+ receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
2865
3094
  ] }) });
2866
3095
  }
2867
3096
  return /* @__PURE__ */ jsxs2("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
@@ -2930,12 +3159,15 @@ function ContextFormRenderer({
2930
3159
  guardMessage === null ? null : /* @__PURE__ */ jsx3("div", { role: "alert", children: guardMessage }),
2931
3160
  confirmation === null ? null : slots.renderSubmissionConfirmation?.({
2932
3161
  findings: confirmation.findings,
3162
+ message: confirmation.message ?? form.translate("form.confirmSensitiveData"),
3163
+ schema: form.schema,
3164
+ visibleValues,
2933
3165
  onConfirm: confirmSubmission,
2934
3166
  onCancel: cancelSubmission
2935
3167
  }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-submission-confirmation", role: "dialog", "aria-modal": "true", children: [
2936
- /* @__PURE__ */ jsx3("p", { children: confirmation.message ?? "Sensitive data may be included. Confirm before submitting." }),
2937
- /* @__PURE__ */ jsx3("button", { type: "button", onClick: confirmSubmission, children: "Confirm submission" }),
2938
- /* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: "Cancel" })
3168
+ /* @__PURE__ */ jsx3("p", { children: confirmation.message ?? form.translate("form.confirmSensitiveData") }),
3169
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
3170
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
2939
3171
  ] }),
2940
3172
  validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-validation-summary", role: "alert", children: [
2941
3173
  validationIssues.length,
@@ -2992,6 +3224,12 @@ var RENDERER_MESSAGES = {
2992
3224
  "form.next": "Next",
2993
3225
  "form.step": "Step {{current}} / {{total}}",
2994
3226
  "form.draftRestored": "Draft restored",
3227
+ "form.submissionBlocked": "Submission blocked because sensitive data was detected.",
3228
+ "form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
3229
+ "form.confirmSubmission": "Confirm submission",
3230
+ "form.cancelSubmission": "Cancel",
3231
+ "form.alreadySubmitted": "Already submitted.",
3232
+ "form.submitAnother": "Submit another response",
2995
3233
  "validation.required": "This field is required."
2996
3234
  };
2997
3235
  var defaultRendererTranslator = {
@@ -3030,9 +3268,12 @@ export {
3030
3268
  FormBuilder,
3031
3269
  FormProvider,
3032
3270
  FormRenderer,
3271
+ createLocalStorageSubmissionAttemptStore,
3033
3272
  createLocalStorageSubmissionReceiptStore,
3034
3273
  resolveInitialFieldType,
3274
+ submissionReceiptQueryKey,
3035
3275
  useField,
3036
3276
  useForm,
3037
- useFormBuilder
3277
+ useFormBuilder,
3278
+ useSubmissionReceipts
3038
3279
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,8 +42,8 @@
42
42
  "typescript"
43
43
  ],
44
44
  "dependencies": {
45
- "@form-engine-ts/core": "2.7.0",
46
- "@form-engine-ts/privacy": "2.7.0"
45
+ "@form-engine-ts/core": "2.9.0",
46
+ "@form-engine-ts/privacy": "2.9.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",