@nodii/onboarding 0.1.0-dev.0 → 0.2.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 +124 -0
- package/dist/{chunk-4SWOVHZ2.js → chunk-A3DE2IAH.js} +155 -18
- package/dist/chunk-A3DE2IAH.js.map +1 -0
- package/dist/{chunk-AFCJLUPK.js → chunk-DWFZA5T6.js} +19 -8
- package/dist/chunk-DWFZA5T6.js.map +1 -0
- package/dist/chunk-FIRSA7EH.js +3 -0
- package/dist/{chunk-ZDY2I6KY.js.map → chunk-FIRSA7EH.js.map} +1 -1
- package/dist/chunk-JFDQZ3AC.js +3 -0
- package/dist/chunk-JFDQZ3AC.js.map +1 -0
- package/dist/{chunk-34BQX2KR.js → chunk-KFHVZGX6.js} +61 -6
- package/dist/chunk-KFHVZGX6.js.map +1 -0
- package/dist/chunk-NDSBIB3N.js +99 -0
- package/dist/chunk-NDSBIB3N.js.map +1 -0
- package/dist/chunk-OLWR5COK.js +10 -0
- package/dist/chunk-OLWR5COK.js.map +1 -0
- package/dist/{chunk-SWMIIUXR.js → chunk-OVULPWNZ.js} +917 -158
- package/dist/chunk-OVULPWNZ.js.map +1 -0
- package/dist/chunk-SEKNVHD3.js +59 -0
- package/dist/chunk-SEKNVHD3.js.map +1 -0
- package/dist/chunk-T7EWURTF.js +321 -0
- package/dist/chunk-T7EWURTF.js.map +1 -0
- package/dist/{chunk-BQCTDU5V.js → chunk-UHOFGWI7.js} +90 -15
- package/dist/chunk-UHOFGWI7.js.map +1 -0
- package/dist/chunk-WKN4U7QF.js +35 -0
- package/dist/chunk-WKN4U7QF.js.map +1 -0
- package/dist/{chunk-AW7W3HAC.js → chunk-XJTJPBJB.js} +40 -3
- package/dist/chunk-XJTJPBJB.js.map +1 -0
- package/dist/chunk-ZR6X3GSM.js +186 -0
- package/dist/chunk-ZR6X3GSM.js.map +1 -0
- package/dist/client-NvqPpfQP.d.ts +86 -0
- package/dist/consent/index.d.ts +59 -0
- package/dist/consent/index.js +7 -0
- package/dist/consent/index.js.map +1 -0
- package/dist/controller-BLH3wnxl.d.ts +177 -0
- package/dist/dynamic/index.d.ts +65 -6
- package/dist/dynamic/index.js +5 -2
- package/dist/errors-vJkye4a0.d.ts +110 -0
- package/dist/form/index.d.ts +12 -4
- package/dist/form/index.js +2 -2
- package/dist/index-BMSbQGWP.d.ts +103 -0
- package/dist/index.d.ts +13 -7
- package/dist/index.js +13 -8
- package/dist/model-BI_I-D_V.d.ts +267 -0
- package/dist/{model-IEyDI54w.d.ts → model-CEwvHlWA.d.ts} +212 -46
- package/dist/render-EWQXF73I.js +5 -0
- package/dist/render-EWQXF73I.js.map +1 -0
- package/dist/schema-BrtjpXfL.d.ts +257 -0
- package/dist/{signup-client-Bl6eUrxR.d.ts → signup-client-CS95nkVO.d.ts} +152 -19
- package/dist/spec/index.d.ts +10 -81
- package/dist/spec/index.js +3 -2
- package/dist/submit/index.d.ts +7 -71
- package/dist/submit/index.js +4 -3
- package/dist/viewer/index.d.ts +4 -0
- package/dist/viewer/index.js +5 -0
- package/dist/viewer/index.js.map +1 -0
- package/dist/widget/index.d.ts +102 -20
- package/dist/widget/index.js +10 -7
- package/package.json +21 -2
- package/dist/chunk-34BQX2KR.js.map +0 -1
- package/dist/chunk-4SWOVHZ2.js.map +0 -1
- package/dist/chunk-AFCJLUPK.js.map +0 -1
- package/dist/chunk-AW7W3HAC.js.map +0 -1
- package/dist/chunk-BIFANV7M.js +0 -22
- package/dist/chunk-BIFANV7M.js.map +0 -1
- package/dist/chunk-BQCTDU5V.js.map +0 -1
- package/dist/chunk-SWMIIUXR.js.map +0 -1
- package/dist/chunk-ZDY2I6KY.js +0 -3
- package/dist/schema-TfyBU6JW.d.ts +0 -107
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# @nodii/onboarding
|
|
2
|
+
|
|
3
|
+
Browser SDK for the Nodii self-signup onboarding flow (D540 / D543): headless
|
|
4
|
+
form + consent state, reCAPTCHA integration, submit clients, and default
|
|
5
|
+
themeable vanilla-TS widgets.
|
|
6
|
+
|
|
7
|
+
Subpath exports:
|
|
8
|
+
|
|
9
|
+
- `@nodii/onboarding` — root barrel (most-common items)
|
|
10
|
+
- `@nodii/onboarding/form` — Phase-1 contact-form headless state
|
|
11
|
+
- `@nodii/onboarding/spec` — spec fetch client + D540 model
|
|
12
|
+
- `@nodii/onboarding/dynamic` — spec→Zod derivation + dynamic signup stepper
|
|
13
|
+
- `@nodii/onboarding/consent` — **headless** ConsentPack controller + gates (D543)
|
|
14
|
+
- `@nodii/onboarding/viewer` — **opt-in** default document viewer (DOMPurify /
|
|
15
|
+
marked / PDF.js). Separate subpath so the base bundle stays lean.
|
|
16
|
+
- `@nodii/onboarding/submit` — submit clients + typed error taxonomy
|
|
17
|
+
- `@nodii/onboarding/recaptcha` — reCAPTCHA v3 loader
|
|
18
|
+
- `@nodii/onboarding/widget` — default themeable widgets (contact + signup +
|
|
19
|
+
consent step)
|
|
20
|
+
|
|
21
|
+
## Embedding the lead (contact) form
|
|
22
|
+
|
|
23
|
+
The contact widget renders the D1165 lead fields. Its choices, labels, the
|
|
24
|
+
privacy-notice link and the bot-challenge siteKey come from the onboarder's
|
|
25
|
+
spec, so fetch it first:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { fetchOnboardingSpec } from "@nodii/onboarding/spec";
|
|
29
|
+
import { mountContactForm } from "@nodii/onboarding/widget";
|
|
30
|
+
|
|
31
|
+
const baseUrl = "https://platform.nucleus-cloud.in"; // the onboarder's intake
|
|
32
|
+
const spec = await fetchOnboardingSpec({ baseUrl, onboarder: "nodii" });
|
|
33
|
+
mountContactForm(document.getElementById("contact")!, {
|
|
34
|
+
onboarder: "nodii",
|
|
35
|
+
baseUrl,
|
|
36
|
+
form: "contact",
|
|
37
|
+
spec,
|
|
38
|
+
intakeKind: "enquiry",
|
|
39
|
+
attribution: { button: "book-a-chat" },
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The spec's privacy-notice link is a presigned URL that expires, so an open
|
|
44
|
+
form re-reads the spec every 5 minutes and swaps in the fresh link
|
|
45
|
+
(`specRefreshMs`; 0 turns it off). A `privacyUrl` you pass is never re-read,
|
|
46
|
+
and must be an http(s) URL, absolute or relative: the form refuses to mount
|
|
47
|
+
with anything else.
|
|
48
|
+
|
|
49
|
+
The widget draws a fixed set of fields. Company, contact name and email are
|
|
50
|
+
always required; the spec's `required` flag is honoured for the interest
|
|
51
|
+
choice only; phone, team size and message are optional; the address fields are
|
|
52
|
+
not drawn. To require more, build the form on the headless
|
|
53
|
+
`@nodii/onboarding/form` and `@nodii/onboarding/submit` modules, as the Nodii
|
|
54
|
+
website does.
|
|
55
|
+
|
|
56
|
+
A `scroll_then_accept` PDF renders inline with PDF.js. Its worker loads from
|
|
57
|
+
next to the package when that URL really serves a script; in a bundled page
|
|
58
|
+
(Vite, webpack) it usually doesn't, and the worker comes from jsDelivr instead,
|
|
59
|
+
pinned to the PDF.js version in use. To self-host it, set
|
|
60
|
+
`GlobalWorkerOptions.workerSrc` on `pdfjs-dist` before the first render: the
|
|
61
|
+
viewer leaves a configured worker alone.
|
|
62
|
+
|
|
63
|
+
When the spec carries no `botChallenge` (D1444), no reCAPTCHA script loads and
|
|
64
|
+
no token is sent. The page's origin must be registered for the onboarder
|
|
65
|
+
(the tenant CORS writer) or the browser refuses the intake's responses.
|
|
66
|
+
|
|
67
|
+
## Tests
|
|
68
|
+
|
|
69
|
+
### Fast unit + happy-dom (default)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bun run test # bun:test — headless controller, submit clients, widgets
|
|
73
|
+
# (happy-dom), spec model, bundle discipline, no-NUL guard
|
|
74
|
+
bun run typecheck # tsc --noEmit (src)
|
|
75
|
+
bun run lint # bun lint + bun fmt check
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Browser tests (Playwright / real Chromium) — COMPLIANCE AUTHORITY
|
|
79
|
+
|
|
80
|
+
The D543 consent scroll-gate, the PDF.js worker load, and DOMPurify-in-a-real-
|
|
81
|
+
DOM are **layout-dependent** and cannot be validated under happy-dom (no layout
|
|
82
|
+
engine). The Playwright spec in `browser-tests/` is the **authority** for those
|
|
83
|
+
compliance paths — it renders the real SDK bundle in Chromium against genuine
|
|
84
|
+
layout and proves:
|
|
85
|
+
|
|
86
|
+
1. **scroll gate** — a long doc's gate is CLOSED before scroll and OPENS only
|
|
87
|
+
after a real scroll-to-end (real geometry); `buildConsentPayload` throws
|
|
88
|
+
before / succeeds (with `scrolled_to_end`) after.
|
|
89
|
+
2. **P0a fabrication guard** — a short doc satisfies only WITH content present;
|
|
90
|
+
an empty/pre-render box never satisfies.
|
|
91
|
+
3. **PDF worker** — the worker is actually FETCHED + loaded (no pre-set
|
|
92
|
+
`workerSrc`); pages render to `<canvas>`; the scroll gate opens only after a
|
|
93
|
+
real scroll.
|
|
94
|
+
4. **open_then_accept** — the open affordance satisfies `opened` and only `opened`.
|
|
95
|
+
5. **DOMPurify** — `<iframe>` / `<script>` / `<object>` are stripped in a real
|
|
96
|
+
browser (closes the "DOMPurify may be inert under bun+happy-dom" gap).
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# One-time (and in CI): install the Chromium browser binary.
|
|
100
|
+
bunx playwright install chromium
|
|
101
|
+
|
|
102
|
+
# Build the browser bundle + run the Playwright spec.
|
|
103
|
+
bun run test:browser
|
|
104
|
+
|
|
105
|
+
# Typecheck the browser harness + spec.
|
|
106
|
+
bun run typecheck:browser
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**CI implication:** `bun run test:browser` requires a Chromium binary, so a CI
|
|
110
|
+
job that runs it MUST add a `bunx playwright install --with-deps chromium` step
|
|
111
|
+
before it (the browser is NOT downloaded by `bun install`). The default
|
|
112
|
+
`ts-ci.yml` `Test` step runs `bun run test` (happy-dom, fast) per package and
|
|
113
|
+
does NOT run the browser spec; wire the browser spec as a dedicated job/step,
|
|
114
|
+
e.g.:
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
- name: Install Chromium for browser tests
|
|
118
|
+
run: cd ts/onboarding && bunx playwright install --with-deps chromium
|
|
119
|
+
- name: Browser tests (consent compliance authority)
|
|
120
|
+
run: cd ts/onboarding && bun run test:browser
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Playwright build artifacts (`browser-tests/.bundle/`, `test-results/`,
|
|
124
|
+
`playwright-report/`) are gitignored.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { createConsentController } from './chunk-ZR6X3GSM.js';
|
|
2
|
+
import { isSubdomainLabelShape, SUBDOMAIN_SHAPE_MESSAGE } from './chunk-OLWR5COK.js';
|
|
1
3
|
import { z } from 'zod/v3';
|
|
2
4
|
|
|
3
|
-
// src/dynamic/derive.ts
|
|
4
5
|
function optionValue(option) {
|
|
5
6
|
if (option !== null && typeof option === "object") {
|
|
6
7
|
const o = option;
|
|
@@ -144,7 +145,12 @@ function deriveAnswersSchema(steps) {
|
|
|
144
145
|
}
|
|
145
146
|
var credentialsSchema = z.object({
|
|
146
147
|
email: z.string().email("A valid account email is required"),
|
|
147
|
-
|
|
148
|
+
displayName: z.string().trim().min(1, "Your name is required").max(200),
|
|
149
|
+
organisationName: z.string().trim().min(1, "Organisation name is required").max(200),
|
|
150
|
+
subdomain: z.string().trim().min(1, "Workspace subdomain is required").max(63).refine(
|
|
151
|
+
(v) => v === "" || v.length > 63 || isSubdomainLabelShape(v),
|
|
152
|
+
SUBDOMAIN_SHAPE_MESSAGE
|
|
153
|
+
)
|
|
148
154
|
});
|
|
149
155
|
function zodIssuesToFieldErrors(issues) {
|
|
150
156
|
return issues.map((issue) => ({
|
|
@@ -161,6 +167,7 @@ function serviceIdsOf(key, value) {
|
|
|
161
167
|
function createSignupStepper(spec) {
|
|
162
168
|
let steps = spec.forms.signup.steps;
|
|
163
169
|
let specVersion = spec.specVersion;
|
|
170
|
+
let consentController = createConsentController(spec.consentPack);
|
|
164
171
|
let state = {
|
|
165
172
|
stepIndex: 0,
|
|
166
173
|
stepCount: steps.length,
|
|
@@ -170,20 +177,53 @@ function createSignupStepper(spec) {
|
|
|
170
177
|
errors: [],
|
|
171
178
|
accountErrors: [],
|
|
172
179
|
consent: { tosAccepted: false, privacyAccepted: false },
|
|
173
|
-
credentials: {
|
|
180
|
+
credentials: {
|
|
181
|
+
email: "",
|
|
182
|
+
displayName: "",
|
|
183
|
+
organisationName: "",
|
|
184
|
+
subdomain: ""
|
|
185
|
+
},
|
|
174
186
|
status: "editing",
|
|
175
187
|
serverError: null,
|
|
176
188
|
specStale: false,
|
|
177
189
|
specVersion,
|
|
178
|
-
terminalStage: null
|
|
190
|
+
terminalStage: null,
|
|
191
|
+
consentState: consentController.getState(),
|
|
192
|
+
consentStale: false,
|
|
193
|
+
staleConsentKeys: [],
|
|
194
|
+
consentErrors: []
|
|
179
195
|
};
|
|
180
196
|
let validatedSteps = /* @__PURE__ */ new Set();
|
|
197
|
+
const serverAccountErrorPaths = /* @__PURE__ */ new Set();
|
|
198
|
+
let consentValidated = false;
|
|
181
199
|
const listeners = /* @__PURE__ */ new Set();
|
|
182
200
|
function emit() {
|
|
183
201
|
for (const listener of listeners) {
|
|
184
202
|
listener(structuredClone(state));
|
|
185
203
|
}
|
|
186
204
|
}
|
|
205
|
+
function syncConsentState() {
|
|
206
|
+
const consentState = consentController.getState();
|
|
207
|
+
const consentErrors = consentValidated ? computeConsentErrors() : state.consentErrors;
|
|
208
|
+
state = { ...state, consentState, consentErrors };
|
|
209
|
+
emit();
|
|
210
|
+
}
|
|
211
|
+
let unsubscribeConsent = consentController.subscribe(
|
|
212
|
+
() => syncConsentState()
|
|
213
|
+
);
|
|
214
|
+
function computeConsentErrors() {
|
|
215
|
+
const errors = [];
|
|
216
|
+
for (const d of consentController.getState().documents) {
|
|
217
|
+
if (d.doc.required && !(d.accepted && d.gateSatisfied)) {
|
|
218
|
+
const need = d.doc.acceptanceLevel === "scroll_then_accept" && !d.gateSatisfied ? "scroll to the end, then accept" : d.doc.acceptanceLevel === "open_then_accept" && !d.gateSatisfied ? "open the document, then accept" : "accept";
|
|
219
|
+
errors.push({
|
|
220
|
+
path: d.key,
|
|
221
|
+
message: `You must ${need} "${d.doc.title}".`
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return errors;
|
|
226
|
+
}
|
|
187
227
|
function knownFieldKeys() {
|
|
188
228
|
const keys = /* @__PURE__ */ new Set();
|
|
189
229
|
for (const step of steps) {
|
|
@@ -209,10 +249,25 @@ function createSignupStepper(spec) {
|
|
|
209
249
|
}
|
|
210
250
|
return [...new Set(services)];
|
|
211
251
|
}
|
|
252
|
+
function effectiveConsentBooleans() {
|
|
253
|
+
if (consentController.isEmpty()) return { ...state.consent };
|
|
254
|
+
const byType = /* @__PURE__ */ new Map();
|
|
255
|
+
for (const d of consentController.getState().documents) {
|
|
256
|
+
byType.set(d.doc.docType, d.accepted && d.gateSatisfied);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
tosAccepted: byType.has("tos") ? byType.get("tos") === true : state.consent.tosAccepted,
|
|
260
|
+
privacyAccepted: byType.has("privacy") ? byType.get("privacy") === true : state.consent.privacyAccepted
|
|
261
|
+
};
|
|
262
|
+
}
|
|
212
263
|
function runStepValidation(step) {
|
|
213
264
|
const result = deriveStepSchema(step).safeParse(stepAnswers(step));
|
|
214
265
|
return result.success ? [] : zodIssuesToFieldErrors(result.error.issues);
|
|
215
266
|
}
|
|
267
|
+
function swapConsentPack(newSpec) {
|
|
268
|
+
const resetKeys = consentController.applyNewPack(newSpec.consentPack);
|
|
269
|
+
return resetKeys;
|
|
270
|
+
}
|
|
216
271
|
function firstStepWithErrors() {
|
|
217
272
|
for (let i = 0; i < steps.length; i++) {
|
|
218
273
|
const step = steps[i];
|
|
@@ -221,6 +276,9 @@ function createSignupStepper(spec) {
|
|
|
221
276
|
return 0;
|
|
222
277
|
}
|
|
223
278
|
return {
|
|
279
|
+
get consent() {
|
|
280
|
+
return consentController;
|
|
281
|
+
},
|
|
224
282
|
getState() {
|
|
225
283
|
return structuredClone(state);
|
|
226
284
|
},
|
|
@@ -240,9 +298,14 @@ function createSignupStepper(spec) {
|
|
|
240
298
|
emit();
|
|
241
299
|
},
|
|
242
300
|
setCredentials(credentials) {
|
|
301
|
+
const previous = state.credentials;
|
|
302
|
+
const cleared = Object.entries(credentials).filter(
|
|
303
|
+
([k, v]) => v !== previous[k] && serverAccountErrorPaths.delete(k)
|
|
304
|
+
).map(([k]) => k);
|
|
243
305
|
state = {
|
|
244
306
|
...state,
|
|
245
|
-
credentials: { ...state.credentials, ...credentials }
|
|
307
|
+
credentials: { ...state.credentials, ...credentials },
|
|
308
|
+
accountErrors: cleared.length === 0 ? state.accountErrors : state.accountErrors.filter((e) => !cleared.includes(e.path))
|
|
246
309
|
};
|
|
247
310
|
emit();
|
|
248
311
|
},
|
|
@@ -295,33 +358,45 @@ function createSignupStepper(spec) {
|
|
|
295
358
|
for (let i = 0; i < steps.length; i++) validatedSteps.add(i);
|
|
296
359
|
const answersResult = deriveAnswersSchema(steps).safeParse(state.answers);
|
|
297
360
|
const errors = answersResult.success ? [] : zodIssuesToFieldErrors(answersResult.error.issues);
|
|
361
|
+
serverAccountErrorPaths.clear();
|
|
298
362
|
const accountErrors = [];
|
|
299
363
|
const credResult = credentialsSchema.safeParse(state.credentials);
|
|
300
364
|
if (!credResult.success) {
|
|
301
365
|
accountErrors.push(...zodIssuesToFieldErrors(credResult.error.issues));
|
|
302
366
|
}
|
|
303
|
-
|
|
367
|
+
const effective = effectiveConsentBooleans();
|
|
368
|
+
if (effective.tosAccepted !== true) {
|
|
304
369
|
accountErrors.push({
|
|
305
370
|
path: "tosAccepted",
|
|
306
371
|
message: "You must accept the Terms of Service"
|
|
307
372
|
});
|
|
308
373
|
}
|
|
309
|
-
if (
|
|
374
|
+
if (effective.privacyAccepted !== true) {
|
|
310
375
|
accountErrors.push({
|
|
311
376
|
path: "privacyAccepted",
|
|
312
377
|
message: "You must accept the Privacy Policy"
|
|
313
378
|
});
|
|
314
379
|
}
|
|
315
|
-
|
|
380
|
+
consentValidated = true;
|
|
381
|
+
const consentErrors = consentController.isEmpty() ? [] : computeConsentErrors();
|
|
382
|
+
state = {
|
|
383
|
+
...state,
|
|
384
|
+
errors,
|
|
385
|
+
accountErrors,
|
|
386
|
+
consentErrors,
|
|
387
|
+
consentState: consentController.getState()
|
|
388
|
+
};
|
|
316
389
|
emit();
|
|
317
|
-
return errors.length === 0 && accountErrors.length === 0;
|
|
390
|
+
return errors.length === 0 && accountErrors.length === 0 && consentErrors.length === 0;
|
|
318
391
|
},
|
|
319
392
|
buildSubmitPayload() {
|
|
320
|
-
|
|
393
|
+
const effective = effectiveConsentBooleans();
|
|
394
|
+
if (effective.tosAccepted !== true || effective.privacyAccepted !== true) {
|
|
321
395
|
throw new Error(
|
|
322
396
|
"buildSubmitPayload: cannot build a submit payload without genuine user consent (tosAccepted && privacyAccepted must both be true). Call validateAll() and surface the consent errors to the user first."
|
|
323
397
|
);
|
|
324
398
|
}
|
|
399
|
+
const consentDocuments = consentController.isEmpty() ? void 0 : consentController.buildConsentPayload();
|
|
325
400
|
const parsed = deriveAnswersSchema(steps).safeParse(state.answers);
|
|
326
401
|
if (!parsed.success) {
|
|
327
402
|
const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(answers)"}: ${i.message}`).join("; ");
|
|
@@ -342,10 +417,16 @@ function createSignupStepper(spec) {
|
|
|
342
417
|
answers,
|
|
343
418
|
...selectedServices.length > 0 ? { selectedServices } : {},
|
|
344
419
|
consent: {
|
|
345
|
-
tosAccepted:
|
|
346
|
-
privacyAccepted:
|
|
420
|
+
tosAccepted: effective.tosAccepted,
|
|
421
|
+
privacyAccepted: effective.privacyAccepted,
|
|
422
|
+
...consentDocuments !== void 0 ? { documents: consentDocuments } : {}
|
|
423
|
+
},
|
|
424
|
+
credentials: {
|
|
425
|
+
email: state.credentials.email,
|
|
426
|
+
...state.credentials.displayName ? { displayName: state.credentials.displayName } : {}
|
|
347
427
|
},
|
|
348
|
-
|
|
428
|
+
organisationName: state.credentials.organisationName,
|
|
429
|
+
subdomain: state.credentials.subdomain
|
|
349
430
|
};
|
|
350
431
|
},
|
|
351
432
|
applyNewSpec(newSpec) {
|
|
@@ -359,6 +440,7 @@ function createSignupStepper(spec) {
|
|
|
359
440
|
for (const [key, value] of Object.entries(state.answers)) {
|
|
360
441
|
if (known.has(key)) answers[key] = value;
|
|
361
442
|
}
|
|
443
|
+
const resetKeys = swapConsentPack(newSpec);
|
|
362
444
|
state = {
|
|
363
445
|
...state,
|
|
364
446
|
answers,
|
|
@@ -366,7 +448,11 @@ function createSignupStepper(spec) {
|
|
|
366
448
|
specVersion,
|
|
367
449
|
specStale: true,
|
|
368
450
|
status: "editing",
|
|
369
|
-
serverError: null
|
|
451
|
+
serverError: null,
|
|
452
|
+
consentState: consentController.getState(),
|
|
453
|
+
consentStale: state.consentStale || resetKeys.length > 0,
|
|
454
|
+
staleConsentKeys: resetKeys,
|
|
455
|
+
consentErrors: []
|
|
370
456
|
};
|
|
371
457
|
state.selectedServices = deriveSelectedServices();
|
|
372
458
|
const stepIndex = Math.min(firstStepWithErrors(), steps.length - 1);
|
|
@@ -380,6 +466,25 @@ function createSignupStepper(spec) {
|
|
|
380
466
|
};
|
|
381
467
|
emit();
|
|
382
468
|
},
|
|
469
|
+
applyNewConsentPack(newSpec) {
|
|
470
|
+
const resetKeys = swapConsentPack(newSpec);
|
|
471
|
+
specVersion = newSpec.specVersion;
|
|
472
|
+
state = {
|
|
473
|
+
...state,
|
|
474
|
+
specVersion,
|
|
475
|
+
consentState: consentController.getState(),
|
|
476
|
+
consentStale: true,
|
|
477
|
+
staleConsentKeys: resetKeys,
|
|
478
|
+
consentErrors: [],
|
|
479
|
+
status: "editing",
|
|
480
|
+
serverError: null
|
|
481
|
+
};
|
|
482
|
+
emit();
|
|
483
|
+
},
|
|
484
|
+
acknowledgeConsentStale() {
|
|
485
|
+
state = { ...state, consentStale: false, staleConsentKeys: [] };
|
|
486
|
+
emit();
|
|
487
|
+
},
|
|
383
488
|
acknowledgeSpecStale() {
|
|
384
489
|
state = { ...state, specStale: false };
|
|
385
490
|
emit();
|
|
@@ -400,8 +505,31 @@ function createSignupStepper(spec) {
|
|
|
400
505
|
state = { ...state, status: "error", serverError: message };
|
|
401
506
|
emit();
|
|
402
507
|
},
|
|
508
|
+
setAccountError(path, message) {
|
|
509
|
+
serverAccountErrorPaths.add(path);
|
|
510
|
+
state = {
|
|
511
|
+
...state,
|
|
512
|
+
status: "editing",
|
|
513
|
+
serverError: null,
|
|
514
|
+
accountErrors: [
|
|
515
|
+
...state.accountErrors.filter((e) => e.path !== path),
|
|
516
|
+
{ path, message }
|
|
517
|
+
]
|
|
518
|
+
};
|
|
519
|
+
emit();
|
|
520
|
+
},
|
|
403
521
|
reset() {
|
|
404
522
|
validatedSteps = /* @__PURE__ */ new Set();
|
|
523
|
+
consentValidated = false;
|
|
524
|
+
serverAccountErrorPaths.clear();
|
|
525
|
+
const currentDocs = consentController.getState().documents.map((d) => d.doc);
|
|
526
|
+
unsubscribeConsent();
|
|
527
|
+
consentController = createConsentController(
|
|
528
|
+
currentDocs.length > 0 ? { documents: currentDocs } : void 0
|
|
529
|
+
);
|
|
530
|
+
unsubscribeConsent = consentController.subscribe(
|
|
531
|
+
() => syncConsentState()
|
|
532
|
+
);
|
|
405
533
|
state = {
|
|
406
534
|
stepIndex: 0,
|
|
407
535
|
stepCount: steps.length,
|
|
@@ -411,12 +539,21 @@ function createSignupStepper(spec) {
|
|
|
411
539
|
errors: [],
|
|
412
540
|
accountErrors: [],
|
|
413
541
|
consent: { tosAccepted: false, privacyAccepted: false },
|
|
414
|
-
credentials: {
|
|
542
|
+
credentials: {
|
|
543
|
+
email: "",
|
|
544
|
+
displayName: "",
|
|
545
|
+
organisationName: "",
|
|
546
|
+
subdomain: ""
|
|
547
|
+
},
|
|
415
548
|
status: "editing",
|
|
416
549
|
serverError: null,
|
|
417
550
|
specStale: false,
|
|
418
551
|
specVersion,
|
|
419
|
-
terminalStage: null
|
|
552
|
+
terminalStage: null,
|
|
553
|
+
consentState: consentController.getState(),
|
|
554
|
+
consentStale: false,
|
|
555
|
+
staleConsentKeys: [],
|
|
556
|
+
consentErrors: []
|
|
420
557
|
};
|
|
421
558
|
emit();
|
|
422
559
|
},
|
|
@@ -430,5 +567,5 @@ function createSignupStepper(spec) {
|
|
|
430
567
|
}
|
|
431
568
|
|
|
432
569
|
export { createSignupStepper, deriveAnswersSchema, deriveFieldSchema, deriveStepSchema, optionValue };
|
|
433
|
-
//# sourceMappingURL=chunk-
|
|
434
|
-
//# sourceMappingURL=chunk-
|
|
570
|
+
//# sourceMappingURL=chunk-A3DE2IAH.js.map
|
|
571
|
+
//# sourceMappingURL=chunk-A3DE2IAH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/dynamic/derive.ts","../src/dynamic/stepper.ts"],"names":["z"],"mappings":"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","file":"chunk-A3DE2IAH.js","sourcesContent":["/**\n * Spec → Zod validator derivation (D540 §3).\n *\n * Turns arbitrary SpecField descriptors from the served spec into Zod\n * schemas for CLIENT-side UX validation. The server re-validates\n * authoritatively (D540 §2) — client validation is a convenience layer,\n * so the derivation is deliberately conservative:\n *\n * - Known `type`s map to typed schemas (see FIELD TYPE MAP below).\n * - An UNKNOWN type falls back to z.string() (never blocks a field the\n * server knows how to validate).\n * - Known `validation` keys are applied (minLength, maxLength, min, max,\n * pattern, format:\"email\"). Unknown validation keys are IGNORED\n * client-side — the server enforces them.\n * - `required:false` fields accept undefined AND \"\" (an empty input is\n * \"not provided\").\n * - select/radio values must be one of `options`; multiselect values\n * must be a subset.\n *\n * FIELD TYPE MAP:\n * text | textarea | tel | (unknown) → string\n * email → string().email()\n * number → coerce.number (DOM inputs are strings)\n * checkbox | boolean → boolean\n * select | radio → string ∈ options\n * multiselect → string[] ⊆ options\n * date → string, parseable as a date\n */\n\nimport { z } from \"zod/v3\";\nimport type { SpecField, SpecStep } from \"../spec/model.js\";\n\n/** Extract the comparable value of an option entry ({value}, {key}, or scalar). */\nexport function optionValue(option: unknown): string {\n if (option !== null && typeof option === \"object\") {\n const o = option as Record<string, unknown>;\n if (\"value\" in o) return String(o[\"value\"]);\n if (\"key\" in o) return String(o[\"key\"]);\n }\n return String(option);\n}\n\ninterface KnownValidation {\n minLength?: number;\n maxLength?: number;\n min?: number;\n max?: number;\n pattern?: string;\n format?: string;\n}\n\nfunction knownValidation(field: SpecField): KnownValidation {\n const v = field.validation ?? {};\n const out: KnownValidation = {};\n if (typeof v[\"minLength\"] === \"number\") out.minLength = v[\"minLength\"];\n if (typeof v[\"maxLength\"] === \"number\") out.maxLength = v[\"maxLength\"];\n if (typeof v[\"min\"] === \"number\") out.min = v[\"min\"];\n if (typeof v[\"max\"] === \"number\") out.max = v[\"max\"];\n if (typeof v[\"pattern\"] === \"string\") out.pattern = v[\"pattern\"];\n if (typeof v[\"format\"] === \"string\") out.format = v[\"format\"];\n return out;\n}\n\nfunction stringSchema(field: SpecField): z.ZodTypeAny {\n const v = knownValidation(field);\n let s = z.string();\n if (field.type === \"email\" || v.format === \"email\") {\n s = s.email(`${field.label} must be a valid email`);\n }\n if (v.minLength !== undefined) {\n s = s.min(\n v.minLength,\n `${field.label} must be at least ${v.minLength} characters`,\n );\n }\n if (v.maxLength !== undefined) {\n s = s.max(\n v.maxLength,\n `${field.label} must be at most ${v.maxLength} characters`,\n );\n }\n if (v.pattern !== undefined) {\n try {\n s = s.regex(\n new RegExp(v.pattern),\n `${field.label} has an invalid format`,\n );\n } catch {\n // Invalid regex in the spec — skip client-side; server enforces.\n }\n }\n return s;\n}\n\nfunction numberSchema(field: SpecField): z.ZodTypeAny {\n const v = knownValidation(field);\n let s = z.coerce.number({\n invalid_type_error: `${field.label} must be a number`,\n });\n if (v.min !== undefined) {\n s = s.min(v.min, `${field.label} must be at least ${v.min}`);\n }\n if (v.max !== undefined) {\n s = s.max(v.max, `${field.label} must be at most ${v.max}`);\n }\n return s;\n}\n\nfunction enumSchema(field: SpecField): z.ZodTypeAny {\n const allowed = (field.options ?? []).map(optionValue);\n if (allowed.length === 0) return z.string();\n return z.string().refine((val) => allowed.includes(val), {\n message: `${field.label} must be one of the offered options`,\n });\n}\n\nfunction multiEnumSchema(field: SpecField): z.ZodTypeAny {\n const allowed = (field.options ?? []).map(optionValue);\n const item =\n allowed.length === 0\n ? z.string()\n : z.string().refine((val) => allowed.includes(val), {\n message: `${field.label} contains a value outside the offered options`,\n });\n return z.array(item);\n}\n\nfunction dateSchema(field: SpecField): z.ZodTypeAny {\n return z.string().refine((val) => !Number.isNaN(Date.parse(val)), {\n message: `${field.label} must be a valid date`,\n });\n}\n\n/** Derive the value schema for one field, BEFORE the required/optional wrap. */\nfunction baseSchema(field: SpecField): z.ZodTypeAny {\n switch (field.type) {\n case \"email\":\n return stringSchema(field);\n case \"number\":\n return numberSchema(field);\n case \"checkbox\":\n case \"boolean\":\n return z.boolean();\n case \"select\":\n case \"radio\":\n return enumSchema(field);\n case \"multiselect\":\n return multiEnumSchema(field);\n case \"date\":\n return dateSchema(field);\n default:\n // text | textarea | tel | any unknown type → string with validation\n return stringSchema(field);\n }\n}\n\n/**\n * Derive the full Zod schema for one SpecField.\n * Optional fields accept undefined and \"\" (\"not provided\" in a form UI).\n * Required fields additionally reject \"\" for string-typed inputs.\n */\nexport function deriveFieldSchema(field: SpecField): z.ZodTypeAny {\n const base = baseSchema(field);\n if (field.required) {\n // Required string inputs must be non-empty.\n if (\n field.type !== \"checkbox\" &&\n field.type !== \"boolean\" &&\n field.type !== \"number\" &&\n field.type !== \"multiselect\"\n ) {\n return z\n .string({ required_error: `${field.label} is required` })\n .min(1, `${field.label} is required`)\n .pipe(base);\n }\n if (field.type === \"number\") {\n // A required number must be PRESENT. Without this guard,\n // z.coerce.number(\"\") coerces the empty DOM value to 0 and passes —\n // then buildSubmitPayload drops the \"\" answer, so the client declares\n // the form valid while the server 422s on a missing required field.\n // Reject empty/missing BEFORE coercion; a provided value flows to\n // the coercing number schema (min/max still apply).\n return z\n .any()\n .refine(\n (val) => val !== \"\" && val !== undefined && val !== null,\n `${field.label} is required`,\n )\n .pipe(base);\n }\n if (field.type === \"multiselect\") {\n return base.refine(\n (val) => Array.isArray(val) && val.length > 0,\n `${field.label} requires at least one selection`,\n );\n }\n return base;\n }\n // Optional: \"\" normalizes to undefined before validation.\n return z.preprocess(\n (val) =>\n val === \"\" || val === undefined || val === null ? undefined : val,\n base.optional(),\n );\n}\n\n/**\n * Derive a STRICT Zod object schema for one step's fields — unknown answer\n * keys within the step are rejected (mirrors the server's authoritative\n * re-validation posture; a stale client must not silently carry fields the\n * spec no longer defines).\n */\nexport function deriveStepSchema(step: SpecStep) {\n const shape: Record<string, z.ZodTypeAny> = {};\n for (const field of step.fields) {\n shape[field.key] = deriveFieldSchema(field);\n }\n return z.object(shape).strict();\n}\n\n/**\n * Derive the STRICT schema for the complete answers object across all\n * steps (used at submit time and by the 409-stale re-validation).\n */\nexport function deriveAnswersSchema(steps: SpecStep[]) {\n const shape: Record<string, z.ZodTypeAny> = {};\n for (const step of steps) {\n for (const field of step.fields) {\n shape[field.key] = deriveFieldSchema(field);\n }\n }\n return z.object(shape).strict();\n}\n","/**\n * Headless dynamic signup stepper (D540 §1/§3).\n *\n * The steps are GENERATED from the served spec: fields bucket by `stage`\n * server-side → the spec's forms.signup.steps array. More configured\n * fields ⇒ more steps. The final `activation` stage carries the\n * service-selection inputs feeding the existing terminal-stage guards.\n *\n * Framework-agnostic: same subscribe/mutate contract as the Phase-1\n * contact-form state. The default component layer renders it; framework\n * integrations can wrap it.\n *\n * 409 spec_stale flow (D540 reject-stale): the CALLER (widget/submit\n * orchestration) refetches the spec on OnboardingSpecStaleError and calls\n * `applyNewSpec(newSpec)`. The stepper re-validates every answer against\n * the NEW derived schema, DROPS answers whose fields no longer exist,\n * marks `specStale: true`, and repositions to the first step with errors —\n * the user re-confirms; the SDK NEVER silently auto-resubmits.\n */\n\nimport { z, type ZodIssue } from \"zod/v3\";\nimport type { OnboardingSpec, SpecStep, TerminalStage } from \"../spec/model.js\";\nimport { deriveAnswersSchema, deriveStepSchema } from \"./derive.js\";\nimport {\n createConsentController,\n type ConsentController,\n type ConsentState,\n type ConsentSubmitDocument,\n} from \"../consent/controller.js\";\nimport type { FieldError } from \"../form/state.js\";\nimport {\n isSubdomainLabelShape,\n SUBDOMAIN_SHAPE_MESSAGE,\n} from \"../submit/subdomain.js\";\n\nexport type StepperStatus = \"editing\" | \"submitting\" | \"success\" | \"error\";\n\nexport interface SignupConsent {\n tosAccepted: boolean;\n privacyAccepted: boolean;\n}\n\nexport interface SignupCredentials {\n email: string;\n displayName: string;\n organisationName: string;\n subdomain: string;\n}\n\nexport interface SignupStepperState {\n /** Zero-based index of the current step. */\n stepIndex: number;\n stepCount: number;\n currentStep: SpecStep | null;\n /** Answers keyed by field key (only keys defined by the active spec). */\n answers: Record<string, unknown>;\n /** Derived from serviceSelection-flagged answers (see buildSubmitPayload). */\n selectedServices: string[];\n /** Validation errors for the CURRENT step (or full-form at submit). */\n errors: FieldError[];\n /** Credential/consent errors (submit-time). */\n accountErrors: FieldError[];\n consent: SignupConsent;\n credentials: SignupCredentials;\n status: StepperStatus;\n serverError: string | null;\n /** True after a 409-stale spec swap until the user re-validates. */\n specStale: boolean;\n /** The specVersion currently driving the form (echoed on submit). */\n specVersion: string;\n /** Set on success from the submit response. */\n terminalStage: TerminalStage | null;\n /**\n * D543 ConsentPack state (per-document acceptance + gates). Empty\n * (documents:[]) when the spec serves no consentPack — the consent stage is\n * then skipped and does not gate the submit.\n */\n consentState: ConsentState;\n /**\n * True after a 409 consent_stale swap until the user re-accepts the changed\n * docs. Distinct from specStale — a doc-version bump does not necessarily\n * change the form fields, and the banner copy differs.\n */\n consentStale: boolean;\n /** Keys of the consent docs whose acceptance was reset by the last swap. */\n staleConsentKeys: string[];\n /** Consent-stage errors (submit-time; e.g. a required doc not accepted). */\n consentErrors: FieldError[];\n}\n\nexport type SignupStepperListener = (state: SignupStepperState) => void;\n\n/** The locked credential constraints (PublicOnboardingSubmitRequest). */\nconst credentialsSchema = z.object({\n email: z.string().email(\"A valid account email is required\"),\n displayName: z.string().trim().min(1, \"Your name is required\").max(200),\n organisationName: z\n .string()\n .trim()\n .min(1, \"Organisation name is required\")\n .max(200),\n subdomain: z\n .string()\n .trim()\n .min(1, \"Workspace subdomain is required\")\n .max(63)\n // Empty and over-long already have their own message above.\n .refine(\n (v) => v === \"\" || v.length > 63 || isSubdomainLabelShape(v),\n SUBDOMAIN_SHAPE_MESSAGE,\n ),\n});\n\nfunction zodIssuesToFieldErrors(issues: ZodIssue[]): FieldError[] {\n return issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n }));\n}\n\n/** Flatten a serviceSelection field's answer into service ids. */\nfunction serviceIdsOf(key: string, value: unknown): string[] {\n if (typeof value === \"string\" && value !== \"\") return [value];\n if (Array.isArray(value)) return value.map((v) => String(v));\n if (value === true) return [key];\n return [];\n}\n\nexport interface SignupStepperControl {\n getState(): SignupStepperState;\n setAnswer(key: string, value: unknown): void;\n setConsent(consent: Partial<SignupConsent>): void;\n setCredentials(credentials: Partial<SignupCredentials>): void;\n /**\n * The headless ConsentPack controller (D543 §5). Exposed so the default\n * component layer AND fully custom tenant UIs drive the SAME state: mark\n * scroll/open evidence, set acceptance, read per-doc gate status. The\n * stepper subscribes to it internally so consent mutations re-emit stepper\n * state (consentState/consentStale) to the widget.\n */\n readonly consent: ConsentController;\n /** Validate ONLY the current step. Returns true when clean. */\n validateStep(): boolean;\n /** Validate current step; advance when clean. Returns true if advanced. */\n next(): boolean;\n back(): void;\n isFirstStep(): boolean;\n isLastStep(): boolean;\n /**\n * Full-form validation: every step's answers + credentials + consent.\n * Populates errors/accountErrors. Returns true when submittable.\n */\n validateAll(): boolean;\n /**\n * The submit payload body (minus botToken, which the transport attaches).\n * Call ONLY after validateAll() returns true.\n *\n * D543 §4: `consent.documents` is the per-doc consent array built from the\n * ACTUAL consent state (omitted when the spec serves no consentPack). The\n * booleans stay as the derived convenience. THROWS if any required consent\n * doc is not accepted at its gate (never fabricates acceptance).\n */\n buildSubmitPayload(): {\n specVersion: string;\n answers: Record<string, unknown>;\n selectedServices?: string[];\n consent: {\n tosAccepted: true;\n privacyAccepted: true;\n documents?: ConsentSubmitDocument[];\n };\n credentials: { email: string; displayName?: string };\n organisationName?: string;\n subdomain: string;\n };\n /**\n * Swap in a refetched spec after 409 spec_stale. Drops answers for\n * removed fields, re-validates the rest, marks specStale, repositions\n * to the first step with errors. ALSO swaps the consentPack (a spec bump\n * may carry new doc versions) — changed docs require re-acceptance. NEVER\n * auto-resubmits.\n */\n applyNewSpec(spec: OnboardingSpec): void;\n /** Clear the specStale flag once the user has re-confirmed. */\n acknowledgeSpecStale(): void;\n /**\n * Swap in the refetched spec's consentPack after 409 consent_stale (D543\n * §4). Resets acceptance for docs whose version/hash changed (re-acceptance\n * required — NEVER auto-accept a changed doc), marks consentStale, records\n * which keys were reset. NEVER auto-resubmits.\n */\n applyNewConsentPack(spec: OnboardingSpec): void;\n /** Clear the consentStale flag once the user has re-accepted. */\n acknowledgeConsentStale(): void;\n setSubmitting(): void;\n setSuccess(terminalStage: TerminalStage): void;\n setError(message: string): void;\n /**\n * Put a server's refusal on the one account field it is about (409\n * subdomain_taken → `subdomain`), so the user is told which input to change.\n * Returns the form to \"editing\" with no form-level error: the field carries\n * the message. Replaces any error already on that field. Editing the field\n * clears it; the next validateAll() recomputes account errors as usual.\n */\n setAccountError(path: keyof SignupCredentials, message: string): void;\n /**\n * Reset to the pristine initial state against the CURRENT spec: step 0,\n * status \"editing\", answers/credentials/consent/errors cleared. This is\n * the only transition back OUT of \"success\"/\"error\"; the widget's reset()\n * relies on it (otherwise a post-success keystroke would re-emit\n * status \"success\" and re-hide the form).\n */\n reset(): void;\n subscribe(listener: SignupStepperListener): () => void;\n}\n\n/**\n * Create a headless stepper for the spec's signup form.\n *\n * @example\n * ```ts\n * const spec = await fetchOnboardingSpec({ baseUrl, onboarder });\n * const stepper = createSignupStepper(spec);\n * stepper.subscribe(render);\n * stepper.setAnswer(\"company_name\", \"ACME\");\n * if (stepper.next()) { ... }\n * ```\n */\nexport function createSignupStepper(\n spec: OnboardingSpec,\n): SignupStepperControl {\n let steps: SpecStep[] = spec.forms.signup.steps;\n let specVersion = spec.specVersion;\n\n // D543 ConsentPack controller. The stepper OWNS it; the widget + custom\n // UIs drive it via the exposed `consent` control. It's the same instance\n // for the stepper's lifetime — applyNewConsentPack SWAPS its docs in place\n // (createConsentController is re-run on reset()).\n let consentController = createConsentController(spec.consentPack);\n\n let state: SignupStepperState = {\n stepIndex: 0,\n stepCount: steps.length,\n currentStep: steps[0] ?? null,\n answers: {},\n selectedServices: [],\n errors: [],\n accountErrors: [],\n consent: { tosAccepted: false, privacyAccepted: false },\n credentials: {\n email: \"\",\n displayName: \"\",\n organisationName: \"\",\n subdomain: \"\",\n },\n status: \"editing\",\n serverError: null,\n specStale: false,\n specVersion,\n terminalStage: null,\n consentState: consentController.getState(),\n consentStale: false,\n staleConsentKeys: [],\n consentErrors: [],\n };\n\n // Which step indices have been validated at least once. Once a step has\n // been validated, EVERY subsequent setAnswer revalidates it — so a\n // fix-then-rebreak on that step re-shows the error immediately (matching\n // the Phase-1 contact-form `hasValidated` fix). Before a step is validated,\n // typing does not spray errors. Cleared on reset()/applyNewSpec.\n let validatedSteps = new Set<number>();\n\n // Account fields whose current error came from the server (setAccountError)\n // rather than from the form's own validation. Only these clear on edit.\n const serverAccountErrorPaths = new Set<string>();\n\n // True once validateAll() has run — after that, EVERY consent mutation\n // recomputes consent errors (fix-then-rebreak parity with field errors), so\n // a per-doc error clears live as the user accepts and persists as long as a\n // required doc is unmet. Cleared on reset()/applyNewSpec/applyNewConsentPack.\n let consentValidated = false;\n\n const listeners = new Set<SignupStepperListener>();\n\n function emit(): void {\n for (const listener of listeners) {\n listener(structuredClone(state));\n }\n }\n\n /**\n * Mirror the consent controller's state into the stepper state and re-emit,\n * so a subscriber that only watches stepper state (the widget) sees consent\n * gate/acceptance changes. Once validateAll() has run, consent errors are\n * recomputed on every mutation so they clear live AND persist correctly\n * regardless of which emit (validateAll vs a consent mutation vs the async\n * viewer's scroll report) fires last.\n */\n function syncConsentState(): void {\n const consentState = consentController.getState();\n const consentErrors = consentValidated\n ? computeConsentErrors()\n : state.consentErrors;\n state = { ...state, consentState, consentErrors };\n emit();\n }\n\n // Wire consent mutations through the stepper so the widget re-renders. The\n // subscription lives for the controller's lifetime; on reset() a NEW\n // controller is created and re-subscribed.\n let unsubscribeConsent = consentController.subscribe(() =>\n syncConsentState(),\n );\n\n /** One error per required consent doc that is not accepted at its gate. */\n function computeConsentErrors(): FieldError[] {\n const errors: FieldError[] = [];\n for (const d of consentController.getState().documents) {\n if (d.doc.required && !(d.accepted && d.gateSatisfied)) {\n const need =\n d.doc.acceptanceLevel === \"scroll_then_accept\" && !d.gateSatisfied\n ? \"scroll to the end, then accept\"\n : d.doc.acceptanceLevel === \"open_then_accept\" && !d.gateSatisfied\n ? \"open the document, then accept\"\n : \"accept\";\n errors.push({\n path: d.key,\n message: `You must ${need} \"${d.doc.title}\".`,\n });\n }\n }\n return errors;\n }\n\n function knownFieldKeys(): Set<string> {\n const keys = new Set<string>();\n for (const step of steps) {\n for (const field of step.fields) keys.add(field.key);\n }\n return keys;\n }\n\n /** Answers restricted to the current step's fields. */\n function stepAnswers(step: SpecStep): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const field of step.fields) {\n if (field.key in state.answers) out[field.key] = state.answers[field.key];\n }\n return out;\n }\n\n function deriveSelectedServices(): string[] {\n const services: string[] = [];\n for (const step of steps) {\n for (const field of step.fields) {\n if (field.serviceSelection === true) {\n services.push(...serviceIdsOf(field.key, state.answers[field.key]));\n }\n }\n }\n return [...new Set(services)];\n }\n\n /**\n * The EFFECTIVE tos/privacy booleans (D543 §4 \"derived convenience\").\n *\n * When the spec serves a consentPack containing a `tos` / `privacy` doc,\n * the boolean is DERIVED from whether that doc is accepted (the array is\n * authoritative). Otherwise (pre-ConsentPack onboarder), the explicit\n * checkbox booleans from setConsent() apply. This keeps the legacy\n * booleans truthful without letting them contradict the consent array.\n */\n function effectiveConsentBooleans(): SignupConsent {\n if (consentController.isEmpty()) return { ...state.consent };\n const byType = new Map<string, boolean>();\n for (const d of consentController.getState().documents) {\n byType.set(d.doc.docType, d.accepted && d.gateSatisfied);\n }\n return {\n tosAccepted: byType.has(\"tos\")\n ? byType.get(\"tos\") === true\n : state.consent.tosAccepted,\n privacyAccepted: byType.has(\"privacy\")\n ? byType.get(\"privacy\") === true\n : state.consent.privacyAccepted,\n };\n }\n\n function runStepValidation(step: SpecStep): FieldError[] {\n const result = deriveStepSchema(step).safeParse(stepAnswers(step));\n return result.success ? [] : zodIssuesToFieldErrors(result.error.issues);\n }\n\n /**\n * Swap the consent controller's docs to the new spec's consentPack, RESET\n * acceptance for docs whose version/hash changed (re-acceptance required —\n * never auto-accept), and fold the result into stepper state. Shared by\n * applyNewSpec (a spec bump may also bump doc versions) and\n * applyNewConsentPack (a pure consent_stale swap). Returns the reset keys.\n *\n * `controllerAlreadyEmitted`: applyNewPack emits through the controller's\n * own subscription (syncConsentState), so the caller need not re-emit for\n * the consent slice — but callers that ALSO mutate other stepper state\n * fold it into their single state update to avoid a double render.\n */\n function swapConsentPack(newSpec: OnboardingSpec): string[] {\n const resetKeys = consentController.applyNewPack(newSpec.consentPack);\n return resetKeys;\n }\n\n function firstStepWithErrors(): number {\n for (let i = 0; i < steps.length; i++) {\n const step = steps[i];\n if (step && runStepValidation(step).length > 0) return i;\n }\n return 0;\n }\n\n return {\n get consent() {\n return consentController;\n },\n\n getState() {\n return structuredClone(state);\n },\n\n setAnswer(key, value) {\n state = {\n ...state,\n answers: { ...state.answers, [key]: value },\n };\n state.selectedServices = deriveSelectedServices();\n // Live-revalidate once THIS step has been validated at least once —\n // regardless of whether errors are currently showing. This is the\n // fix-then-rebreak fix: after fixing a field (errors → 0), breaking it\n // again must re-show the error without a fresh next()/validateStep().\n if (validatedSteps.has(state.stepIndex) && state.currentStep) {\n state = { ...state, errors: runStepValidation(state.currentStep) };\n }\n emit();\n },\n\n setConsent(consent) {\n state = { ...state, consent: { ...state.consent, ...consent } };\n emit();\n },\n\n setCredentials(credentials) {\n // A server's refusal of a field (setAccountError) answers the value\n // that was sent. Once the user changes that field it no longer applies;\n // an input event that re-sends the same value (autofill, IME) is no\n // change, and the refusal stands.\n const previous = state.credentials as unknown as Record<string, unknown>;\n const cleared = Object.entries(credentials)\n .filter(\n ([k, v]) => v !== previous[k] && serverAccountErrorPaths.delete(k),\n )\n .map(([k]) => k);\n state = {\n ...state,\n credentials: { ...state.credentials, ...credentials },\n accountErrors:\n cleared.length === 0\n ? state.accountErrors\n : state.accountErrors.filter((e) => !cleared.includes(e.path)),\n };\n emit();\n },\n\n validateStep() {\n if (!state.currentStep) return true;\n validatedSteps.add(state.stepIndex);\n const errors = runStepValidation(state.currentStep);\n state = { ...state, errors };\n emit();\n return errors.length === 0;\n },\n\n next() {\n if (!state.currentStep) return false;\n validatedSteps.add(state.stepIndex);\n const errors = runStepValidation(state.currentStep);\n if (errors.length > 0) {\n state = { ...state, errors };\n emit();\n return false;\n }\n if (state.stepIndex >= steps.length - 1) return false;\n const stepIndex = state.stepIndex + 1;\n state = {\n ...state,\n stepIndex,\n currentStep: steps[stepIndex] ?? null,\n errors: [],\n };\n emit();\n return true;\n },\n\n back() {\n if (state.stepIndex === 0) return;\n const stepIndex = state.stepIndex - 1;\n state = {\n ...state,\n stepIndex,\n currentStep: steps[stepIndex] ?? null,\n errors: [],\n };\n emit();\n },\n\n isFirstStep() {\n return state.stepIndex === 0;\n },\n\n isLastStep() {\n return state.stepIndex >= steps.length - 1;\n },\n\n validateAll() {\n // A full validation counts as validating every step.\n for (let i = 0; i < steps.length; i++) validatedSteps.add(i);\n // Answers across ALL steps (strict — unknown keys rejected).\n const answersResult = deriveAnswersSchema(steps).safeParse(state.answers);\n const errors = answersResult.success\n ? []\n : zodIssuesToFieldErrors(answersResult.error.issues);\n\n // Credentials + consent (locked submit contract). Recomputed from the\n // form alone, so a server refusal (setAccountError) is dropped here.\n serverAccountErrorPaths.clear();\n const accountErrors: FieldError[] = [];\n const credResult = credentialsSchema.safeParse(state.credentials);\n if (!credResult.success) {\n accountErrors.push(...zodIssuesToFieldErrors(credResult.error.issues));\n }\n // The tos/privacy booleans are the EFFECTIVE (derived-from-consentPack\n // when present) values. This keeps the legacy booleans in lockstep with\n // the ConsentPack — a user who accepted the tos DOC satisfies tosAccepted\n // without a separate checkbox, and one who did NOT is blocked here.\n const effective = effectiveConsentBooleans();\n if (effective.tosAccepted !== true) {\n accountErrors.push({\n path: \"tosAccepted\",\n message: \"You must accept the Terms of Service\",\n });\n }\n if (effective.privacyAccepted !== true) {\n accountErrors.push({\n path: \"privacyAccepted\",\n message: \"You must accept the Privacy Policy\",\n });\n }\n\n // D543 consent stage: every REQUIRED consent doc must be accepted at its\n // gate. Empty consentPack ⇒ vacuously complete (no consentErrors). Mark\n // consent validated so subsequent mutations recompute errors live.\n consentValidated = true;\n const consentErrors = consentController.isEmpty()\n ? []\n : computeConsentErrors();\n\n state = {\n ...state,\n errors,\n accountErrors,\n consentErrors,\n consentState: consentController.getState(),\n };\n emit();\n return (\n errors.length === 0 &&\n accountErrors.length === 0 &&\n consentErrors.length === 0\n );\n },\n\n buildSubmitPayload() {\n // COMPLIANCE-CRITICAL: never fabricate legal consent. Read the ACTUAL\n // consent state and refuse to build a payload unless the user genuinely\n // accepted both. A headless caller that skips validateAll() must not be\n // able to transmit consent:true for a user who never consented. The\n // booleans are the EFFECTIVE (consentPack-derived when present) values.\n const effective = effectiveConsentBooleans();\n if (\n effective.tosAccepted !== true ||\n effective.privacyAccepted !== true\n ) {\n throw new Error(\n \"buildSubmitPayload: cannot build a submit payload without genuine \" +\n \"user consent (tosAccepted && privacyAccepted must both be true). \" +\n \"Call validateAll() and surface the consent errors to the user first.\",\n );\n }\n\n // D543 §4: the consent-document array is authoritative. This THROWS if\n // any REQUIRED doc is not accepted at its gate (never fabricates\n // acceptance) — the same compliance discipline as the booleans above.\n // Omitted (undefined) when the spec serves no consentPack.\n const consentDocuments = consentController.isEmpty()\n ? undefined\n : consentController.buildConsentPayload();\n\n // Run answers through the derived schema so the wire carries COERCED\n // typed values (e.g. number fields as numbers, not raw DOM strings) —\n // this is the exact validation validateAll() applies, so a payload can\n // only be built from a state that would pass. A required field that is\n // empty/missing FAILS here (see deriveFieldSchema's required wrap),\n // so buildSubmitPayload throws rather than silently dropping it.\n const parsed = deriveAnswersSchema(steps).safeParse(state.answers);\n if (!parsed.success) {\n const detail = parsed.error.issues\n .map((i) => `${i.path.join(\".\") || \"(answers)\"}: ${i.message}`)\n .join(\"; \");\n throw new Error(\n `buildSubmitPayload: answers failed validation — call validateAll() first (${detail}).`,\n );\n }\n\n // Only keys the active spec defines, with empty optionals omitted.\n const known = knownFieldKeys();\n const answers: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(parsed.data)) {\n if (known.has(key) && value !== undefined && value !== \"\") {\n answers[key] = value;\n }\n }\n const selectedServices = deriveSelectedServices();\n return {\n specVersion,\n answers,\n ...(selectedServices.length > 0 ? { selectedServices } : {}),\n consent: {\n tosAccepted: effective.tosAccepted,\n privacyAccepted: effective.privacyAccepted,\n ...(consentDocuments !== undefined\n ? { documents: consentDocuments }\n : {}),\n } as {\n tosAccepted: true;\n privacyAccepted: true;\n documents?: ConsentSubmitDocument[];\n },\n credentials: {\n email: state.credentials.email,\n ...(state.credentials.displayName\n ? { displayName: state.credentials.displayName }\n : {}),\n },\n organisationName: state.credentials.organisationName,\n subdomain: state.credentials.subdomain,\n };\n },\n\n applyNewSpec(newSpec) {\n steps = newSpec.forms.signup.steps;\n specVersion = newSpec.specVersion;\n\n // Drop answers for fields the new spec no longer defines.\n const known = new Set<string>();\n for (const step of steps) {\n for (const field of step.fields) known.add(field.key);\n }\n const answers: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(state.answers)) {\n if (known.has(key)) answers[key] = value;\n }\n\n // A spec bump may ALSO carry new consent-doc versions — swap the pack so\n // changed docs require re-acceptance. Mark consentStale iff any accepted\n // doc was actually reset. (applyNewPack emits via the subscription; the\n // final state below overwrites consentState with the post-swap value.)\n const resetKeys = swapConsentPack(newSpec);\n\n state = {\n ...state,\n answers,\n stepCount: steps.length,\n specVersion,\n specStale: true,\n status: \"editing\",\n serverError: null,\n consentState: consentController.getState(),\n consentStale: state.consentStale || resetKeys.length > 0,\n staleConsentKeys: resetKeys,\n consentErrors: [],\n };\n state.selectedServices = deriveSelectedServices();\n\n // Reposition to the first step with errors under the NEW schema so\n // the user reviews exactly what changed; errors render immediately.\n const stepIndex = Math.min(firstStepWithErrors(), steps.length - 1);\n const currentStep = steps[Math.max(stepIndex, 0)] ?? null;\n // The repositioned step is being shown with errors → treat it as\n // validated so live-revalidation is active there immediately.\n validatedSteps = new Set<number>([Math.max(stepIndex, 0)]);\n state = {\n ...state,\n stepIndex: Math.max(stepIndex, 0),\n currentStep,\n errors: currentStep ? runStepValidation(currentStep) : [],\n };\n emit();\n },\n\n applyNewConsentPack(newSpec) {\n // Pure consent_stale swap (D543 §4): the form fields are unchanged; only\n // the consent docs may have bumped. Reset acceptance for changed docs\n // (re-acceptance required — NEVER auto-accept), mark consentStale.\n const resetKeys = swapConsentPack(newSpec);\n // Keep specVersion in lockstep — a consent_stale refetch carries the\n // current spec, whose specVersion the next submit must echo.\n specVersion = newSpec.specVersion;\n state = {\n ...state,\n specVersion,\n consentState: consentController.getState(),\n consentStale: true,\n staleConsentKeys: resetKeys,\n consentErrors: [],\n status: \"editing\",\n serverError: null,\n };\n emit();\n },\n\n acknowledgeConsentStale() {\n state = { ...state, consentStale: false, staleConsentKeys: [] };\n emit();\n },\n\n acknowledgeSpecStale() {\n state = { ...state, specStale: false };\n emit();\n },\n\n setSubmitting() {\n state = {\n ...state,\n status: \"submitting\",\n serverError: null,\n };\n emit();\n },\n\n setSuccess(terminalStage) {\n state = { ...state, status: \"success\", terminalStage, serverError: null };\n emit();\n },\n\n setError(message) {\n state = { ...state, status: \"error\", serverError: message };\n emit();\n },\n\n setAccountError(path, message) {\n serverAccountErrorPaths.add(path);\n // The refusal is the field's to show: the form is editable again, with\n // no form-level banner left to go stale once the field is fixed.\n state = {\n ...state,\n status: \"editing\",\n serverError: null,\n accountErrors: [\n ...state.accountErrors.filter((e) => e.path !== path),\n { path, message },\n ],\n };\n emit();\n },\n\n reset() {\n validatedSteps = new Set<number>();\n consentValidated = false;\n serverAccountErrorPaths.clear();\n // Rebuild the consent controller from the CURRENT (possibly swapped)\n // docs so reset() clears all acceptance/evidence back to pristine. Drop\n // the old subscription and re-subscribe the new instance.\n const currentDocs = consentController\n .getState()\n .documents.map((d) => d.doc);\n unsubscribeConsent();\n consentController = createConsentController(\n currentDocs.length > 0 ? { documents: currentDocs } : undefined,\n );\n unsubscribeConsent = consentController.subscribe(() =>\n syncConsentState(),\n );\n state = {\n stepIndex: 0,\n stepCount: steps.length,\n currentStep: steps[0] ?? null,\n answers: {},\n selectedServices: [],\n errors: [],\n accountErrors: [],\n consent: { tosAccepted: false, privacyAccepted: false },\n credentials: {\n email: \"\",\n displayName: \"\",\n organisationName: \"\",\n subdomain: \"\",\n },\n status: \"editing\",\n serverError: null,\n specStale: false,\n specVersion,\n terminalStage: null,\n consentState: consentController.getState(),\n consentStale: false,\n staleConsentKeys: [],\n consentErrors: [],\n };\n emit();\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { contactFormSchema } from './chunk-
|
|
1
|
+
import { contactFormSchema } from './chunk-WKN4U7QF.js';
|
|
2
2
|
|
|
3
3
|
// src/form/state.ts
|
|
4
4
|
function zodIssuesToFieldErrors(issues) {
|
|
@@ -8,12 +8,20 @@ function zodIssuesToFieldErrors(issues) {
|
|
|
8
8
|
}));
|
|
9
9
|
}
|
|
10
10
|
var INITIAL_VALUES = {
|
|
11
|
-
name:
|
|
11
|
+
name: void 0,
|
|
12
|
+
companyName: "",
|
|
13
|
+
contactName: "",
|
|
12
14
|
email: "",
|
|
13
15
|
phone: void 0,
|
|
14
|
-
message: void 0
|
|
16
|
+
message: void 0,
|
|
17
|
+
interestModules: [],
|
|
18
|
+
sizeBand: void 0,
|
|
19
|
+
attribution: void 0,
|
|
20
|
+
privacyNoticeAcknowledged: false,
|
|
21
|
+
marketingOptIn: false,
|
|
22
|
+
intakeKind: "enquiry"
|
|
15
23
|
};
|
|
16
|
-
function createContactFormState() {
|
|
24
|
+
function createContactFormState(options = {}) {
|
|
17
25
|
let state = {
|
|
18
26
|
values: { ...INITIAL_VALUES },
|
|
19
27
|
errors: [],
|
|
@@ -29,8 +37,11 @@ function createContactFormState() {
|
|
|
29
37
|
}
|
|
30
38
|
function runValidation() {
|
|
31
39
|
const result = contactFormSchema.safeParse(state.values);
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
const errors = result.success ? [] : zodIssuesToFieldErrors(result.error.issues);
|
|
41
|
+
if (options.requireInterest && state.values.interestModules.length === 0) {
|
|
42
|
+
errors.push({ path: "interestModules", message: "Choose at least one" });
|
|
43
|
+
}
|
|
44
|
+
return errors;
|
|
34
45
|
}
|
|
35
46
|
let hasValidated = false;
|
|
36
47
|
return {
|
|
@@ -88,5 +99,5 @@ function createContactFormState() {
|
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
export { createContactFormState };
|
|
91
|
-
//# sourceMappingURL=chunk-
|
|
92
|
-
//# sourceMappingURL=chunk-
|
|
102
|
+
//# sourceMappingURL=chunk-DWFZA5T6.js.map
|
|
103
|
+
//# sourceMappingURL=chunk-DWFZA5T6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/form/state.ts"],"names":[],"mappings":"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","file":"chunk-DWFZA5T6.js","sourcesContent":["/**\n * Headless form state manager for the contact form.\n *\n * Designed to be framework-agnostic — callers supply a change callback\n * and call the returned mutators. The widget layer uses this internally;\n * framework integrations (React, Vue, etc.) can wrap it in their own hooks.\n */\n\nimport type { ZodIssue } from \"zod/v3\";\nimport { contactFormSchema, type ContactFormValues } from \"./schema.js\";\n\nexport interface FieldError {\n path: string;\n message: string;\n}\n\nexport type FormStatus = \"idle\" | \"submitting\" | \"success\" | \"error\";\n\nexport interface ContactFormState {\n values: ContactFormValues;\n /** Per-field validation errors from the last validate() call. */\n errors: FieldError[];\n /** Cross-field / server error message. */\n serverError: string | null;\n status: FormStatus;\n /** True if values have been touched at least once. */\n dirty: boolean;\n}\n\nexport type ContactFormStateListener = (state: ContactFormState) => void;\n\nexport interface ContactFormStateOptions {\n /**\n * The onboarder publishes the interest question as required (its spec's\n * `forms.contact` field `interestModules`), so an empty choice is a field\n * error rather than a lead that says nothing about what was asked.\n */\n requireInterest?: boolean;\n}\n\nexport interface ContactFormControl {\n /** Current snapshot of form state. */\n getState(): ContactFormState;\n /** Update a single field value and re-validate if dirty. */\n setValue<K extends keyof ContactFormValues>(\n field: K,\n value: ContactFormValues[K],\n ): void;\n /**\n * Run full client-side validation. Returns true if valid.\n * Also marks the form dirty so future setValue calls auto-validate.\n */\n validate(): boolean;\n /** Mark the form as submitting. Called by the submit client. */\n setSubmitting(): void;\n /** Mark the form as successfully submitted. */\n setSuccess(): void;\n /** Set a server-side error (e.g. 429 rate limited). */\n setError(message: string): void;\n /** Reset to initial idle state. */\n reset(): void;\n /** Subscribe to state changes. Returns unsubscribe function. */\n subscribe(listener: ContactFormStateListener): () => void;\n}\n\nfunction zodIssuesToFieldErrors(issues: ZodIssue[]): FieldError[] {\n return issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n }));\n}\n\nconst INITIAL_VALUES: ContactFormValues = {\n name: undefined,\n companyName: \"\",\n contactName: \"\",\n email: \"\",\n phone: undefined,\n message: undefined,\n interestModules: [],\n sizeBand: undefined,\n attribution: undefined,\n privacyNoticeAcknowledged: false,\n marketingOptIn: false,\n intakeKind: \"enquiry\",\n};\n\n/**\n * Create a new headless contact form state controller.\n *\n * @example\n * ```ts\n * const form = createContactFormState();\n * form.subscribe((state) => renderUI(state));\n * form.setValue(\"name\", \"Alice\");\n * const valid = form.validate();\n * if (valid) await submitContact(form.getState().values, botToken);\n * ```\n */\nexport function createContactFormState(\n options: ContactFormStateOptions = {},\n): ContactFormControl {\n let state: ContactFormState = {\n values: { ...INITIAL_VALUES },\n errors: [],\n serverError: null,\n status: \"idle\",\n dirty: false,\n };\n\n const listeners = new Set<ContactFormStateListener>();\n\n function emit(): void {\n for (const listener of listeners) {\n listener({ ...state, values: { ...state.values } });\n }\n }\n\n function runValidation(): FieldError[] {\n const result = contactFormSchema.safeParse(state.values);\n const errors = result.success\n ? []\n : zodIssuesToFieldErrors(result.error.issues);\n if (options.requireInterest && state.values.interestModules.length === 0) {\n errors.push({ path: \"interestModules\", message: \"Choose at least one\" });\n }\n return errors;\n }\n\n // Once validate() has run at least once, EVERY subsequent setValue\n // revalidates — errors both clear when a field is fixed AND reappear\n // when a fixed field is broken again. (Previously the revalidation was\n // gated on errors.length > 0, so fix-then-rebreak silently showed no\n // error until the next explicit validate().)\n let hasValidated = false;\n\n return {\n getState() {\n return { ...state, values: { ...state.values } };\n },\n\n setValue(field, value) {\n state = {\n ...state,\n values: { ...state.values, [field]: value },\n dirty: true,\n };\n if (hasValidated) {\n state = { ...state, errors: runValidation() };\n }\n emit();\n },\n\n validate() {\n hasValidated = true;\n const errors = runValidation();\n state = { ...state, errors, dirty: true };\n emit();\n return errors.length === 0;\n },\n\n setSubmitting() {\n state = { ...state, status: \"submitting\", serverError: null, errors: [] };\n emit();\n },\n\n setSuccess() {\n state = { ...state, status: \"success\", serverError: null };\n emit();\n },\n\n setError(message) {\n state = { ...state, status: \"error\", serverError: message };\n emit();\n },\n\n reset() {\n hasValidated = false;\n state = {\n values: { ...INITIAL_VALUES },\n errors: [],\n serverError: null,\n status: \"idle\",\n dirty: false,\n };\n emit();\n },\n\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"]}
|