@parag.more.withsurface.com/forms-sdk 0.1.4
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 +63 -0
- package/dist/index.cjs +2737 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.mjs +2714 -0
- package/dist/index.mjs.map +7 -0
- package/dist/react/index.cjs +44 -0
- package/dist/react/index.cjs.map +7 -0
- package/dist/react/index.mjs +21 -0
- package/dist/react/index.mjs.map +7 -0
- package/dist/surface-forms-sdk.min.js +2 -0
- package/dist/surface-forms-sdk.min.js.map +7 -0
- package/dist/types/bind/bindForm.d.ts +43 -0
- package/dist/types/bind/bindForm.d.ts.map +1 -0
- package/dist/types/bind/bindForm.test.d.ts +2 -0
- package/dist/types/bind/bindForm.test.d.ts.map +1 -0
- package/dist/types/bind/bookingWidget.d.ts +35 -0
- package/dist/types/bind/bookingWidget.d.ts.map +1 -0
- package/dist/types/bind/bookingWidget.test.d.ts +2 -0
- package/dist/types/bind/bookingWidget.test.d.ts.map +1 -0
- package/dist/types/core/client/emailValidation.d.ts +23 -0
- package/dist/types/core/client/emailValidation.d.ts.map +1 -0
- package/dist/types/core/client/emailValidation.test.d.ts +2 -0
- package/dist/types/core/client/emailValidation.test.d.ts.map +1 -0
- package/dist/types/core/client/responses.d.ts +35 -0
- package/dist/types/core/client/responses.d.ts.map +1 -0
- package/dist/types/core/client/responses.test.d.ts +2 -0
- package/dist/types/core/client/responses.test.d.ts.map +1 -0
- package/dist/types/core/client/runtimeConfig.contract.test.d.ts +2 -0
- package/dist/types/core/client/runtimeConfig.contract.test.d.ts.map +1 -0
- package/dist/types/core/client/runtimeConfig.d.ts +72 -0
- package/dist/types/core/client/runtimeConfig.d.ts.map +1 -0
- package/dist/types/core/client/runtimeConfig.test.d.ts +2 -0
- package/dist/types/core/client/runtimeConfig.test.d.ts.map +1 -0
- package/dist/types/core/client/scheduler.contract.test.d.ts +2 -0
- package/dist/types/core/client/scheduler.contract.test.d.ts.map +1 -0
- package/dist/types/core/client/scheduler.d.ts +121 -0
- package/dist/types/core/client/scheduler.d.ts.map +1 -0
- package/dist/types/core/client/scheduler.test.d.ts +2 -0
- package/dist/types/core/client/scheduler.test.d.ts.map +1 -0
- package/dist/types/core/client/transport.d.ts +56 -0
- package/dist/types/core/client/transport.d.ts.map +1 -0
- package/dist/types/core/client/transport.test.d.ts +2 -0
- package/dist/types/core/client/transport.test.d.ts.map +1 -0
- package/dist/types/core/engine/answerAudit.d.ts +21 -0
- package/dist/types/core/engine/answerAudit.d.ts.map +1 -0
- package/dist/types/core/engine/answerAudit.test.d.ts +2 -0
- package/dist/types/core/engine/answerAudit.test.d.ts.map +1 -0
- package/dist/types/core/engine/answerStates.contract.test.d.ts +2 -0
- package/dist/types/core/engine/answerStates.contract.test.d.ts.map +1 -0
- package/dist/types/core/engine/answerStates.d.ts +92 -0
- package/dist/types/core/engine/answerStates.d.ts.map +1 -0
- package/dist/types/core/engine/formEngine.d.ts +145 -0
- package/dist/types/core/engine/formEngine.d.ts.map +1 -0
- package/dist/types/core/engine/formEngine.test.d.ts +2 -0
- package/dist/types/core/engine/formEngine.test.d.ts.map +1 -0
- package/dist/types/core/engine/redirect.d.ts +36 -0
- package/dist/types/core/engine/redirect.d.ts.map +1 -0
- package/dist/types/core/engine/redirect.test.d.ts +2 -0
- package/dist/types/core/engine/redirect.test.d.ts.map +1 -0
- package/dist/types/core/engine/responsePayload.contract.test.d.ts +2 -0
- package/dist/types/core/engine/responsePayload.contract.test.d.ts.map +1 -0
- package/dist/types/core/engine/responsePayload.d.ts +74 -0
- package/dist/types/core/engine/responsePayload.d.ts.map +1 -0
- package/dist/types/core/engine/responsePayload.test.d.ts +2 -0
- package/dist/types/core/engine/responsePayload.test.d.ts.map +1 -0
- package/dist/types/core/engine/urlData.contract.test.d.ts +2 -0
- package/dist/types/core/engine/urlData.contract.test.d.ts.map +1 -0
- package/dist/types/core/engine/urlData.d.ts +30 -0
- package/dist/types/core/engine/urlData.d.ts.map +1 -0
- package/dist/types/core/engine/urlData.test.d.ts +2 -0
- package/dist/types/core/engine/urlData.test.d.ts.map +1 -0
- package/dist/types/core/engine/urlPrefill.d.ts +21 -0
- package/dist/types/core/engine/urlPrefill.d.ts.map +1 -0
- package/dist/types/core/engine/urlPrefill.test.d.ts +2 -0
- package/dist/types/core/engine/urlPrefill.test.d.ts.map +1 -0
- package/dist/types/core/events/dropoffDetection.d.ts +21 -0
- package/dist/types/core/events/dropoffDetection.d.ts.map +1 -0
- package/dist/types/core/events/dropoffDetection.test.d.ts +2 -0
- package/dist/types/core/events/dropoffDetection.test.d.ts.map +1 -0
- package/dist/types/core/events/emitter.d.ts +23 -0
- package/dist/types/core/events/emitter.d.ts.map +1 -0
- package/dist/types/core/events/emitter.test.d.ts +2 -0
- package/dist/types/core/events/emitter.test.d.ts.map +1 -0
- package/dist/types/core/events/formJourneyTracker.contract.test.d.ts +2 -0
- package/dist/types/core/events/formJourneyTracker.contract.test.d.ts.map +1 -0
- package/dist/types/core/events/formJourneyTracker.d.ts +69 -0
- package/dist/types/core/events/formJourneyTracker.d.ts.map +1 -0
- package/dist/types/core/events/formJourneyTracker.test.d.ts +2 -0
- package/dist/types/core/events/formJourneyTracker.test.d.ts.map +1 -0
- package/dist/types/core/events/pushEvents.contract.test.d.ts +2 -0
- package/dist/types/core/events/pushEvents.contract.test.d.ts.map +1 -0
- package/dist/types/core/events/pushEvents.d.ts +62 -0
- package/dist/types/core/events/pushEvents.d.ts.map +1 -0
- package/dist/types/core/events/pushEvents.test.d.ts +2 -0
- package/dist/types/core/events/pushEvents.test.d.ts.map +1 -0
- package/dist/types/core/events/startedDetection.d.ts +11 -0
- package/dist/types/core/events/startedDetection.d.ts.map +1 -0
- package/dist/types/core/events/startedDetection.test.d.ts +2 -0
- package/dist/types/core/events/startedDetection.test.d.ts.map +1 -0
- package/dist/types/core/events/viewDetection.d.ts +15 -0
- package/dist/types/core/events/viewDetection.d.ts.map +1 -0
- package/dist/types/core/events/viewDetection.test.d.ts +2 -0
- package/dist/types/core/events/viewDetection.test.d.ts.map +1 -0
- package/dist/types/core/identity/browserFingerprint.d.ts +40 -0
- package/dist/types/core/identity/browserFingerprint.d.ts.map +1 -0
- package/dist/types/core/identity/browserFingerprint.test.d.ts +2 -0
- package/dist/types/core/identity/browserFingerprint.test.d.ts.map +1 -0
- package/dist/types/core/identity/ensureIdentified.d.ts +27 -0
- package/dist/types/core/identity/ensureIdentified.d.ts.map +1 -0
- package/dist/types/core/identity/ensureIdentified.test.d.ts +2 -0
- package/dist/types/core/identity/ensureIdentified.test.d.ts.map +1 -0
- package/dist/types/core/identity/identify.contract.test.d.ts +2 -0
- package/dist/types/core/identity/identify.contract.test.d.ts.map +1 -0
- package/dist/types/core/identity/identify.d.ts +48 -0
- package/dist/types/core/identity/identify.d.ts.map +1 -0
- package/dist/types/core/identity/identify.test.d.ts +2 -0
- package/dist/types/core/identity/identify.test.d.ts.map +1 -0
- package/dist/types/core/identity/journey.contract.test.d.ts +2 -0
- package/dist/types/core/identity/journey.contract.test.d.ts.map +1 -0
- package/dist/types/core/identity/journey.d.ts +28 -0
- package/dist/types/core/identity/journey.d.ts.map +1 -0
- package/dist/types/core/identity/journey.test.d.ts +2 -0
- package/dist/types/core/identity/journey.test.d.ts.map +1 -0
- package/dist/types/core/identity/journey.testStubs.d.ts +5 -0
- package/dist/types/core/identity/journey.testStubs.d.ts.map +1 -0
- package/dist/types/core/identity/leadDataCache.d.ts +27 -0
- package/dist/types/core/identity/leadDataCache.d.ts.map +1 -0
- package/dist/types/core/identity/leadDataCache.test.d.ts +2 -0
- package/dist/types/core/identity/leadDataCache.test.d.ts.map +1 -0
- package/dist/types/enrichment/attachEnrichment.d.ts +35 -0
- package/dist/types/enrichment/attachEnrichment.d.ts.map +1 -0
- package/dist/types/enrichment/attachEnrichment.test.d.ts +2 -0
- package/dist/types/enrichment/attachEnrichment.test.d.ts.map +1 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/react/index.d.ts +2 -0
- package/dist/types/react/index.d.ts.map +1 -0
- package/dist/types/react/ssr.test.d.ts +2 -0
- package/dist/types/react/ssr.test.d.ts.map +1 -0
- package/dist/types/react/useSurfaceForm.d.ts +25 -0
- package/dist/types/react/useSurfaceForm.d.ts.map +1 -0
- package/dist/types/react/useSurfaceForm.test.d.ts +2 -0
- package/dist/types/react/useSurfaceForm.test.d.ts.map +1 -0
- package/dist/types/surfaceForms.d.ts +120 -0
- package/dist/types/surfaceForms.d.ts.map +1 -0
- package/dist/types/surfaceForms.test.d.ts +2 -0
- package/dist/types/surfaceForms.test.d.ts.map +1 -0
- package/dist/types/tracking/attachTracking.d.ts +35 -0
- package/dist/types/tracking/attachTracking.d.ts.map +1 -0
- package/dist/types/tracking/attachTracking.test.d.ts +2 -0
- package/dist/types/tracking/attachTracking.test.d.ts.map +1 -0
- package/dist/types/tracking/conversionRules.contract.test.d.ts +2 -0
- package/dist/types/tracking/conversionRules.contract.test.d.ts.map +1 -0
- package/dist/types/tracking/conversionRules.d.ts +87 -0
- package/dist/types/tracking/conversionRules.d.ts.map +1 -0
- package/dist/types/tracking/conversionRules.test.d.ts +2 -0
- package/dist/types/tracking/conversionRules.test.d.ts.map +1 -0
- package/dist/types/tracking/vendors.d.ts +46 -0
- package/dist/types/tracking/vendors.d.ts.map +1 -0
- package/dist/types/tracking/vendors.test.d.ts +2 -0
- package/dist/types/tracking/vendors.test.d.ts.map +1 -0
- package/dist/types/validation/attachEmailValidation.d.ts +8 -0
- package/dist/types/validation/attachEmailValidation.d.ts.map +1 -0
- package/dist/types/validation/attachEmailValidation.test.d.ts +2 -0
- package/dist/types/validation/attachEmailValidation.test.d.ts.map +1 -0
- package/docs/api.md +310 -0
- package/docs/reference/binding.md +116 -0
- package/docs/reference/clients.md +212 -0
- package/docs/reference/engine.md +193 -0
- package/docs/reference/events.md +93 -0
- package/docs/reference/identity.md +116 -0
- package/llms.txt +317 -0
- package/package.json +25 -0
- package/skills/surface-forms-sdk/SKILL.md +153 -0
- package/skills/surface-forms-sdk/debugging.md +64 -0
- package/skills/surface-forms-sdk/editing.md +67 -0
- package/skills/surface-forms-sdk/patterns.md +222 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Reference — DOM binding, booking widget, React adapter
|
|
2
|
+
|
|
3
|
+
Exhaustive reference for the `bind/` layer and the React subpath. Map of everything:
|
|
4
|
+
[`../api.md`](../api.md). Concrete HTML shapes:
|
|
5
|
+
[`../../skills/surface-forms-sdk/patterns.md`](../../skills/surface-forms-sdk/patterns.md).
|
|
6
|
+
|
|
7
|
+
## `bindForm({engine, root, expectedQuestionIds?}): () => void`
|
|
8
|
+
|
|
9
|
+
Makes plain `data-*` HTML drive a `FormEngine` with zero authored JS. `init` calls it when
|
|
10
|
+
`container` is passed; use directly with your own engine. Returns a full teardown. Exact
|
|
11
|
+
behavior:
|
|
12
|
+
|
|
13
|
+
### Field discovery + resolution
|
|
14
|
+
|
|
15
|
+
- Bound elements are everything with `data-question-id` under `root` (the element itself when it
|
|
16
|
+
IS an `input`/`select`/`textarea`, otherwise every such control inside it).
|
|
17
|
+
- **Nearest-wrapper rule**: a control inside a NESTED `data-question-id` wrapper belongs to the
|
|
18
|
+
inner question only — it never also binds an outer one.
|
|
19
|
+
- **Field name per control** (`fieldNameFor`): wrapper `data-field-name` ?? control
|
|
20
|
+
`data-field-name` ?? inferred — `"selected"` for a `<select>` on a Dropdown, `"email"` for
|
|
21
|
+
`input[type=email]`, else `"input"`.
|
|
22
|
+
- **Question type** (`questionTypeFor`): wrapper `data-question-type` ?? control attr ??
|
|
23
|
+
inferred — `input[type=email]` → `EmailForm`, else `ShortInput`. Every answer carries `type`
|
|
24
|
+
(the wire validates a discriminated union).
|
|
25
|
+
- **Values**: checkboxes contribute `checked` (boolean), radios their `value`, everything else
|
|
26
|
+
`field.value`. Multi-field components accumulate into ONE state per question
|
|
27
|
+
(`{firstName, lastName, …}`).
|
|
28
|
+
- **Pre-filled values are seeded at bind time**: non-empty DOM values (SSR, autofill, values set
|
|
29
|
+
before init resolved) are captured immediately, not only on the next input event.
|
|
30
|
+
|
|
31
|
+
### Choice groups (`CHOICE_ANSWER_BUILDERS`)
|
|
32
|
+
|
|
33
|
+
For containers whose type is one of the five choice components, the answer is built from the
|
|
34
|
+
whole radio/checkbox group (never per control; `data-field-name` is ignored on the options):
|
|
35
|
+
|
|
36
|
+
| Type | Built answer |
|
|
37
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
38
|
+
| `MultipleOptionsQuestion` | `{choices: [{key: option.value, value: option.checked, index}, …]}` — the FULL option list |
|
|
39
|
+
| `MultipleChoiceQuestion` / `MMChoiceQuestion` | `{choices: [{key, value}, …]}` — full list, no index |
|
|
40
|
+
| `GraphicOptions` / `MultipleChoiceSingleAnswer` | `{value, index}` of the checked option (`{value: "", index: -1}` when none) |
|
|
41
|
+
|
|
42
|
+
Control `value` attributes must equal the record's option keys exactly. A stray text control in
|
|
43
|
+
the group (an "Other" input) is left to the author's own `setAnswer`.
|
|
44
|
+
|
|
45
|
+
### Navigation markers
|
|
46
|
+
|
|
47
|
+
Selector: `[data-surface-goto-step], .surface-disqualify-button, .surface-submit-button,
|
|
48
|
+
.surface-next-button`. ONE action per element, most specific wins, in priority order: `goto` →
|
|
49
|
+
`disqualify` → `submit` → `next`. `goto` failures (unknown step) are caught and console-warned,
|
|
50
|
+
never thrown into the page. A `<form>` root's native `submit` event is also intercepted
|
|
51
|
+
(`preventDefault`) and finishes the form.
|
|
52
|
+
|
|
53
|
+
### Step visibility
|
|
54
|
+
|
|
55
|
+
On every `stepChanged` (and at bind), each `[data-step-id]` container gets
|
|
56
|
+
`hidden = (its id !== currentStepId)` — one visible step at a time. Single-step records may omit
|
|
57
|
+
containers. (Author containers after the first as `hidden` in the HTML to avoid the pre-init
|
|
58
|
+
flash.)
|
|
59
|
+
|
|
60
|
+
### View detection + drift warning
|
|
61
|
+
|
|
62
|
+
- `observeFormView(root, () => engine.markViewed())` is wired automatically.
|
|
63
|
+
- `expectedQuestionIds` (init derives them from the record) minus the ids actually bound →
|
|
64
|
+
a console warning naming the unbound ids; the runtime counterpart of `validate_form_html`.
|
|
65
|
+
|
|
66
|
+
## `mountBookingWidget(options): () => void`
|
|
67
|
+
|
|
68
|
+
The unstyled Surface-scheduler booking flow (date → time → name/email form → booked) over a
|
|
69
|
+
`SchedulerClient` ([`clients.md`](clients.md)). Never iframe the hosted booking page.
|
|
70
|
+
|
|
71
|
+
### Options (`BookingWidgetOptions`)
|
|
72
|
+
|
|
73
|
+
| Option | Type | Semantics |
|
|
74
|
+
| ------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
75
|
+
| `container` | `Element` | The widget appends its own `div.surface-booking` root here; teardown removes it. |
|
|
76
|
+
| `scheduler` | `SchedulerClient` | From `createSchedulerClient` (share the transport; pass `preview` there for synthetic bookings). |
|
|
77
|
+
| `eventTypeId` | `string` | From the record's `CalendlyScreen` content. |
|
|
78
|
+
| `timezone` | `string?` | Defaults to the visitor's (`Intl.DateTimeFormat().resolvedOptions().timeZone`); slot labels format in it. |
|
|
79
|
+
| `duration` | `number?` | Threaded into availability/slots/book queries. |
|
|
80
|
+
| `prefill` | `{name?, email?}?` | Pre-fills the widget's name/email inputs. |
|
|
81
|
+
| `identity` | `{responseId?, leadSessionId?, fingerprint?}?` | Threaded into `book()` for booking↔response linkage. |
|
|
82
|
+
| `formatters` | `{month?, date?, time?}?` | Override the copy: `month("YYYY-MM")`, `date("YYYY-MM-DD")`, `time(slot)` (ISO instants; format in the widget timezone). Defaults are locale-formatted via `Intl`. |
|
|
83
|
+
| `onComplete` | `(booking: Booking) => void` | Fires after a successful booking — persist via `setAnswer({...toSchedulerAnswerState(booking), type: "<record type>"})` and navigate. |
|
|
84
|
+
|
|
85
|
+
### Behavior + DOM
|
|
86
|
+
|
|
87
|
+
- Every month/date/slot element carries its raw value in `data-value` (`YYYY-MM`, `YYYY-MM-DD`,
|
|
88
|
+
ISO instant) — relabel or assert without parsing text.
|
|
89
|
+
- Async renders are generation-guarded: a stale fetch resolving after navigation never touches
|
|
90
|
+
the DOM; teardown ignores in-flight results.
|
|
91
|
+
- Errors render `p.surface-booking-error` + `button.surface-booking-retry`; a failed booking
|
|
92
|
+
keeps the form up with the error appended; the submit button disables while booking.
|
|
93
|
+
- No CSS is injected; the full class tree (also in `llms.txt`):
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
div.surface-booking
|
|
97
|
+
├─ p.surface-booking-month [data-value] (or p.surface-booking-loading / error+retry)
|
|
98
|
+
├─ div.surface-booking-month-nav > button.surface-booking-month-button ×2
|
|
99
|
+
└─ ul.surface-booking-dates > li.surface-booking-date > button.surface-booking-date-button [data-value]
|
|
100
|
+
(li.surface-booking-no-dates when empty)
|
|
101
|
+
-- date picked -- p.surface-booking-day [data-value] · button.surface-booking-back
|
|
102
|
+
ul.surface-booking-slots > li.surface-booking-slot > button.surface-booking-slot-button [data-value]
|
|
103
|
+
-- slot picked -- form.surface-booking-form > input.surface-booking-name + input.surface-booking-email + button.surface-booking-submit
|
|
104
|
+
-- booked -- p.surface-booking-booked
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## React — `useSurfaceForm(engine): UseSurfaceFormResult` (from `<package>/react`)
|
|
108
|
+
|
|
109
|
+
`useSyncExternalStore` adapter over any `FormEngine` (including the `init` handle). Returns
|
|
110
|
+
`{state, setAnswer, next, back, goToStep, submit, disqualify, savePartial, markViewed, on}` —
|
|
111
|
+
`state` re-renders on engine mutations and is referentially stable between them. React ≥18 is an
|
|
112
|
+
optional peer dependency; the main entry stays React-free. One engine per mounted form;
|
|
113
|
+
`destroy()` on unmount (see the SPA pattern in
|
|
114
|
+
[`patterns.md`](../../skills/surface-forms-sdk/patterns.md)); init-level members not on the hook
|
|
115
|
+
(`flush`, `mergeMeta`, `validateEmail`, `capabilities`, `identify`, `destroy`) are read from the
|
|
116
|
+
handle itself.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Reference — transport, runtime config, responses, scheduler, email validation, enrichment
|
|
2
|
+
|
|
3
|
+
Exhaustive reference for the `core/client` domain plus the watcher modules that ride on it.
|
|
4
|
+
Map of everything: [`../api.md`](../api.md).
|
|
5
|
+
|
|
6
|
+
## Transport (`core/client/transport`)
|
|
7
|
+
|
|
8
|
+
- **`DEFAULT_API_BASE_URL`** = `"https://forms.withsurface.com"`.
|
|
9
|
+
- **`SDK_VERSION`** rides every request: header `X-Surface-Sdk-Version` on fetch paths, `?sdv=`
|
|
10
|
+
query param on beacons (sendBeacon cannot carry headers).
|
|
11
|
+
- **`createTransport({apiBaseUrl?}): Transport`** — trailing slash stripped; all paths resolve
|
|
12
|
+
absolute against it (SDK pages are customer-hosted, always cross-origin).
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
type Transport = {
|
|
16
|
+
get: <T>(path) => Promise<TransportResult<T>>;
|
|
17
|
+
post: <T>(
|
|
18
|
+
path,
|
|
19
|
+
payload,
|
|
20
|
+
opts?: { keepalive?: boolean; credentials?: boolean }
|
|
21
|
+
) => Promise<TransportResult<T>>;
|
|
22
|
+
/** true only when sendBeacon queued it; the fire-and-forget fetch fallback reports false. */
|
|
23
|
+
beacon: (path, payload) => boolean;
|
|
24
|
+
};
|
|
25
|
+
type TransportResult<T> = { ok: true; data: T } | { ok: false; error: TransportError };
|
|
26
|
+
type TransportError = { kind: "network"; cause: unknown } | { kind: "http"; status: number; body?: unknown };
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Never throws — always a typed result (the never-break-the-host-page guarantee). `credentials:
|
|
30
|
+
true` sends cookies (the `{formId}_pa` repeat-visitor cookie lives on the API origin;
|
|
31
|
+
`SameSite=None; Secure`, so only credentialed requests carry it cross-origin). The beacon
|
|
32
|
+
fallback fetch is always credentialed (matches sendBeacon's behavior); unserializable payloads
|
|
33
|
+
are swallowed.
|
|
34
|
+
|
|
35
|
+
## Runtime config (`core/client/runtimeConfig`)
|
|
36
|
+
|
|
37
|
+
- **`fetchRuntimeConfig(transport, formId, {preview?, previewToken?})`** →
|
|
38
|
+
`GET /api/v1/public/forms/{formId}/runtime-config` — the same payload the hosted page reads.
|
|
39
|
+
`preview: true` appends `?preview=1` (+`&pt=<previewToken>`): a NEVER-published form serves its
|
|
40
|
+
draft config when the token validates; otherwise the published config (a placeholder step for
|
|
41
|
+
never-published forms). `settings.password` never arrives (server-stripped; gated forms
|
|
42
|
+
validate via `POST /api/v1/check-password`).
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
type RuntimeConfig = {
|
|
46
|
+
id: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
environmentId?: string;
|
|
49
|
+
config: { steps?: Array<{ id; endStepKind?; components? }> } & Record<string, unknown>;
|
|
50
|
+
settings?: ({ trackPartialResponses?: boolean; trackCookie?: boolean } & Record<string, unknown>) | null;
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Helpers over a fetched config (all tolerate grid-layout or flat component arrays):
|
|
55
|
+
|
|
56
|
+
- **`stepsOf(config)`** → ordered `{id, endStepKind?}[]` — the engine's `steps` input.
|
|
57
|
+
- **`statefulQuestionsOf(config)`** → `{questionId, componentType?}[]` (answer-recording
|
|
58
|
+
components); **`statefulQuestionIdsOf(config)`** → just the ids.
|
|
59
|
+
- **`enrichmentSourcesOf(config)`** → `{questionId, fieldNames}[]`: IdentityInfo fires on its
|
|
60
|
+
configured `enrichmentSourceField` (default `emailAddress`) and always on `websiteUrl`;
|
|
61
|
+
EmailForm on `email`.
|
|
62
|
+
- **`hasEnabledEnrichmentProvider(settings)`** — v2 enrichment config with ≥1 enabled provider
|
|
63
|
+
exposing fields (the runtime gate for automatic enrichment).
|
|
64
|
+
|
|
65
|
+
## Responses (`core/client/responses`)
|
|
66
|
+
|
|
67
|
+
- **`submitResponse(transport, payload)`** → `POST /api/v1/responses/` (awaited, credentialed —
|
|
68
|
+
reads/refreshes the `{formId}_pa` cookie). Returns
|
|
69
|
+
`CreateResponseResult = { responseId, leadId: string|null, resumeToken: string|null }`; a 200
|
|
70
|
+
without an id is normalized to an http error.
|
|
71
|
+
- **`updateResponse(transport, payload)`** → beacon to `/api/v1/responses/update`
|
|
72
|
+
(fire-and-forget; the engine uses an awaited keepalive post for the FINISHING update — see
|
|
73
|
+
[`engine.md`](engine.md)).
|
|
74
|
+
|
|
75
|
+
## Scheduler (`core/client/scheduler`)
|
|
76
|
+
|
|
77
|
+
- **`createSchedulerClient({transport, preview?}): SchedulerClient`** — public Surface-scheduler
|
|
78
|
+
endpoints; shapes mirror the native booking UI byte-for-byte. `preview: true`: queries carry
|
|
79
|
+
`preview=true`, and `book()` resolves a fabricated booking with NO network call.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
type SchedulerClient = {
|
|
83
|
+
getPublicEventType: <T = unknown>(eventTypeId) => Promise<TransportResult<T>>;
|
|
84
|
+
getAvailabilitySummary: (args: {
|
|
85
|
+
eventTypeId;
|
|
86
|
+
timezone?;
|
|
87
|
+
}) => Promise<TransportResult<{ data?: AvailabilitySummary }>>;
|
|
88
|
+
getMonthAvailability: (args: {
|
|
89
|
+
eventTypeId;
|
|
90
|
+
month /* "YYYY-MM" */;
|
|
91
|
+
timezone;
|
|
92
|
+
duration?;
|
|
93
|
+
hostId?;
|
|
94
|
+
ownerEmail?;
|
|
95
|
+
}) => Promise<TransportResult<{ data?: { dates?: unknown[]; resolvedHost? } }>>;
|
|
96
|
+
getSlots: (args: {
|
|
97
|
+
eventTypeId;
|
|
98
|
+
date /* "YYYY-MM-DD" */;
|
|
99
|
+
timezone;
|
|
100
|
+
duration?;
|
|
101
|
+
hostId?;
|
|
102
|
+
ownerEmail?;
|
|
103
|
+
}) => Promise<TransportResult<{ data?: { slots?: TimeSlot[] } }>>;
|
|
104
|
+
book: (args: {
|
|
105
|
+
eventTypeId;
|
|
106
|
+
startTime;
|
|
107
|
+
endTime /* required; preview fabrication only, never sent */;
|
|
108
|
+
timezone;
|
|
109
|
+
inviteeName;
|
|
110
|
+
inviteeEmail;
|
|
111
|
+
duration?;
|
|
112
|
+
intakeResponses?;
|
|
113
|
+
guestEmails?;
|
|
114
|
+
captchaToken?;
|
|
115
|
+
responseId?;
|
|
116
|
+
leadSessionId?;
|
|
117
|
+
fingerprint?;
|
|
118
|
+
hostRouting?;
|
|
119
|
+
}) => Promise<TransportResult<Booking>>;
|
|
120
|
+
reschedule: (args: {
|
|
121
|
+
bookingId;
|
|
122
|
+
rescheduleToken;
|
|
123
|
+
newStartTime;
|
|
124
|
+
timezone;
|
|
125
|
+
}) => Promise<TransportResult<Booking>>;
|
|
126
|
+
};
|
|
127
|
+
type Booking = {
|
|
128
|
+
id;
|
|
129
|
+
startTime;
|
|
130
|
+
endTime;
|
|
131
|
+
timezone;
|
|
132
|
+
inviteeName;
|
|
133
|
+
inviteeEmail;
|
|
134
|
+
meetingJoinLink: string | null;
|
|
135
|
+
cancelToken;
|
|
136
|
+
rescheduleToken;
|
|
137
|
+
attendees: BookingAttendee[];
|
|
138
|
+
};
|
|
139
|
+
type BookingAttendee = { name; email; isHost: boolean };
|
|
140
|
+
type TimeSlot = { start: string; end: string }; // ISO instants
|
|
141
|
+
type AvailabilitySummary = {
|
|
142
|
+
has_availability;
|
|
143
|
+
available_slots_7d;
|
|
144
|
+
available_slots_14d;
|
|
145
|
+
available_slots_30d;
|
|
146
|
+
available_days_7d;
|
|
147
|
+
next_available_date: string | null;
|
|
148
|
+
total_available_slots;
|
|
149
|
+
rep_meeting_count_today;
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`book()`'s `responseId`/`leadSessionId`/`fingerprint` thread into the booking meta block — the
|
|
154
|
+
server links booking↔response with them; pass what you have.
|
|
155
|
+
|
|
156
|
+
- **`toSchedulerAnswerState(booking): Record<string, unknown>`** — the persisted scheduler
|
|
157
|
+
answer (`SchedulerAnswer` shape, [`engine.md`](engine.md)). Emits `type: "DynamicScheduler"` —
|
|
158
|
+
override `type` to the record's component type when persisting
|
|
159
|
+
(`{...toSchedulerAnswerState(b), type: "CalendlyScreen"}`).
|
|
160
|
+
|
|
161
|
+
The DOM widget over this client (`mountBookingWidget`) is in [`binding.md`](binding.md).
|
|
162
|
+
|
|
163
|
+
## Email validation (`core/client/emailValidation` + `validation/attachEmailValidation`)
|
|
164
|
+
|
|
165
|
+
- **`createEmailValidator({transport, environmentId, preview?}): EmailValidator`** —
|
|
166
|
+
`POST /api/v1/edge/emailValidation` (provider waterfall + the environment's spam blacklist;
|
|
167
|
+
vendor keys stay server-side).
|
|
168
|
+
- `validate(email)` / `validateBatch(emails)`; a batch posts only uncached addresses and shares
|
|
169
|
+
one request.
|
|
170
|
+
- **Per-address Promise cache** for the validator's lifetime — successes AND failures stick, so
|
|
171
|
+
nothing is ever billed twice and concurrent calls share the in-flight request.
|
|
172
|
+
- **Fail-open**: request failure or `validationSuccessfullyRan: false` →
|
|
173
|
+
`{isValid: true, status: "unknown", reason: "validation_failed", failed: true}`. The same
|
|
174
|
+
address can later resolve definitively — design for the transition, check `failed` to
|
|
175
|
+
soft-warn.
|
|
176
|
+
- `preview: true` → synthetic `{isValid: true, status: "preview"}` with no network.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
type EmailValidationResult = {
|
|
180
|
+
email;
|
|
181
|
+
isValid: boolean;
|
|
182
|
+
status?;
|
|
183
|
+
reason?;
|
|
184
|
+
blacklistEntryIds?: string[];
|
|
185
|
+
failed?: boolean;
|
|
186
|
+
};
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- **`attachEmailValidation({engine, validator}): () => void`** — the `emailValidation: true`
|
|
190
|
+
watcher: watches answer keys `email`/`emailAddress`/`workEmailAddress`, debounces 300 ms
|
|
191
|
+
(native's), flushes on `stepCompleted`/`completed`, then merges native's response-meta keys via
|
|
192
|
+
`engine.mergeMeta`: `isEmailValid` (ANY currently-present email valid; fail-open counts valid),
|
|
193
|
+
`emailValidation` (`{status, reason, blacklistEntryIds?}` of the first detailed verdict),
|
|
194
|
+
`emailValidationFailed` (any verdict failed). The aggregation window follows the LIVE answers —
|
|
195
|
+
a replaced address's verdict stops describing the response; verdicts landing after the finish
|
|
196
|
+
amend the stored response. Returns a teardown.
|
|
197
|
+
|
|
198
|
+
## Enrichment (`enrichment/attachEnrichment`)
|
|
199
|
+
|
|
200
|
+
- **`attachEnrichment({engine, transport, formId, sources}): () => void`** — real-time identity
|
|
201
|
+
enrichment. Watches the `sources` (`{questionId, fieldNames}[]`, from `enrichmentSourcesOf`);
|
|
202
|
+
a value that is a valid email or domain and stable for 1 s fires
|
|
203
|
+
`POST /api/v1/services/enrichment/realtime` ONCE per value (deduped, in-flight guarded);
|
|
204
|
+
`stepCompleted`/`completed` flush the pending debounce. The result is stored via
|
|
205
|
+
`engine.setExternalState(ENRICHMENT_STATE_ID, …)` — riding every save as `enrichedFields` and
|
|
206
|
+
readable at `state().externalStates.selfServeEnrichment.data` for caller autofill. The request
|
|
207
|
+
is never awaited (submission must not block on the vendor); a post-finish result amends the
|
|
208
|
+
stored response. Returns a teardown.
|
|
209
|
+
- **`ENRICHMENT_STATE_ID`** = `"selfServeEnrichment"`; **`EnrichmentSource`** =
|
|
210
|
+
`{questionId, fieldNames}`.
|
|
211
|
+
- `init` wires this automatically only when the record was fetched AND
|
|
212
|
+
`hasEnabledEnrichmentProvider(settings)` — check `form.capabilities.enrichment`.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Reference — engine, answer states, payloads, page-context helpers
|
|
2
|
+
|
|
3
|
+
Exhaustive reference for the `core/engine` domain. Map of everything: [`../api.md`](../api.md).
|
|
4
|
+
|
|
5
|
+
## `createFormEngine(options): FormEngine`
|
|
6
|
+
|
|
7
|
+
The headless core: answers, traversal, response writes, lifecycle events. `SurfaceForms.init`
|
|
8
|
+
creates one for you; construct directly for offline/custom stacks.
|
|
9
|
+
|
|
10
|
+
### Options
|
|
11
|
+
|
|
12
|
+
| Option | Type | Semantics |
|
|
13
|
+
| ----------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| `formId` | `string` | Required. |
|
|
15
|
+
| `environmentId` | `string?` | Stamped on `form_viewed`/`form_completed` events. |
|
|
16
|
+
| `steps` | `Array<{id, endStepKind?: "thank_you"\|"disqualified"}>` | Required, ≥1 (throws on empty). The ordered data contract; `endStepKind` drives the ending-step reveal. |
|
|
17
|
+
| `transport` | `Transport` | Required. |
|
|
18
|
+
| `resolveNextStep` | `(currentStepId, answers) => {stepId, reason?} \| "END" \| "INVALID"` | Routing hook consulted by `next()`/`submit()`. `"END"` finishes, `"INVALID"` disqualifies, a step object advances (its `reason` becomes the journey transition reason). Default: linear order, `"END"` after the last step. |
|
|
19
|
+
| `trackPartialResponses` | `boolean` (default `true`) | Gates every mid-form write. The FINISHING write always happens regardless. |
|
|
20
|
+
| `trackCookies` | `boolean` (default `false`) | Rides the payload's cookie-meta gate. |
|
|
21
|
+
| `externalStates` | `ExternalStates?` | Seed pseudo-answers (e.g. a `URL_DATA` entry). |
|
|
22
|
+
| `meta` | `Record<string, unknown>?` | Seed response meta; mutated in place by `mergeMeta` (rides every write by reference). |
|
|
23
|
+
| `identity` | `() => {fingerprint?, leadSessionId?, leadId?}` | A GETTER read at every write/event — a later re-identify attributes subsequent writes to the fresh session. |
|
|
24
|
+
| `pushEvents` | `PushEventClient?` | Receives the lifecycle event stream (see [`events.md`](events.md)). |
|
|
25
|
+
| `formJourney` | `FormJourneyTracker?` | Receives step-journey visits/transitions/terminals. |
|
|
26
|
+
| `preview` | `boolean` (default `false`) | `write()` short-circuits — traversal and events run, responses are never created. |
|
|
27
|
+
| `resume` | `{responseId, answers?}` | Seeds the responseId (every write becomes an update — no duplicate row) and the stored answers (cloned, keyed under the FIRST step — the wire flattens steps; presentation is the caller's navigation). |
|
|
28
|
+
|
|
29
|
+
### Methods
|
|
30
|
+
|
|
31
|
+
- **`setAnswer(questionId, state)`** — merges (`{...prev, ...state}`) into the current step's
|
|
32
|
+
answer for that question. Fires `started` (and the `form_started` event) exactly once, on the
|
|
33
|
+
first deep divergence from the initial answers. Notifies subscribers.
|
|
34
|
+
- **`next(): Promise<void>`** — resolves the next step. Not finished → emits `stepCompleted`,
|
|
35
|
+
pushes the `form_step_submitted`+`form_step_viewed` batch (`stepCompletionTime` = seconds since
|
|
36
|
+
engine creation), records the journey transition, writes a partial (if enabled) stamped with the
|
|
37
|
+
step being LEFT, advances, emits `stepChanged`. Finished (`"END"`/`"INVALID"`/no next step) →
|
|
38
|
+
behaves like `submit()`/`disqualify()`.
|
|
39
|
+
- **`back(): void`** — pops history, emits `stepChanged`, records a `back_nav` journey transition.
|
|
40
|
+
NEVER writes. No-op when history is empty or the form finished.
|
|
41
|
+
- **`goToStep(stepId): Promise<void>`** — rejects unknown ids without touching state. Same-step
|
|
42
|
+
no-ops. A step already in history → rewinds like `back()` (no write). Otherwise a forward jump
|
|
43
|
+
with full forward semantics; journey reason `conditional_match` (the caller's own condition
|
|
44
|
+
chose the target).
|
|
45
|
+
- **`submit(): Promise<void>`** — finishes: terminal write (`finished: true`), then reveals the
|
|
46
|
+
record's `thank_you` step (display-only step change, no write), records the journey terminal,
|
|
47
|
+
pushes `form_completed`, emits `completed`. When it resolves, the server HAS the response — see
|
|
48
|
+
Delivery below.
|
|
49
|
+
- **`disqualify(): Promise<void>`** — finishes with `invalidResponse: true`; reveals the
|
|
50
|
+
`disqualified` ending; journey terminal carries
|
|
51
|
+
`disqualification: {reason: "conditional_match", atStepId}`; emits `disqualified`.
|
|
52
|
+
- **`savePartial(): Promise<void>`** — explicit mid-form write. The stepId is captured at CALL
|
|
53
|
+
time, so a save queued behind an in-flight advance stays attributed to the step the caller was
|
|
54
|
+
on. Skipped silently if the form finishes before it runs.
|
|
55
|
+
- **`flush(): Promise<void>`** — resolves when every write queued so far (including post-finish
|
|
56
|
+
amends) has settled.
|
|
57
|
+
- **`markViewed(): void`** — once: pushes `form_viewed` + first-step `form_step_viewed`, emits
|
|
58
|
+
`viewed`.
|
|
59
|
+
- **`setExternalState(id, {type, ...})`** — writes/replaces a pseudo-answer that rides every
|
|
60
|
+
subsequent save; post-finish it amends the stored response (below).
|
|
61
|
+
- **`mergeMeta(partial)`** — `Object.assign` onto `meta`; rides the next save or amends
|
|
62
|
+
post-finish. (The `init` handle wraps this with a reserved-key guard.)
|
|
63
|
+
- **`state(): FormEngineState`** — `{currentStepId, answers, externalStates, responseId,
|
|
64
|
+
resumeToken, finished}`. The snapshot is cached until the next mutation, so it is referentially
|
|
65
|
+
stable — the `useSyncExternalStore` contract.
|
|
66
|
+
- **`subscribe(listener): Unsubscribe`** — store subscription; fires on every state change.
|
|
67
|
+
- **`on(event, handler): Unsubscribe`** — see Events.
|
|
68
|
+
|
|
69
|
+
### Events (`FormEngineEvents`)
|
|
70
|
+
|
|
71
|
+
| Event | Payload | Fires |
|
|
72
|
+
| --------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
73
|
+
| `viewed` | `{formId}` | Once, from `markViewed`. |
|
|
74
|
+
| `started` | `{formId}` | Once, first answer divergence. |
|
|
75
|
+
| `stepChanged` | `{fromStepId, toStepId}` | Every step move, incl. rewinds and the ending reveal. |
|
|
76
|
+
| `stepCompleted` | `{stepId}` | Leaving a step forward (also flush signal for debounced watchers). |
|
|
77
|
+
| `completed` | `{formId}` | Successful non-disqualified finish. |
|
|
78
|
+
| `disqualified` | `{stepId}` | Disqualified finish. |
|
|
79
|
+
| `saved` | `{responseId, finished, resumeToken}` | After a CONFIRMED write: every create, every partial dispatch, the awaited terminal update. Never fires for a failed terminal write. |
|
|
80
|
+
| `error` | `{scope: "submit", error}` | Failed create (typed `TransportError`) or failed terminal update. |
|
|
81
|
+
|
|
82
|
+
### Write serialization + delivery
|
|
83
|
+
|
|
84
|
+
- One queue serializes every write-initiating call: a double-clicked `next()` joins the pending
|
|
85
|
+
advance (advances once); `submit()` during an in-flight `next()` queues behind it and then
|
|
86
|
+
finishes (submit intent never dropped); queued ops re-check `finished` when they run, so
|
|
87
|
+
nothing writes after the finishing submit.
|
|
88
|
+
- First forward write CREATES the response (`POST /api/v1/responses/`, awaited, credentialed);
|
|
89
|
+
later writes UPDATE it. Every write resends the FULL answer set + external states.
|
|
90
|
+
- A failed create: `error` event; `response_submission_error` pushEvent only for network-kind
|
|
91
|
+
failures (the server logs HTTP-level ones itself); the responseId stays null (the next write
|
|
92
|
+
retries the create).
|
|
93
|
+
- **The terminal (finishing) update is awaited** `fetch(keepalive)`: `submit()` resolving means
|
|
94
|
+
delivery is confirmed. On failure: `error` event + one best-effort beacon fallback, and `saved`
|
|
95
|
+
does NOT fire. Mid-form updates are beacon-first fire-and-forget (the server's drop-off sweep
|
|
96
|
+
reconciles missed ones).
|
|
97
|
+
- **Post-finish amends**: `setExternalState`/`mergeMeta` after the finish re-issue the terminal
|
|
98
|
+
update by extending the queue (full resends are idempotent; a result landing while the terminal
|
|
99
|
+
create is still in flight amends after it settles; a failed terminal create is never
|
|
100
|
+
resurrected as a new response).
|
|
101
|
+
|
|
102
|
+
## Typed answer states (`core/engine/answerStates`)
|
|
103
|
+
|
|
104
|
+
Exact wire shapes; contract-tested against the server schemas. `setAnswer` merges partials, so
|
|
105
|
+
patching one key of a multi-field state is legal.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
type ShortInputAnswer = { type: "ShortInput"; input: string };
|
|
109
|
+
type LongInputAnswer = { type: "LongInput"; input: string };
|
|
110
|
+
type EmailFormAnswer = { type: "EmailForm"; email: string; subscriptionCheck?: boolean };
|
|
111
|
+
type DropdownAnswer = { type: "Dropdown"; selected: string | string[] };
|
|
112
|
+
type DisclaimerAnswer = { type: "Disclaimer"; check?: boolean };
|
|
113
|
+
type FileUploaderAnswer = { type: "FileUploader"; fileUrl?: string };
|
|
114
|
+
// The ENTIRE option list, one entry per record option — never only the picked one:
|
|
115
|
+
type MultipleOptionsAnswer = { type: "MultipleOptionsQuestion";
|
|
116
|
+
choices: Array<{ key: string; value: boolean; index: number }>; selectedOther?: boolean };
|
|
117
|
+
type MMChoiceAnswer = { type: "MMChoiceQuestion"; choices: Array<{ key: string; value: boolean }> };
|
|
118
|
+
type MultipleChoiceAnswer = { type: "MultipleChoiceQuestion"; choices: Array<{ key: string; value: boolean }> };
|
|
119
|
+
type GraphicOptionsAnswer = { type: "GraphicOptions"; value: string; index: number };
|
|
120
|
+
type MultipleChoiceSingleAnswerAnswer = { type: "MultipleChoiceSingleAnswer"; value: string; index: number };
|
|
121
|
+
type IdentityInfoAnswer = { type: "IdentityInfo" } & Partial<Record<IdentityInfoAnswerKey, string>>;
|
|
122
|
+
type SchedulerAnswer = { type: "CalendlyScreen" | "DynamicScheduler"; eventScheduled: boolean;
|
|
123
|
+
uri: string; meetingTime?: string; meetingEndTime?: string; ownerEmails?: string;
|
|
124
|
+
guestEmails?: string; inviteeName?: string; googleMeetLink?: string };
|
|
125
|
+
type KnownAnswerState = /* union of all of the above */;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`IDENTITY_INFO_ANSWER_KEYS` (complete): `firstName`, `lastName`, `fullName`, `emailAddress`,
|
|
129
|
+
`workEmailAddress`, `phoneNumber`, `companyName`, `numberOfEmployees`, `websiteUrl`, `title`,
|
|
130
|
+
`streetAddress`, `aptSuiteEtc`, `city`, `state`, `country`, `zipCode`, `companyRevenue`,
|
|
131
|
+
`profilePictureUrl`.
|
|
132
|
+
|
|
133
|
+
## Response payload builders (`core/engine/responsePayload`)
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
type SingleFormResponse = { questionId: string; response: Record<string, unknown> };
|
|
137
|
+
type AnswerStates = Record<stepId, Record<questionId, Record<string, unknown>>>;
|
|
138
|
+
type ExternalStates = Record<stateId, { type: string } & Record<string, unknown>>;
|
|
139
|
+
type FormResponsePayload = {
|
|
140
|
+
formId;
|
|
141
|
+
responses: SingleFormResponse[];
|
|
142
|
+
enrichedFields;
|
|
143
|
+
finished;
|
|
144
|
+
invalidResponse;
|
|
145
|
+
stepId;
|
|
146
|
+
fingerprint?;
|
|
147
|
+
leadSessionId?;
|
|
148
|
+
meta;
|
|
149
|
+
responseShapeVersion: "2.0";
|
|
150
|
+
};
|
|
151
|
+
type FormUpdateResponsePayload = Omit<FormResponsePayload, "responseShapeVersion"> & { responseId };
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- **`buildResponsesArray(answers, externalStates)`** → `{responses, enrichedFields, bookingId?}` —
|
|
155
|
+
flattens ALL steps' answers plus every external state as a pseudo-answer keyed by its stateId;
|
|
156
|
+
skips `SCHEDULER_AVAILABILITY_DATA` (transient); detects a Surface-native booking (first answer
|
|
157
|
+
with `eventScheduled: true` and a non-Calendly/Cal.com `uri`) into `bookingId`; hoists the
|
|
158
|
+
enrichment state's fields into `enrichedFields` (existing keys win).
|
|
159
|
+
- **`createSubmitResponsePayload(options)`** / **`createUpdateResponsePayload(options)`** — the
|
|
160
|
+
full wire payloads; every write resends everything (server-side UTM preservation depends on the
|
|
161
|
+
`URL_DATA` entry riding every update). Cookie capture inside `meta` is gated by `trackCookies`.
|
|
162
|
+
|
|
163
|
+
## Page-context helpers
|
|
164
|
+
|
|
165
|
+
### `core/engine/urlData`
|
|
166
|
+
|
|
167
|
+
- **`buildUrlData({trackCookies?, defaultUrl?}): UrlData`** — the `URL_DATA` attribution entry:
|
|
168
|
+
`{type: "URL_DATA", fullUrl?, params?, referrer?, cookies?}` read directly from
|
|
169
|
+
`location`/`document` (self-hosted pages need no query-param relay). `defaultUrl` wins over
|
|
170
|
+
`location.href`; cookies only when `trackCookies`; malformed URLs are swallowed.
|
|
171
|
+
- **`constructUrlParamsObject(url)`** — query params as a record (repeated params: last wins,
|
|
172
|
+
pinned native quirk).
|
|
173
|
+
- **`parseCookiesObject(cookieString)`** — `document.cookie` → record.
|
|
174
|
+
|
|
175
|
+
### `core/engine/urlPrefill`
|
|
176
|
+
|
|
177
|
+
- **`parsePrefillParams(search): ParsedUrlQuestionId[]`** / **`parseQuestionId(input)`** — the
|
|
178
|
+
`?<questionId>_<fieldName>=value` prefill syntax. Splits on the FIRST underscore (field names
|
|
179
|
+
may contain more); both sides trimmed; `url`/`referrer`/`cookies` reserved for attribution;
|
|
180
|
+
params without an underscore ignored. `ParsedUrlQuestionId` =
|
|
181
|
+
`{originalId, questionId, fieldName, value}`.
|
|
182
|
+
|
|
183
|
+
### `core/engine/redirect`
|
|
184
|
+
|
|
185
|
+
- **`buildRedirectUrl(url, {leadId?, sessionId?})`** — appends `leadId`/`sessionId` query params
|
|
186
|
+
(identity handoff so the destination's tag/SDK resolves the same visitor). Blank → `""`;
|
|
187
|
+
unparseable → passed through unchanged. Protocol default pins native's `addProtocolToUrl`
|
|
188
|
+
including its quirk: the check is `startsWith("https")`, so an explicit `http://` URL gets
|
|
189
|
+
double-prefixed.
|
|
190
|
+
- **`performRedirect({url, redirectType?, leadId?, sessionId?, preview?})`** — `"CurrentWindow"`
|
|
191
|
+
(default) sets `window.location`, `"NewTab"` opens a tab. `preview: true` never navigates.
|
|
192
|
+
- **`DEFAULT_REDIRECT_TIMEOUT_MS`** = `400` (native's delay before an ending redirect).
|
|
193
|
+
- **`RedirectType`** = `"NewTab" | "CurrentWindow"`.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Reference — event pipeline, journey telemetry, detectors, emitter
|
|
2
|
+
|
|
3
|
+
Exhaustive reference for the `core/events` domain. Map of everything: [`../api.md`](../api.md).
|
|
4
|
+
(The engine's own lifecycle events — `viewed`/`started`/`stepChanged`/… — are in
|
|
5
|
+
[`engine.md`](engine.md); this file covers the modules beneath them.)
|
|
6
|
+
|
|
7
|
+
## Push events (`core/events/pushEvents`)
|
|
8
|
+
|
|
9
|
+
- **`createPushEventClient({transport, preview?}): PushEventClient`** — the internal analytics
|
|
10
|
+
pipeline, `POST /api/v1/pushEvent`, beacon-first. Client-side bot filter (UA regex); any event
|
|
11
|
+
missing `type`/`entityType`/`entityId` drops the WHOLE request; `preview: true` blocks every
|
|
12
|
+
send. `push(request)` is fire-and-forget.
|
|
13
|
+
- **`buildStepTransitionEventBatch({sessionId, formId, currentStepId, nextStepId, stepCompletionTime})`**
|
|
14
|
+
— the one batch shape the server accepts: `form_step_submitted` (with
|
|
15
|
+
`stepCompletionTime` property, seconds) + `form_step_viewed` for the next step.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
type SurfaceEventType =
|
|
19
|
+
| "form_viewed"
|
|
20
|
+
| "form_started"
|
|
21
|
+
| "form_completed"
|
|
22
|
+
| "form_step_viewed"
|
|
23
|
+
| "form_step_submitted"
|
|
24
|
+
| "form_step_dropoff"
|
|
25
|
+
| "form_question_submitted"
|
|
26
|
+
| "response_submission_created"
|
|
27
|
+
| "response_submission_error";
|
|
28
|
+
type SurfaceEventEntityType = "form" | "form_step" | "response";
|
|
29
|
+
type PushEventProperty = { key: string; value: unknown };
|
|
30
|
+
type PushEventPayload = {
|
|
31
|
+
type;
|
|
32
|
+
entityType;
|
|
33
|
+
entityId;
|
|
34
|
+
stepId?: string | null;
|
|
35
|
+
leadSessionId?: string | null;
|
|
36
|
+
properties?: PushEventProperty[];
|
|
37
|
+
leadId?;
|
|
38
|
+
environmentId?;
|
|
39
|
+
};
|
|
40
|
+
type PushEventBatch = { events: [submitted, viewed] }; // exactly the step-transition pair
|
|
41
|
+
type PushEventRequest = PushEventPayload | PushEventBatch;
|
|
42
|
+
type PushEventClient = { push: (request: PushEventRequest) => void };
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Form-journey telemetry (`core/events/formJourneyTracker`)
|
|
46
|
+
|
|
47
|
+
- **`createFormJourneyTracker({transport, preview?}): FormJourneyTracker`** — step-journey
|
|
48
|
+
visits/transitions/outcomes beaconed to `POST /api/v1/form-journey/track`. Direct native port:
|
|
49
|
+
visit ordinals (revisits get new ordinals; back-nav never overwrites a prior enter time),
|
|
50
|
+
close-previous-visit timing (`exitedAt`/`durationMs`), 50-visit cap with `truncated`,
|
|
51
|
+
freeze-on-terminal-outcome, microtask coalescing (a transition immediately followed by a
|
|
52
|
+
terminal sends ONE beacon), synchronous flush for the `in_progress` terminal (pagehide-safe).
|
|
53
|
+
Sends only once a responseId is set.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
type FormJourneyOutcome = "in_progress" | "completed" | "partial" | "disqualified" | "dropped_off";
|
|
57
|
+
type FormJourneyTransitionReason = "initial" | "linear" | "conditional_match" | "back_nav" | …;
|
|
58
|
+
type FormJourneyVisit = { ordinal; stepId; enteredAt; exitedAt?; durationMs?; fromStepId?;
|
|
59
|
+
transitionReason; transitionMeta? };
|
|
60
|
+
type FormJourneyTrackPayload = { responseId; formId; outcome?; disqualification?:
|
|
61
|
+
{ reason: "conditional_match"|"default_fallback"; matched? } | null;
|
|
62
|
+
disqualifiedAtStepId?; disqualifiedTriggerStepId?; events: FormJourneyVisit[] };
|
|
63
|
+
interface FormJourneyTracker {
|
|
64
|
+
recordEntry(stepId): void;
|
|
65
|
+
recordTransition({fromStepId, toStepId, reason, transitionMeta?}): void;
|
|
66
|
+
recordTerminal({outcome, disqualification?: {reason, atStepId, triggerStepId?, matched?}}): void;
|
|
67
|
+
setResponseId(responseId): void;
|
|
68
|
+
setFormId(formId): void;
|
|
69
|
+
hasRecordedTransition(): boolean;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Detectors
|
|
74
|
+
|
|
75
|
+
- **`observeFormView(element, onViewed): () => void`** — `form_viewed` detection:
|
|
76
|
+
IntersectionObserver on the bound root, fires the callback exactly once, then disconnects.
|
|
77
|
+
When IO is unavailable the callback fires immediately (counting a below-fold mount beats
|
|
78
|
+
losing view analytics). Returns a teardown.
|
|
79
|
+
- **`createStartedDetector(initialAnswers): (current) => boolean`** — `form_started` detection:
|
|
80
|
+
snapshots (deep-clones) the initial answers; returns true exactly ONCE, on the first
|
|
81
|
+
deep-inequality (key-order insensitive) vs the snapshot.
|
|
82
|
+
- **`observeDropoff(pushEvents, getState): () => void`** — `form_step_dropoff` on `pagehide`
|
|
83
|
+
(the only teardown event that still delivers beacons), only after the form started, never for
|
|
84
|
+
bfcache suspensions (`event.persisted`). `getState` is read at fire time:
|
|
85
|
+
`() => ({started, formId, stepId, sessionId})`. Returns a teardown.
|
|
86
|
+
|
|
87
|
+
## Emitter primitive (`core/events/emitter`)
|
|
88
|
+
|
|
89
|
+
- **`createEmitter<TEventMap>(): Emitter<TEventMap>`** — the typed, instance-scoped emitter the
|
|
90
|
+
engine's `on()` is built on. `on(event, handler) => Unsubscribe`; `emit(event, payload)`.
|
|
91
|
+
Handler exceptions are isolated per handler — a throwing host callback never breaks the engine
|
|
92
|
+
or sibling handlers. No module-level singletons: multi-form pages get independent emitters.
|
|
93
|
+
- **`Unsubscribe`** = `() => void`.
|