@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/README.md CHANGED
@@ -123,3 +123,14 @@ be created with the SSR-safe `createLocalStorageSubmissionReceiptStore`; `render
123
123
  state. Text controls forward schema `minLength`, `maxLength`, and `pattern` constraints to the DOM, and
124
124
  `renderCharacterCount` can replace the default count. Guard evaluation, confirmation, receipt persistence, and provider
125
125
  submission share an in-flight lock so rapid clicks cannot submit twice.
126
+
127
+ The Builder basic-settings section edits source `title` and `description` through the same policy-aware action pipeline.
128
+ Submission confirmation slots receive the effective message, localized schema, and visible answers. An `onSubmit` result
129
+ may provide `submissionId` and `submittedAt`, which Renderer copies into its receipt. Receipt stores support `getBatch`,
130
+ and `useSubmissionReceipts` loads multiple form/version receipts for list and dashboard surfaces.
131
+
132
+ Receipt persistence is best-effort: `onReceiptError` observes storage failures while the successful completion screen is
133
+ preserved. Pass an SSR-safe `createLocalStorageSubmissionAttemptStore()` as `attemptStore` to reserve an ID immediately
134
+ before submission. Renderer injects it as `attemptId` and `submissionId`, retains it after a failed request, promotes it
135
+ to the receipt after success, and then clears the attempt. Custom receipt stores may omit `getBatch`; the hook falls back
136
+ to concurrent `get` calls.
package/dist/index.cjs CHANGED
@@ -23,14 +23,116 @@ __export(index_exports, {
23
23
  FormBuilder: () => FormBuilder,
24
24
  FormProvider: () => FormProvider,
25
25
  FormRenderer: () => FormRenderer,
26
+ createLocalStorageSubmissionAttemptStore: () => createLocalStorageSubmissionAttemptStore,
26
27
  createLocalStorageSubmissionReceiptStore: () => createLocalStorageSubmissionReceiptStore,
27
28
  resolveInitialFieldType: () => resolveInitialFieldType,
29
+ submissionReceiptQueryKey: () => submissionReceiptQueryKey,
28
30
  useField: () => useField,
29
31
  useForm: () => useForm,
30
- useFormBuilder: () => useFormBuilder
32
+ useFormBuilder: () => useFormBuilder,
33
+ useSubmissionReceipts: () => useSubmissionReceipts
31
34
  });
32
35
  module.exports = __toCommonJS(index_exports);
33
36
 
37
+ // src/attempt.ts
38
+ function attemptKey(namespace, formId, formVersion) {
39
+ return `${namespace}:${formId}:v${formVersion}`;
40
+ }
41
+ function browserStorage() {
42
+ if (typeof window === "undefined") return null;
43
+ try {
44
+ return window.localStorage;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+ function parseAttempt(serialized) {
50
+ try {
51
+ const value = JSON.parse(serialized);
52
+ 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))) {
53
+ return null;
54
+ }
55
+ return {
56
+ attemptId: value.attemptId,
57
+ formId: value.formId,
58
+ formVersion: value.formVersion,
59
+ createdAt: value.createdAt
60
+ };
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+ function defaultAttemptId() {
66
+ const randomUuid = globalThis.crypto?.randomUUID;
67
+ if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
68
+ return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
69
+ }
70
+ function createLocalStorageSubmissionAttemptStore(options = {}) {
71
+ const namespace = options.namespace ?? "form_engine_attempt";
72
+ if (namespace.trim().length === 0) throw new TypeError("Attempt namespace must not be empty.");
73
+ const memory = /* @__PURE__ */ new Map();
74
+ const inFlight = /* @__PURE__ */ new Map();
75
+ const get = async (formId, formVersion) => {
76
+ const key = attemptKey(namespace, formId, formVersion);
77
+ const remembered = memory.get(key);
78
+ if (remembered !== void 0) return remembered;
79
+ const storage = browserStorage();
80
+ if (storage === null) return null;
81
+ try {
82
+ const serialized = storage.getItem(key);
83
+ if (serialized === null) return null;
84
+ const attempt = parseAttempt(serialized);
85
+ if (attempt === null || attempt.formId !== formId || attempt.formVersion !== formVersion) return null;
86
+ memory.set(key, attempt);
87
+ return attempt;
88
+ } catch {
89
+ return null;
90
+ }
91
+ };
92
+ return {
93
+ get,
94
+ async getOrCreate(formId, formVersion, idFactory = defaultAttemptId) {
95
+ const key = attemptKey(namespace, formId, formVersion);
96
+ const pending = inFlight.get(key);
97
+ if (pending !== void 0) return pending;
98
+ const create = (async () => {
99
+ const existing = await get(formId, formVersion);
100
+ if (existing !== null) return existing;
101
+ const attemptId = idFactory();
102
+ if (typeof attemptId !== "string" || attemptId.trim().length === 0) {
103
+ throw new TypeError("Attempt idFactory must return a non-empty string.");
104
+ }
105
+ const attempt = {
106
+ attemptId,
107
+ formId,
108
+ formVersion,
109
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
110
+ };
111
+ memory.set(key, attempt);
112
+ try {
113
+ browserStorage()?.setItem(key, JSON.stringify(attempt));
114
+ } catch {
115
+ }
116
+ return attempt;
117
+ })();
118
+ inFlight.set(key, create);
119
+ try {
120
+ return await create;
121
+ } finally {
122
+ inFlight.delete(key);
123
+ }
124
+ },
125
+ async clear(formId, formVersion) {
126
+ const key = attemptKey(namespace, formId, formVersion);
127
+ memory.delete(key);
128
+ try {
129
+ browserStorage()?.removeItem(key);
130
+ } catch {
131
+ }
132
+ }
133
+ };
134
+ }
135
+
34
136
  // src/builder.tsx
35
137
  var import_core2 = require("@form-engine-ts/core");
36
138
  var import_react2 = require("react");
@@ -972,6 +1074,9 @@ var FIELD_TYPES = [
972
1074
  ];
973
1075
  var BUILDER_DEFAULTS = {
974
1076
  "builder.formBuilder": "Form builder",
1077
+ "builder.basicSettings": "Basic settings",
1078
+ "builder.formTitle": "Form title",
1079
+ "builder.formDescription": "Form description",
975
1080
  "builder.moveUp": "Move {{title}} up",
976
1081
  "builder.moveDown": "Move {{title}} down",
977
1082
  "builder.delete": "Delete {{title}}",
@@ -1137,7 +1242,7 @@ function FormBuilder({
1137
1242
  }) {
1138
1243
  const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
1139
1244
  const components = GUARDED_COMPONENTS;
1140
- const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextInput } = components;
1245
+ const { Button, Checkbox, ErrorMessage, Fieldset, IconButton, Section, Select, TextArea, TextInput } = components;
1141
1246
  const ToolbarSlot = slots?.toolbar;
1142
1247
  const FieldEditorSlot = slots?.fieldEditor;
1143
1248
  const OptionEditorSlot = slots?.optionEditor;
@@ -1440,6 +1545,42 @@ function FormBuilder({
1440
1545
  }
1441
1546
  },
1442
1547
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Fieldset, { className: "form-engine-builder__controls", disabled: readOnly, children: [
1548
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1549
+ Section,
1550
+ {
1551
+ className: "form-engine-builder__basic-settings",
1552
+ headingId: "builder-basic-settings-heading",
1553
+ title: translate("builder.basicSettings"),
1554
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__grid", children: [
1555
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
1556
+ translate("builder.formTitle"),
1557
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1558
+ TextInput,
1559
+ {
1560
+ name: "title",
1561
+ required: true,
1562
+ error: schema.title.trim().length === 0,
1563
+ helperText: schema.title.trim().length === 0 ? translate("builder.required") : "",
1564
+ value: schema.title,
1565
+ onChange: (value) => setSourceText({ kind: "form" }, "title", value)
1566
+ }
1567
+ )
1568
+ ] }),
1569
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
1570
+ translate("builder.formDescription"),
1571
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1572
+ TextArea,
1573
+ {
1574
+ name: "description",
1575
+ rows: 3,
1576
+ value: schema.description ?? "",
1577
+ onChange: (value) => setSourceText({ kind: "form" }, "description", value)
1578
+ }
1579
+ )
1580
+ ] })
1581
+ ] })
1582
+ }
1583
+ ),
1443
1584
  pagesEnabled ? PagesSlot === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1444
1585
  Section,
1445
1586
  {
@@ -2279,7 +2420,7 @@ function FormProvider({
2279
2420
  setSubmitError(null);
2280
2421
  }, [initialValues]);
2281
2422
  const submit = (0, import_react3.useCallback)(
2282
- async (beforeSubmit) => {
2423
+ async (beforeSubmit, prepareSubmission) => {
2283
2424
  if (submissionInFlight.current) return { status: "cancelled" };
2284
2425
  const validation = (0, import_core3.validateAnswers)(validSchema, values);
2285
2426
  if (!validation.valid) {
@@ -2300,10 +2441,11 @@ function FormProvider({
2300
2441
  setSubmitStatus("idle");
2301
2442
  return { status: "cancelled" };
2302
2443
  }
2303
- await onSubmit(visibleValues);
2444
+ const submissionValues = prepareSubmission === void 0 ? visibleValues : await prepareSubmission(visibleValues);
2445
+ const response = await onSubmit(submissionValues);
2304
2446
  if (resetOnSuccess) setValues({ ...initialValues });
2305
2447
  setSubmitStatus("success");
2306
- return { status: "success" };
2448
+ return response === void 0 ? { status: "success" } : { status: "success", response };
2307
2449
  } catch (cause) {
2308
2450
  const error = cause instanceof Error ? cause : new Error(String(cause));
2309
2451
  setSubmitError(error);
@@ -2372,10 +2514,14 @@ function useField(fieldId) {
2372
2514
  }
2373
2515
 
2374
2516
  // src/receipt.ts
2517
+ var import_react4 = require("react");
2518
+ function submissionReceiptQueryKey(formId, formVersion) {
2519
+ return `${formId}:v${formVersion}`;
2520
+ }
2375
2521
  function receiptKey(namespace, formId, formVersion) {
2376
2522
  return `${namespace}:${formId}:v${formVersion}`;
2377
2523
  }
2378
- function browserStorage() {
2524
+ function browserStorage2() {
2379
2525
  if (typeof window === "undefined") return null;
2380
2526
  try {
2381
2527
  return window.localStorage;
@@ -2403,35 +2549,90 @@ function parseReceipt(serialized) {
2403
2549
  function createLocalStorageSubmissionReceiptStore(options = {}) {
2404
2550
  const namespace = options.namespace ?? "form_engine_receipt";
2405
2551
  if (namespace.trim().length === 0) throw new TypeError("Receipt namespace must not be empty.");
2552
+ const get = async (formId, formVersion) => {
2553
+ const storage = browserStorage2();
2554
+ if (storage === null) return null;
2555
+ try {
2556
+ const serialized = storage.getItem(receiptKey(namespace, formId, formVersion));
2557
+ if (serialized === null) return null;
2558
+ const receipt = parseReceipt(serialized);
2559
+ return receipt?.formId === formId && receipt.formVersion === formVersion ? receipt : null;
2560
+ } catch {
2561
+ return null;
2562
+ }
2563
+ };
2406
2564
  return {
2407
- async get(formId, formVersion) {
2408
- const storage = browserStorage();
2409
- if (storage === null) return null;
2410
- try {
2411
- const serialized = storage.getItem(receiptKey(namespace, formId, formVersion));
2412
- if (serialized === null) return null;
2413
- const receipt = parseReceipt(serialized);
2414
- return receipt?.formId === formId && receipt.formVersion === formVersion ? receipt : null;
2415
- } catch {
2416
- return null;
2417
- }
2565
+ get,
2566
+ async getBatch(queries) {
2567
+ const receipts = await Promise.all(queries.map((query) => get(query.formId, query.formVersion)));
2568
+ return new Map(
2569
+ receipts.flatMap(
2570
+ (receipt) => receipt === null ? [] : [[submissionReceiptQueryKey(receipt.formId, receipt.formVersion), receipt]]
2571
+ )
2572
+ );
2418
2573
  },
2419
2574
  async save(receipt) {
2420
- const storage = browserStorage();
2575
+ const storage = browserStorage2();
2421
2576
  if (storage === null) return;
2422
2577
  storage.setItem(receiptKey(namespace, receipt.formId, receipt.formVersion), JSON.stringify(receipt));
2423
2578
  },
2424
2579
  async remove(formId, formVersion) {
2425
- const storage = browserStorage();
2580
+ const storage = browserStorage2();
2426
2581
  if (storage === null) return;
2427
2582
  storage.removeItem(receiptKey(namespace, formId, formVersion));
2428
2583
  }
2429
2584
  };
2430
2585
  }
2586
+ function useSubmissionReceipts(store, queries) {
2587
+ const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
2588
+ const stableQueries = (0, import_react4.useMemo)(() => {
2589
+ const parsed = JSON.parse(querySignature);
2590
+ if (!Array.isArray(parsed)) return [];
2591
+ return parsed.flatMap(
2592
+ (entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
2593
+ );
2594
+ }, [querySignature]);
2595
+ const [state, setState] = (0, import_react4.useState)({
2596
+ receipts: /* @__PURE__ */ new Map(),
2597
+ isLoading: stableQueries.length > 0,
2598
+ error: null
2599
+ });
2600
+ (0, import_react4.useEffect)(() => {
2601
+ let active = true;
2602
+ if (stableQueries.length === 0) {
2603
+ setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
2604
+ return () => {
2605
+ active = false;
2606
+ };
2607
+ }
2608
+ setState((current) => ({ ...current, isLoading: true, error: null }));
2609
+ const load = store.getBatch?.(stableQueries) ?? Promise.all(stableQueries.map((query) => store.get(query.formId, query.formVersion))).then(
2610
+ (receipts) => new Map(
2611
+ receipts.flatMap(
2612
+ (receipt) => receipt === null ? [] : [[submissionReceiptQueryKey(receipt.formId, receipt.formVersion), receipt]]
2613
+ )
2614
+ )
2615
+ );
2616
+ void load.then((receipts) => {
2617
+ if (active) setState({ receipts, isLoading: false, error: null });
2618
+ }).catch((cause) => {
2619
+ if (!active) return;
2620
+ setState({
2621
+ receipts: /* @__PURE__ */ new Map(),
2622
+ isLoading: false,
2623
+ error: cause instanceof Error ? cause : new Error(String(cause))
2624
+ });
2625
+ });
2626
+ return () => {
2627
+ active = false;
2628
+ };
2629
+ }, [stableQueries, store]);
2630
+ return state;
2631
+ }
2431
2632
 
2432
2633
  // src/renderer.tsx
2433
2634
  var import_core4 = require("@form-engine-ts/core");
2434
- var import_react4 = require("react");
2635
+ var import_react5 = require("react");
2435
2636
  var import_jsx_runtime3 = require("react/jsx-runtime");
2436
2637
  function describedBy(field, error, helpId, errorId) {
2437
2638
  const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
@@ -2656,31 +2857,34 @@ function ContextFormRenderer({
2656
2857
  onDraftSave,
2657
2858
  submissionGuards = [],
2658
2859
  receiptStore,
2860
+ attemptStore,
2861
+ onReceiptError,
2659
2862
  slots = {}
2660
2863
  }) {
2661
2864
  const form = useForm();
2662
- const prefix = (0, import_react4.useId)().replace(/:/g, "");
2663
- const formRef = (0, import_react4.useRef)(null);
2664
- const loadedDraftKey = (0, import_react4.useRef)(null);
2665
- const [draftRestored, setDraftRestored] = (0, import_react4.useState)(false);
2666
- const [currentPageIndex, setCurrentPageIndex] = (0, import_react4.useState)(0);
2667
- const [focusFieldId, setFocusFieldId] = (0, import_react4.useState)(null);
2668
- const [confirmation, setConfirmation] = (0, import_react4.useState)(null);
2669
- const [guardMessage, setGuardMessage] = (0, import_react4.useState)(null);
2670
- const [receipt, setReceipt] = (0, import_react4.useState)(null);
2671
- const [receiptLoaded, setReceiptLoaded] = (0, import_react4.useState)(receiptStore === void 0);
2672
- const rendererSubmissionInFlight = (0, import_react4.useRef)(false);
2865
+ const prefix = (0, import_react5.useId)().replace(/:/g, "");
2866
+ const formRef = (0, import_react5.useRef)(null);
2867
+ const loadedDraftKey = (0, import_react5.useRef)(null);
2868
+ const [draftRestored, setDraftRestored] = (0, import_react5.useState)(false);
2869
+ const [currentPageIndex, setCurrentPageIndex] = (0, import_react5.useState)(0);
2870
+ const [focusFieldId, setFocusFieldId] = (0, import_react5.useState)(null);
2871
+ const [confirmation, setConfirmation] = (0, import_react5.useState)(null);
2872
+ const [guardMessage, setGuardMessage] = (0, import_react5.useState)(null);
2873
+ const [receipt, setReceipt] = (0, import_react5.useState)(null);
2874
+ const [receiptLoaded, setReceiptLoaded] = (0, import_react5.useState)(receiptStore === void 0);
2875
+ const rendererSubmissionInFlight = (0, import_react5.useRef)(false);
2673
2876
  const pages = form.schema.pages;
2674
- const visiblePageIndexes = (0, import_react4.useMemo)(
2877
+ const visiblePageIndexes = (0, import_react5.useMemo)(
2675
2878
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
2676
2879
  [form.pageVisibility, pages]
2677
2880
  );
2678
2881
  const activePage = pages?.[currentPageIndex];
2679
2882
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
2680
2883
  const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
2884
+ const visibleValues = (0, import_react5.useMemo)(() => (0, import_core4.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
2681
2885
  const submitState = confirmation === null ? form.submitStatus : "confirming";
2682
2886
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
2683
- (0, import_react4.useEffect)(() => {
2887
+ (0, import_react5.useEffect)(() => {
2684
2888
  let active = true;
2685
2889
  if (receiptStore === void 0) {
2686
2890
  setReceipt(null);
@@ -2701,14 +2905,14 @@ function ContextFormRenderer({
2701
2905
  active = false;
2702
2906
  };
2703
2907
  }, [form.schema.id, form.schema.version, receiptStore]);
2704
- (0, import_react4.useEffect)(() => {
2908
+ (0, import_react5.useEffect)(() => {
2705
2909
  if (pages === void 0 || visiblePageIndexes.length === 0) {
2706
2910
  setCurrentPageIndex(0);
2707
2911
  return;
2708
2912
  }
2709
2913
  if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
2710
2914
  }, [currentPageIndex, pages, visiblePageIndexes]);
2711
- (0, import_react4.useEffect)(() => {
2915
+ (0, import_react5.useEffect)(() => {
2712
2916
  if (focusFieldId === null) return;
2713
2917
  const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
2714
2918
  (element) => element.dataset.fieldId === focusFieldId
@@ -2719,7 +2923,7 @@ function ContextFormRenderer({
2719
2923
  setFocusFieldId(null);
2720
2924
  }
2721
2925
  }, [focusFieldId]);
2722
- (0, import_react4.useEffect)(() => {
2926
+ (0, import_react5.useEffect)(() => {
2723
2927
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
2724
2928
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
2725
2929
  if (loadedDraftKey.current === loadIdentity) return;
@@ -2731,7 +2935,7 @@ function ContextFormRenderer({
2731
2935
  form.restoreValues(draft.values);
2732
2936
  setDraftRestored(true);
2733
2937
  }, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
2734
- (0, import_react4.useEffect)(() => {
2938
+ (0, import_react5.useEffect)(() => {
2735
2939
  if (form.submitStatus === "success") return;
2736
2940
  const timeout = globalThis.setTimeout(() => {
2737
2941
  onDraftSave?.(form.values);
@@ -2764,7 +2968,7 @@ function ContextFormRenderer({
2764
2968
  let confirmationMessage;
2765
2969
  let requiresConfirmation = false;
2766
2970
  for (const guard of guards) {
2767
- const result = await guard(form.schema, (0, import_core4.selectVisibleAnswers)(form.schema, form.values));
2971
+ const result = await guard(form.schema, visibleValues);
2768
2972
  if (result.status === "allow") continue;
2769
2973
  findings.push(...result.findings);
2770
2974
  if (result.status === "block") {
@@ -2794,7 +2998,7 @@ function ContextFormRenderer({
2794
2998
  try {
2795
2999
  const guardResult = await runSubmissionGuards(submissionGuards);
2796
3000
  if (guardResult.status === "block") {
2797
- setGuardMessage(guardResult.message ?? "Submission blocked because sensitive data was detected.");
3001
+ setGuardMessage(guardResult.message ?? form.translate("form.submissionBlocked"));
2798
3002
  return { status: "cancelled" };
2799
3003
  }
2800
3004
  if (guardResult.status === "confirm") {
@@ -2812,7 +3016,18 @@ function ContextFormRenderer({
2812
3016
  setGuardMessage(null);
2813
3017
  rendererSubmissionInFlight.current = true;
2814
3018
  try {
2815
- const result = await form.submit(beforeSubmit);
3019
+ let submissionAttempt;
3020
+ const result = await form.submit(
3021
+ beforeSubmit,
3022
+ attemptStore === void 0 ? void 0 : async (values) => {
3023
+ submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version);
3024
+ return {
3025
+ ...values,
3026
+ attemptId: submissionAttempt.attemptId,
3027
+ submissionId: submissionAttempt.attemptId
3028
+ };
3029
+ }
3030
+ );
2816
3031
  if (result.status === "invalid") {
2817
3032
  const invalidPageIndex = pages?.findIndex(
2818
3033
  (page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
@@ -2823,13 +3038,30 @@ function ContextFormRenderer({
2823
3038
  }
2824
3039
  if (result.status !== "success") return result;
2825
3040
  if (receiptStore !== void 0) {
3041
+ const response = result.response;
3042
+ const submissionId = response?.submissionId ?? submissionAttempt?.attemptId;
2826
3043
  const storedReceipt = {
2827
3044
  formId: form.schema.id,
2828
3045
  formVersion: form.schema.version,
2829
- submittedAt: (/* @__PURE__ */ new Date()).toISOString()
3046
+ submittedAt: response?.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3047
+ ...submissionId === void 0 ? {} : { submissionId }
2830
3048
  };
2831
- await receiptStore.save(storedReceipt);
2832
- setReceipt(storedReceipt);
3049
+ try {
3050
+ await receiptStore.save(storedReceipt);
3051
+ setReceipt(storedReceipt);
3052
+ } catch (cause) {
3053
+ const error = cause instanceof Error ? cause : new Error(String(cause));
3054
+ try {
3055
+ onReceiptError?.(error, storedReceipt);
3056
+ } catch {
3057
+ }
3058
+ }
3059
+ }
3060
+ if (attemptStore !== void 0 && submissionAttempt !== void 0) {
3061
+ try {
3062
+ await attemptStore.clear(form.schema.id, form.schema.version);
3063
+ } catch {
3064
+ }
2833
3065
  }
2834
3066
  if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
2835
3067
  globalThis.localStorage.removeItem(autoSaveKey);
@@ -2869,8 +3101,8 @@ function ContextFormRenderer({
2869
3101
  receipt,
2870
3102
  ...receiptStore === void 0 ? {} : { onReset: () => void resetReceipt() }
2871
3103
  }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "status", children: [
2872
- "Already submitted.",
2873
- receiptStore === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void resetReceipt(), children: "Submit another response" })
3104
+ form.translate("form.alreadySubmitted"),
3105
+ receiptStore === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
2874
3106
  ] }) });
2875
3107
  }
2876
3108
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
@@ -2924,7 +3156,7 @@ function ContextFormRenderer({
2924
3156
  ...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
2925
3157
  };
2926
3158
  if (slots.renderField !== void 0) {
2927
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react4.Fragment, { children: slots.renderField({
3159
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react5.Fragment, { children: slots.renderField({
2928
3160
  question: field,
2929
3161
  value: form.values[field.id],
2930
3162
  onChange: (value) => {
@@ -2939,12 +3171,15 @@ function ContextFormRenderer({
2939
3171
  guardMessage === null ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: guardMessage }),
2940
3172
  confirmation === null ? null : slots.renderSubmissionConfirmation?.({
2941
3173
  findings: confirmation.findings,
3174
+ message: confirmation.message ?? form.translate("form.confirmSensitiveData"),
3175
+ schema: form.schema,
3176
+ visibleValues,
2942
3177
  onConfirm: confirmSubmission,
2943
3178
  onCancel: cancelSubmission
2944
3179
  }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-submission-confirmation", role: "dialog", "aria-modal": "true", children: [
2945
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation.message ?? "Sensitive data may be included. Confirm before submitting." }),
2946
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: confirmSubmission, children: "Confirm submission" }),
2947
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: "Cancel" })
3180
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation.message ?? form.translate("form.confirmSensitiveData") }),
3181
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
3182
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
2948
3183
  ] }),
2949
3184
  validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-validation-summary", role: "alert", children: [
2950
3185
  validationIssues.length,
@@ -3001,6 +3236,12 @@ var RENDERER_MESSAGES = {
3001
3236
  "form.next": "Next",
3002
3237
  "form.step": "Step {{current}} / {{total}}",
3003
3238
  "form.draftRestored": "Draft restored",
3239
+ "form.submissionBlocked": "Submission blocked because sensitive data was detected.",
3240
+ "form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
3241
+ "form.confirmSubmission": "Confirm submission",
3242
+ "form.cancelSubmission": "Cancel",
3243
+ "form.alreadySubmitted": "Already submitted.",
3244
+ "form.submitAnother": "Submit another response",
3004
3245
  "validation.required": "This field is required."
3005
3246
  };
3006
3247
  var defaultRendererTranslator = {
@@ -3040,9 +3281,12 @@ function FormRenderer(props) {
3040
3281
  FormBuilder,
3041
3282
  FormProvider,
3042
3283
  FormRenderer,
3284
+ createLocalStorageSubmissionAttemptStore,
3043
3285
  createLocalStorageSubmissionReceiptStore,
3044
3286
  resolveInitialFieldType,
3287
+ submissionReceiptQueryKey,
3045
3288
  useField,
3046
3289
  useForm,
3047
- useFormBuilder
3290
+ useFormBuilder,
3291
+ useSubmissionReceipts
3048
3292
  });