@parag.more.withsurface.com/forms-sdk 0.1.5 → 0.1.6
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 +9 -4
- package/dist/index.cjs +1 -2736
- package/dist/index.mjs +1 -2713
- package/dist/react/index.cjs +1 -43
- package/dist/react/index.mjs +1 -20
- package/dist/surface-forms-sdk.min.js +21 -1
- package/dist/types/bind/bindForm.d.ts +2 -0
- package/dist/types/bind/bookingWidget.d.ts +29 -6
- package/dist/types/bind/scheduler/embedUtils.d.ts +50 -0
- package/dist/types/bind/scheduler/mountScheduler.d.ts +2 -0
- package/dist/types/bind/scheduler/providers/cal.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/calendly.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/chilical.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/clari.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/hubspot.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/index.d.ts +4 -0
- package/dist/types/bind/scheduler/providers/reclaimai.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/revenuehero.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/savvycal.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/surface.d.ts +3 -0
- package/dist/types/bind/scheduler/providers/zoom.d.ts +3 -0
- package/dist/types/bind/scheduler/styles.d.ts +3 -0
- package/dist/types/bind/scheduler/types.d.ts +65 -0
- package/dist/types/core/client/emailValidation.d.ts +3 -0
- package/dist/types/core/client/responses.d.ts +3 -0
- package/dist/types/core/client/runtimeConfig.d.ts +40 -0
- package/dist/types/core/client/scheduler.d.ts +4 -0
- package/dist/types/core/client/transport.d.ts +4 -0
- package/dist/types/core/engine/answerAudit.d.ts +3 -0
- package/dist/types/core/engine/answerStates.d.ts +4 -0
- package/dist/types/core/engine/formEngine.d.ts +16 -0
- package/dist/types/core/engine/redirect.d.ts +1 -0
- package/dist/types/core/engine/responsePayload.d.ts +4 -0
- package/dist/types/core/engine/schedulerEvents.d.ts +40 -0
- package/dist/types/core/engine/urlData.d.ts +2 -0
- package/dist/types/core/engine/urlPrefill.d.ts +1 -0
- package/dist/types/core/events/dropoffDetection.d.ts +1 -0
- package/dist/types/core/events/emitter.d.ts +1 -0
- package/dist/types/core/events/formJourneyTracker.d.ts +1 -0
- package/dist/types/core/events/pushEvents.d.ts +2 -0
- package/dist/types/core/events/startedDetection.d.ts +1 -0
- package/dist/types/core/events/viewDetection.d.ts +1 -0
- package/dist/types/core/identity/browserFingerprint.d.ts +3 -0
- package/dist/types/core/identity/ensureIdentified.d.ts +1 -0
- package/dist/types/core/identity/identify.d.ts +1 -0
- package/dist/types/core/identity/journey.d.ts +3 -0
- package/dist/types/core/identity/journey.testStubs.d.ts +1 -0
- package/dist/types/core/identity/leadDataCache.d.ts +1 -0
- package/dist/types/enrichment/attachEnrichment.d.ts +1 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/react/useSurfaceForm.d.ts +1 -0
- package/dist/types/surfaceForms.d.ts +28 -1
- package/dist/types/tracking/attachTracking.d.ts +1 -0
- package/dist/types/tracking/conversionRules.d.ts +3 -0
- package/dist/types/tracking/vendors.d.ts +3 -0
- package/docs/api.md +51 -39
- package/docs/reference/binding.md +86 -13
- package/docs/reference/clients.md +21 -1
- package/docs/reference/engine.md +5 -1
- package/docs/reference/events.md +4 -1
- package/llms.txt +58 -10
- package/package.json +1 -1
- package/skills/surface-forms-sdk/SKILL.md +19 -6
- package/skills/surface-forms-sdk/patterns.md +30 -5
- package/dist/types/bind/bindForm.test.d.ts +0 -1
- package/dist/types/bind/bookingWidget.test.d.ts +0 -1
- package/dist/types/core/client/emailValidation.test.d.ts +0 -1
- package/dist/types/core/client/responses.test.d.ts +0 -1
- package/dist/types/core/client/runtimeConfig.contract.test.d.ts +0 -1
- package/dist/types/core/client/runtimeConfig.test.d.ts +0 -1
- package/dist/types/core/client/scheduler.contract.test.d.ts +0 -1
- package/dist/types/core/client/scheduler.test.d.ts +0 -1
- package/dist/types/core/client/transport.test.d.ts +0 -1
- package/dist/types/core/engine/answerAudit.test.d.ts +0 -1
- package/dist/types/core/engine/answerStates.contract.test.d.ts +0 -1
- package/dist/types/core/engine/formEngine.test.d.ts +0 -1
- package/dist/types/core/engine/redirect.test.d.ts +0 -1
- package/dist/types/core/engine/responsePayload.contract.test.d.ts +0 -1
- package/dist/types/core/engine/responsePayload.test.d.ts +0 -1
- package/dist/types/core/engine/urlData.contract.test.d.ts +0 -1
- package/dist/types/core/engine/urlData.test.d.ts +0 -1
- package/dist/types/core/engine/urlPrefill.test.d.ts +0 -1
- package/dist/types/core/events/dropoffDetection.test.d.ts +0 -1
- package/dist/types/core/events/emitter.test.d.ts +0 -1
- package/dist/types/core/events/formJourneyTracker.contract.test.d.ts +0 -1
- package/dist/types/core/events/formJourneyTracker.test.d.ts +0 -1
- package/dist/types/core/events/pushEvents.contract.test.d.ts +0 -1
- package/dist/types/core/events/pushEvents.test.d.ts +0 -1
- package/dist/types/core/events/startedDetection.test.d.ts +0 -1
- package/dist/types/core/events/viewDetection.test.d.ts +0 -1
- package/dist/types/core/identity/browserFingerprint.test.d.ts +0 -1
- package/dist/types/core/identity/ensureIdentified.test.d.ts +0 -1
- package/dist/types/core/identity/identify.contract.test.d.ts +0 -1
- package/dist/types/core/identity/identify.test.d.ts +0 -1
- package/dist/types/core/identity/journey.contract.test.d.ts +0 -1
- package/dist/types/core/identity/journey.test.d.ts +0 -1
- package/dist/types/core/identity/leadDataCache.test.d.ts +0 -1
- package/dist/types/enrichment/attachEnrichment.test.d.ts +0 -1
- package/dist/types/react/ssr.test.d.ts +0 -1
- package/dist/types/react/useSurfaceForm.test.d.ts +0 -1
- package/dist/types/surfaceForms.test.d.ts +0 -1
- package/dist/types/tracking/attachTracking.test.d.ts +0 -1
- package/dist/types/tracking/conversionRules.contract.test.d.ts +0 -1
- package/dist/types/tracking/conversionRules.test.d.ts +0 -1
- package/dist/types/tracking/vendors.test.d.ts +0 -1
- package/dist/types/validation/attachEmailValidation.test.d.ts +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
export type LifecycleUrlData = {
|
|
2
3
|
url: string;
|
|
3
4
|
urlParams: Record<string, string>;
|
|
@@ -11,11 +12,13 @@ export type VendorSettings = {
|
|
|
11
12
|
};
|
|
12
13
|
};
|
|
13
14
|
export type StepSubmittedPayload = LifecycleUrlData & {
|
|
15
|
+
|
|
14
16
|
stepId: string;
|
|
15
17
|
formId: string;
|
|
16
18
|
emailProvided: boolean;
|
|
17
19
|
meetingBooked: boolean;
|
|
18
20
|
};
|
|
21
|
+
|
|
19
22
|
export declare const createVendorTracker: (win: Window, settings: VendorSettings) => {
|
|
20
23
|
formStarted: (formId: string, urlData: LifecycleUrlData) => void;
|
|
21
24
|
stepSubmitted: (payload: StepSubmittedPayload) => void;
|
package/docs/api.md
CHANGED
|
@@ -35,25 +35,25 @@ exact URL and host it tried.
|
|
|
35
35
|
|
|
36
36
|
### `SurfaceFormsInitOptions`
|
|
37
37
|
|
|
38
|
-
| Option | Type | Meaning
|
|
39
|
-
| --------------------- | ---------------------------- |
|
|
40
|
-
| `environmentId` | `string` | Required. From `get_sdk_binding_map`.
|
|
41
|
-
| `formId` | `string` | Required.
|
|
42
|
-
| `apiBaseUrl` | `string?` | The environment's host. **Required unless the environment is on `https://forms.withsurface.com`** — `get_sdk_binding_map` returns it.
|
|
43
|
-
| `container` | `Element?` | Enables the `data-*` binding layer + view detection on this root (§4). Omit for fully headless use.
|
|
44
|
-
| `steps` | `Array<{id, endStepKind?}>?` | Caller-provided step order — **skips the record fetch entirely** (offline mode). Normally omitted.
|
|
45
|
-
| `settings` | object? | Behavioral gates + tracking config (`trackPartialResponses`, `trackCookies`, `gtmId`, `gaMeasurementId`, `metaPixelId`, `hubspotTracking`, `adsConversions`). Caller values win over the record's.
|
|
46
|
-
| `expectedQuestionIds` | `string[]?` | Overrides the record-derived list behind the unbound-question console warning.
|
|
47
|
-
| `resolveNextStep` | `ResolveNextStep?` | Routing hook for `next()`/`submit()` (§7.3); defaults to linear record order.
|
|
48
|
-
| `urlDataStateId` | `string?` | Seeds a `URL_DATA` attribution pseudo-answer under this id (rides every save).
|
|
49
|
-
| `source` | `LeadSource?` | Lead source for identify; defaults `"sdkForm"`.
|
|
50
|
-
| `journey` | `boolean?` | Page-view tracking module (§7.6), default off.
|
|
51
|
-
| `emailValidation` | `boolean?` | Automatic waterfall email validation (§5), default off — verdicts bill the form owner's validator quota.
|
|
52
|
-
| `preview` | `boolean?` | Blocks ALL network writes (identify, responses, events, journey, enrichment, tracking). Scheduler returns synthetic bookings.
|
|
53
|
-
| `previewToken` | `string?` | With `preview: true`, serves a never-published form's DRAFT config. From `create_form` / `get_sdk_binding_map`.
|
|
54
|
-
| `resume` | `{responseId, resumeToken}?` | Continue a stored response (§3). Ignored in preview.
|
|
55
|
-
| `debug` | `boolean?` | Logs lifecycle + audits every answer against the record; warns on states the server would reject and on inert config (declared enrichment source, no provider). Dev only.
|
|
56
|
-
| `meta` | `Record<string, unknown>?` | Seed response meta. `surfaceTagStatus` is always overwritten to `"sdk"`.
|
|
38
|
+
| Option | Type | Meaning |
|
|
39
|
+
| --------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| `environmentId` | `string` | Required. From `get_sdk_binding_map`. |
|
|
41
|
+
| `formId` | `string` | Required. |
|
|
42
|
+
| `apiBaseUrl` | `string?` | The environment's host. **Required unless the environment is on `https://forms.withsurface.com`** — `get_sdk_binding_map` returns it. |
|
|
43
|
+
| `container` | `Element?` | Enables the `data-*` binding layer + view detection on this root (§4). Omit for fully headless use. |
|
|
44
|
+
| `steps` | `Array<{id, endStepKind?}>?` | Caller-provided step order — **skips the record fetch entirely** (offline mode). Normally omitted. |
|
|
45
|
+
| `settings` | object? | Behavioral gates + tracking config (`trackPartialResponses`, `trackCookies`, `gtmId`, `gaMeasurementId`, `metaPixelId`, `hubspotTracking`, `adsConversions`). Caller values win over the record's. |
|
|
46
|
+
| `expectedQuestionIds` | `string[]?` | Overrides the record-derived list behind the unbound-question console warning. |
|
|
47
|
+
| `resolveNextStep` | `ResolveNextStep?` | Routing hook for `next()`/`submit()` (§7.3); defaults to linear record order. |
|
|
48
|
+
| `urlDataStateId` | `string?` | Seeds a `URL_DATA` attribution pseudo-answer under this id (rides every save). |
|
|
49
|
+
| `source` | `LeadSource?` | Lead source for identify; defaults `"sdkForm"`. |
|
|
50
|
+
| `journey` | `boolean?` | Page-view tracking module (§7.6), default off. |
|
|
51
|
+
| `emailValidation` | `boolean?` | Automatic waterfall email validation (§5), default off — verdicts bill the form owner's validator quota. |
|
|
52
|
+
| `preview` | `boolean?` | Blocks ALL network writes (identify, responses, events, journey, enrichment, tracking). Scheduler returns synthetic bookings. |
|
|
53
|
+
| `previewToken` | `string?` | With `preview: true`, serves a never-published form's DRAFT config. From `create_form` / `get_sdk_binding_map`. |
|
|
54
|
+
| `resume` | `{responseId, resumeToken}?` | Continue a stored response (§3). Ignored in preview. |
|
|
55
|
+
| `debug` | `boolean?` | Logs lifecycle + navigation intent (`next requested from step …`, distinguishing an un-fired click from a blocked advance) + audits every answer against the record; warns on states the server would reject and on inert config (declared enrichment source, no provider). Dev only. |
|
|
56
|
+
| `meta` | `Record<string, unknown>?` | Seed response meta. `surfaceTagStatus` is always overwritten to `"sdk"`. |
|
|
57
57
|
|
|
58
58
|
## 2. The form handle — `SurfaceForm`
|
|
59
59
|
|
|
@@ -61,26 +61,28 @@ Everything the engine exposes (§7.3) minus internals, plus init-level helpers.
|
|
|
61
61
|
Full method semantics, event payloads, and delivery guarantees:
|
|
62
62
|
[`reference/engine.md`](reference/engine.md).
|
|
63
63
|
|
|
64
|
-
| Member
|
|
65
|
-
|
|
|
66
|
-
| `setAnswer`
|
|
67
|
-
| `next` / `back`
|
|
68
|
-
| `goToStep`
|
|
69
|
-
| `submit`
|
|
70
|
-
| `disqualify`
|
|
71
|
-
| `savePartial`
|
|
72
|
-
| `flush`
|
|
73
|
-
| `markViewed`
|
|
74
|
-
| `state`
|
|
75
|
-
| `subscribe`
|
|
76
|
-
| `on`
|
|
77
|
-
| `setExternalState`
|
|
78
|
-
| `mergeMeta`
|
|
79
|
-
| `identify`
|
|
80
|
-
| `validateEmail`
|
|
81
|
-
| `capabilities`
|
|
82
|
-
| `runtimeConfig`
|
|
83
|
-
| `
|
|
64
|
+
| Member | Signature | Semantics |
|
|
65
|
+
| ------------------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
66
|
+
| `setAnswer` | `(questionId, state) => void` | Merges a partial state into the question's answer. Use the typed answer states (§6) — wrong shapes are the classic production 400. |
|
|
67
|
+
| `next` / `back` | `() => Promise<void>` / `() => void` | Linear advance (saves progress; the step being LEFT is stamped on the write) / rewind (never writes). |
|
|
68
|
+
| `goToStep` | `(stepId) => Promise<void>` | Customer-owned routing: forward jumps save like `next()`; jumps to a visited step rewind like `back()`. Rejects unknown ids. |
|
|
69
|
+
| `submit` | `() => Promise<void>` | Finishes the form. **The terminal write is awaited** — when this resolves (and `saved` fires with `finished: true`), the server has the finished response; a delivery failure emits `error` instead. Then reveals the record's `thank_you` step. |
|
|
70
|
+
| `disqualify` | `() => Promise<void>` | Finishes as disqualified (`invalidResponse: true`); reveals the `disqualified` ending. |
|
|
71
|
+
| `savePartial` | `() => Promise<void>` | Explicit mid-form save (fire-and-forget beacon delivery). |
|
|
72
|
+
| `flush` | `() => Promise<void>` | Resolves when every queued write (including post-finish amends) has settled. Await before programmatic teardown. |
|
|
73
|
+
| `markViewed` | `() => void` | Emits `form_viewed` + first-step `form_step_viewed`, once. The bind layer calls it via IntersectionObserver; headless callers call it themselves. |
|
|
74
|
+
| `state` | `() => FormEngineState` | `{ currentStepId, answers, externalStates, responseId, resumeToken, finished }`. Referentially stable between mutations (safe for `useSyncExternalStore`). |
|
|
75
|
+
| `subscribe` | `(cb) => Unsubscribe` | Store subscription (fires on any state change). |
|
|
76
|
+
| `on` | `(event, cb) => Unsubscribe` | Lifecycle events: `viewed`, `started`, `stepChanged {fromStepId,toStepId}`, `stepCompleted {stepId}`, `completed {formId}`, `disqualified {stepId}`, `saved {responseId, finished, resumeToken}`, `error {scope, error}`, `scheduler` (`SchedulerLifecycleEvent`, §7.8). |
|
|
77
|
+
| `setExternalState` | `(id, {type, ...}) => void` | Writes a pseudo-answer that rides every save (enrichment results, custom attribution). Post-finish it amends the stored response. |
|
|
78
|
+
| `mergeMeta` | `(partial) => void` | Records caller-computed values (qualification score, experiment bucket) onto response `meta`; rides the next save or amends post-finish. Reserved SDK keys (`surfaceTagStatus`, email-validation verdicts) are dropped with a warning. |
|
|
79
|
+
| `identify` | `() => Promise<unknown>` | Re-runs visitor identify (SPA route change); later writes attribute to the fresh session. |
|
|
80
|
+
| `validateEmail` | `(email) => Promise<EmailValidationResult>` | On-demand deliverability verdict (§5). |
|
|
81
|
+
| `capabilities` | `{enrichment, emailValidation, tracking}` | What actually runs in THIS environment — a record can declare features the environment doesn't back. |
|
|
82
|
+
| `runtimeConfig` | `RuntimeConfig?` | The fetched record (undefined when `steps` was caller-provided). |
|
|
83
|
+
| `notifyScheduler` | `(event) => void` | Emits on the `scheduler` channel (used by `mountScheduler`; emit-only, never writes). |
|
|
84
|
+
| `transport` / `identity` / `preview` | advanced | The boot transport, the live identity getter, and the preview flag — for modules acting on the form's behalf (`mountScheduler` uses all three). |
|
|
85
|
+
| `destroy` | `() => void` | Tears down every module binding. One engine per page-life — re-`init` without `destroy` creates duplicate responses. |
|
|
84
86
|
|
|
85
87
|
## 3. Resume
|
|
86
88
|
|
|
@@ -268,6 +270,16 @@ true` page-view module (tag-compatible cookies, duplicate-URL skip, SPA
|
|
|
268
270
|
|
|
269
271
|
Full `SchedulerClient` signatures and widget options: [`reference/clients.md`](reference/clients.md) + [`reference/binding.md`](reference/binding.md).
|
|
270
272
|
|
|
273
|
+
- **`mountScheduler(options): () => void`** — the DEFAULT scheduler path: one
|
|
274
|
+
call renders the record's scheduler question for ANY supported provider
|
|
275
|
+
(Surface's widget default-styled, or the third-party embed native forms
|
|
276
|
+
use), emits the `SchedulerLifecycleEvent` union on `form.on("scheduler")` +
|
|
277
|
+
`onEvent`, and auto-persists the booking into the response
|
|
278
|
+
(`persist: false` opts out). Options table, provider matrix, and
|
|
279
|
+
persistence semantics: [`reference/binding.md`](reference/binding.md).
|
|
280
|
+
- **`schedulerQuestionsOf(config): SchedulerQuestion[]`** — the record's
|
|
281
|
+
scheduler questions with provider + embed content
|
|
282
|
+
([`reference/clients.md`](reference/clients.md)).
|
|
271
283
|
- **`createSchedulerClient({ transport, preview? }): SchedulerClient`** —
|
|
272
284
|
public Surface-scheduler endpoints: `getPublicEventType`,
|
|
273
285
|
`getAvailabilitySummary`, `getMonthAvailability` (`YYYY-MM`), `getSlots`,
|
|
@@ -63,24 +63,97 @@ flash.)
|
|
|
63
63
|
- `expectedQuestionIds` (init derives them from the record) minus the ids actually bound →
|
|
64
64
|
a console warning naming the unbound ids; the runtime counterpart of `validate_form_html`.
|
|
65
65
|
|
|
66
|
+
## `mountScheduler(options): () => void`
|
|
67
|
+
|
|
68
|
+
The DEFAULT scheduler path: renders the record's scheduler question for any supported provider,
|
|
69
|
+
emits the lifecycle on the form's `scheduler` event channel, and persists the booking into the
|
|
70
|
+
response automatically. Reach for `mountBookingWidget` (below) only when building a custom UI.
|
|
71
|
+
|
|
72
|
+
### Options (`MountSchedulerOptions`)
|
|
73
|
+
|
|
74
|
+
| Option | Type | Semantics |
|
|
75
|
+
| ------------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
76
|
+
| `form` | `SurfaceForm` | The `init` handle — config, transport, identity, and persistence all come from it. |
|
|
77
|
+
| `container` | `Element` | The embed mounts here; teardown removes it. |
|
|
78
|
+
| `questionId` | `string?` | Required only when the record has >1 scheduler question (the error names the candidates). |
|
|
79
|
+
| `stepId` | `string?` | Alternative selector: the step carrying the scheduler question. |
|
|
80
|
+
| `styled` | `boolean?` (default `true`) | Injects a stylesheet scoped under `.surface-scheduler` on THIS container (Surface widget look + frame sizing). Override via `--surface-scheduler-*` custom props or the same class names; `false` keeps everything bare. |
|
|
81
|
+
| `persist` | `boolean?` (default `true`) | Auto-persist lifecycle + booking into the response. `false` still emits events but never writes (and never creates a response). |
|
|
82
|
+
| `prefill` | `{name?, lastName?, email?}?` | Overrides the prefill resolved from the record's linked answer refs (`linkNameToStep` etc.). |
|
|
83
|
+
| `formatters` | `{month?, date?, time?}?` | Surface provider only — forwarded to the booking widget. |
|
|
84
|
+
| `calendarId` | `string?` | DynamicScheduler only: pick a routed calendar by name. Without it: the default calendar, or a lone `calendars[0]`; ambiguous routing throws (conditions are your page's own routing). |
|
|
85
|
+
| `onEvent` | `(SchedulerLifecycleEvent) => void?` | Same events as `form.on("scheduler", …)`. |
|
|
86
|
+
| `onComplete` | `(SchedulerBookingSummary) => void?` | Fires alongside `booking_confirmed`. NOT needed for persistence — that's automatic. |
|
|
87
|
+
|
|
88
|
+
### Lifecycle events (`SchedulerLifecycleEvent`)
|
|
89
|
+
|
|
90
|
+
Every event carries `{questionId, stepId, provider}` plus:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
| { type: "viewed" } // once per mount
|
|
94
|
+
| { type: "date_selected"; date? /* "YYYY-MM-DD" */ }
|
|
95
|
+
| { type: "slot_selected"; slot?: { start; end? } }
|
|
96
|
+
| { type: "form_opened" }
|
|
97
|
+
| { type: "booking_confirmed"; booking: SchedulerBookingSummary }
|
|
98
|
+
| { type: "booking_cancelled"; reason? }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`SchedulerBookingSummary` = `{provider, uri, meetingTime?, meetingEndTime?, inviteeName?,
|
|
102
|
+
guestEmails?, ownerEmails?, joinLink?, raw?}` — `uri` is the provider-native booking id
|
|
103
|
+
(HubSpot's embed reports none; its uri is the single space native forms persist), `raw` the
|
|
104
|
+
untouched provider payload.
|
|
105
|
+
|
|
106
|
+
### Providers
|
|
107
|
+
|
|
108
|
+
| Provider (record `calendarType`) | Embed | Booking signal | Extra lifecycle |
|
|
109
|
+
| -------------------------------- | -------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
|
|
110
|
+
| `Surface` | SDK booking widget (styled by default) | widget booking | `date_selected`, `slot_selected`, `form_opened`; sub-steps persist as `schedulerSubStep` |
|
|
111
|
+
| `Calendly` | iframe | postMessage + server-poll fallback | provider-reported `viewed`, `slot_selected`; poll-reported cancellation |
|
|
112
|
+
| `Cal` (Cal.com) | vendor script | `bookingSuccessful` CustomEvent | — |
|
|
113
|
+
| `SavvyCal` | vendor script | `savvycal.scheduled` CustomEvent | — |
|
|
114
|
+
| `ChiliCal` (Chili Piper) | iframe | `booking-confirmed` postMessage | — |
|
|
115
|
+
| `Hubspot` | vendor script | `meetingBookSucceeded` postMessage | — |
|
|
116
|
+
| `RevenueHero` | vendor script + router submit | `MEETING_BOOKED` postMessage | — |
|
|
117
|
+
| `Zoom` | iframe | `bookingForm` postMessage | server-poll backfills host email / times / join link into the answer |
|
|
118
|
+
| `ReclaimAI` | iframe | server poll ONLY (no client events) | poll carries the booking |
|
|
119
|
+
| `Clari` | iframe | **none** — embed-only; nothing is tracked or persisted | — |
|
|
120
|
+
|
|
121
|
+
postMessage listeners are origin-checked strictly (exact origin or anchored host suffix — the
|
|
122
|
+
SDK mirrors the vendor protocols, not the laxness some native checks have).
|
|
123
|
+
|
|
124
|
+
### Persistence semantics
|
|
125
|
+
|
|
126
|
+
- Confirmed booking → the frozen scheduler answer keys (`eventScheduled`, `uri`,
|
|
127
|
+
`meetingTime`…, `type` = the record's component type) merged via `setAnswer`, then a partial
|
|
128
|
+
save; after `submit()` the engine amends the stored response instead. `meta.bookingId` (Surface
|
|
129
|
+
bookings only) and the `meeting_booked` ad-conversion trigger follow automatically.
|
|
130
|
+
- Poll-reported cancellation (after a confirmed booking) → `eventCancelled: true` + save.
|
|
131
|
+
- Calendly/Zoom/Reclaim embeds carry the `responseId` in their URL, so mounting them creates the
|
|
132
|
+
response with a partial save when none exists yet. The Surface widget resolves identity lazily
|
|
133
|
+
at book time instead — lookers who never book create nothing.
|
|
134
|
+
- `preview: true` on the form blocks every write and poll; the Surface widget books through the
|
|
135
|
+
fabricated preview client, so the whole flow demos offline.
|
|
136
|
+
|
|
66
137
|
## `mountBookingWidget(options): () => void`
|
|
67
138
|
|
|
68
|
-
The unstyled Surface-scheduler booking flow (date → time →
|
|
69
|
-
`SchedulerClient` ([`clients.md`](clients.md)). Never iframe
|
|
139
|
+
The custom-UI escape hatch: the unstyled Surface-scheduler booking flow (date → time →
|
|
140
|
+
name/email form → booked) over a `SchedulerClient` ([`clients.md`](clients.md)). Never iframe
|
|
141
|
+
the hosted booking page. For the zero-effort path use `mountScheduler` (above).
|
|
70
142
|
|
|
71
143
|
### Options (`BookingWidgetOptions`)
|
|
72
144
|
|
|
73
|
-
| Option | Type
|
|
74
|
-
| ------------- |
|
|
75
|
-
| `container` | `Element`
|
|
76
|
-
| `scheduler` | `SchedulerClient`
|
|
77
|
-
| `eventTypeId` | `string`
|
|
78
|
-
| `timezone` | `string?`
|
|
79
|
-
| `duration` | `number?`
|
|
80
|
-
| `prefill` | `{name?, email?}?`
|
|
81
|
-
| `identity` | `
|
|
82
|
-
| `formatters` | `{month?, date?, time?}?`
|
|
83
|
-
| `
|
|
145
|
+
| Option | Type | Semantics |
|
|
146
|
+
| ------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
147
|
+
| `container` | `Element` | The widget appends its own `div.surface-booking` root here; teardown removes it. |
|
|
148
|
+
| `scheduler` | `SchedulerClient` | From `createSchedulerClient` (share the transport; pass `preview` there for synthetic bookings). |
|
|
149
|
+
| `eventTypeId` | `string` | From the record's `CalendlyScreen` content. |
|
|
150
|
+
| `timezone` | `string?` | Defaults to the visitor's (`Intl.DateTimeFormat().resolvedOptions().timeZone`); slot labels format in it. |
|
|
151
|
+
| `duration` | `number?` | Threaded into availability/slots/book queries. |
|
|
152
|
+
| `prefill` | `{name?, email?}?` | Pre-fills the widget's name/email inputs. |
|
|
153
|
+
| `identity` | `BookingIdentity \| (() => BookingIdentity \| Promise<BookingIdentity>)?` | Threaded into `book()` for booking↔response linkage. The getter form resolves at book time — a response created after mount still links. |
|
|
154
|
+
| `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`. |
|
|
155
|
+
| `onEvent` | `(BookingWidgetEvent) => void?` | Sub-step transitions: `viewed`, `date_selected {date}`, `slot_selected {slot}`, `form_opened` (fresh opens only, not error re-renders), `booking_confirmed {booking}` (alongside `onComplete`). |
|
|
156
|
+
| `onComplete` | `(booking: Booking) => void` | Fires after a successful booking — persist via `setAnswer({...toSchedulerAnswerState(booking), type: "<record type>"})` and navigate. |
|
|
84
157
|
|
|
85
158
|
### Behavior + DOM
|
|
86
159
|
|
|
@@ -158,7 +158,27 @@ server links booking↔response with them; pass what you have.
|
|
|
158
158
|
override `type` to the record's component type when persisting
|
|
159
159
|
(`{...toSchedulerAnswerState(b), type: "CalendlyScreen"}`).
|
|
160
160
|
|
|
161
|
-
The DOM widget over this client (`mountBookingWidget`)
|
|
161
|
+
The DOM widget over this client (`mountBookingWidget`) and the zero-effort embedded path
|
|
162
|
+
(`mountScheduler`) are in [`binding.md`](binding.md).
|
|
163
|
+
|
|
164
|
+
- **`schedulerQuestionsOf(config): SchedulerQuestion[]`** (`core/client/runtimeConfig`) — the
|
|
165
|
+
record's scheduler questions with everything `mountScheduler` reads:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
type SchedulerQuestion = {
|
|
169
|
+
questionId;
|
|
170
|
+
stepId;
|
|
171
|
+
componentType: "CalendlyScreen" | "DynamicScheduler"; // also the persisted answer `type`
|
|
172
|
+
calendarType: string; // provider; the record omits it for the original default, Calendly
|
|
173
|
+
content: {
|
|
174
|
+
dataUrl?; eventTypeId?; savvyCalLinkId?; revenueHeroRouterId?;
|
|
175
|
+
dataUrlParams?: Record<string, { stepId; questionId; variable }>; // answer-state refs → embed params
|
|
176
|
+
linkNameToStep? / linkLastNameToStep? / linkEmailToStep?: { stepId; questionId }; // prefill refs
|
|
177
|
+
addSchedulerQueryParams?; skipFormIfPrefilled?;
|
|
178
|
+
calendars?; defaultCalendar?; // DynamicScheduler routing
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
```
|
|
162
182
|
|
|
163
183
|
## Email validation (`core/client/emailValidation` + `validation/attachEmailValidation`)
|
|
164
184
|
|
package/docs/reference/engine.md
CHANGED
|
@@ -30,7 +30,10 @@ creates one for you; construct directly for offline/custom stacks.
|
|
|
30
30
|
|
|
31
31
|
- **`setAnswer(questionId, state)`** — merges (`{...prev, ...state}`) into the current step's
|
|
32
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.
|
|
33
|
+
first deep divergence from the initial answers. Notifies subscribers. After the finishing
|
|
34
|
+
write it amends the stored response (like `setExternalState`/`mergeMeta`) — a booking
|
|
35
|
+
confirmed on a thank-you-step scheduler still persists; before that, it never writes on its
|
|
36
|
+
own (writes ride navigation/`savePartial`).
|
|
34
37
|
- **`next(): Promise<void>`** — resolves the next step. Not finished → emits `stepCompleted`,
|
|
35
38
|
pushes the `form_step_submitted`+`form_step_viewed` batch (`stepCompletionTime` = seconds since
|
|
36
39
|
engine creation), records the journey transition, writes a partial (if enabled) stamped with the
|
|
@@ -78,6 +81,7 @@ resumeToken, finished}`. The snapshot is cached until the next mutation, so it i
|
|
|
78
81
|
| `disqualified` | `{stepId}` | Disqualified finish. |
|
|
79
82
|
| `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
83
|
| `error` | `{scope: "submit", error}` | Failed create (typed `TransportError`) or failed terminal update. |
|
|
84
|
+
| `scheduler` | `SchedulerLifecycleEvent` | Scheduler lifecycle, emitted via `notifyScheduler` (the `mountScheduler` path — union in [`binding.md`](binding.md)). Emit-only. |
|
|
81
85
|
|
|
82
86
|
### Write serialization + delivery
|
|
83
87
|
|
package/docs/reference/events.md
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Exhaustive reference for the `core/events` domain. Map of everything: [`../api.md`](../api.md).
|
|
4
4
|
(The engine's own lifecycle events — `viewed`/`started`/`stepChanged`/… — are in
|
|
5
|
-
[`engine.md`](engine.md);
|
|
5
|
+
[`engine.md`](engine.md); the `scheduler` channel's `SchedulerLifecycleEvent` union is in
|
|
6
|
+
[`binding.md`](binding.md) with `mountScheduler`. This file covers the modules beneath them.
|
|
7
|
+
Scheduler lifecycle events are client-side only — the push-event vocabulary below is a closed
|
|
8
|
+
server contract and gains no scheduler types.)
|
|
6
9
|
|
|
7
10
|
## Push events (`core/events/pushEvents`)
|
|
8
11
|
|
package/llms.txt
CHANGED
|
@@ -45,9 +45,11 @@ Options beyond the four above: `preview: true` (no network writes at all — ide
|
|
|
45
45
|
responses, events, journey all blocked; pass `previewToken` from the `create_form`
|
|
46
46
|
or `get_sdk_binding_map` reply alongside it and a never-published form serves its
|
|
47
47
|
DRAFT config, so you can preview before the first publish), `debug: true`
|
|
48
|
-
(logs every captured answer + lifecycle event
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
(logs every captured answer + lifecycle event + navigation intent — a
|
|
49
|
+
`next requested from step …` line per `next()`/`submit()`/… call, so a click
|
|
50
|
+
that never fired is distinguishable from a blocked advance — and warns on
|
|
51
|
+
states the server would reject AND on inert configuration like a declared
|
|
52
|
+
enrichment source with no enabled provider; development only), `journey: true` (page-view tracking),
|
|
51
53
|
`urlDataStateId` (attribution pseudo-answer), `emailValidation: true`
|
|
52
54
|
(automatic verdicts, see below), `resume`, `meta`, `source`.
|
|
53
55
|
|
|
@@ -95,7 +97,12 @@ Then:
|
|
|
95
97
|
### Choice questions bind as a GROUP
|
|
96
98
|
|
|
97
99
|
Five types are group-built — `MultipleOptionsQuestion`, `MultipleChoiceQuestion`,
|
|
98
|
-
`MMChoiceQuestion`, `GraphicOptions`, and `MultipleChoiceSingleAnswer`.
|
|
100
|
+
`MMChoiceQuestion`, `GraphicOptions`, and `MultipleChoiceSingleAnswer`. All five
|
|
101
|
+
bind when present in the record, but only the first three are creatable via
|
|
102
|
+
`create_form` (`list_component_types` is the authority) —
|
|
103
|
+
`MultipleChoiceQuestion` and `MultipleChoiceSingleAnswer` are legacy types you
|
|
104
|
+
will only meet on pre-existing forms; build new records with
|
|
105
|
+
`MultipleOptionsQuestion`. Their
|
|
99
106
|
answers are structured (the record's whole option list / a selection object,
|
|
100
107
|
and their shapes differ — only `MultipleOptionsQuestion` carries an option
|
|
101
108
|
`index`; trust each question's `answerShape` from the binding map, never a
|
|
@@ -152,6 +159,11 @@ SPA route change), `validateEmail(email)`, `mergeMeta(partial)`,
|
|
|
152
159
|
`disqualified`, `saved` (carries `responseId` + `resumeToken`), `error`. Every
|
|
153
160
|
`on`/`subscribe` call returns an unsubscribe function.
|
|
154
161
|
|
|
162
|
+
`state().answers` is keyed by stepId, then questionId
|
|
163
|
+
(`Record<stepId, Record<questionId, state>>`) — reading an EARLIER step's
|
|
164
|
+
answer (booking prefill, a summary screen) indexes by that step's id:
|
|
165
|
+
`state().answers["step_about"]?.["q_name"]`, not by the current step.
|
|
166
|
+
|
|
155
167
|
Delivery guarantees: the FINISHING write is awaited — when `submit()` resolves
|
|
156
168
|
(and `saved` fires with `finished: true`) the server has the finished response;
|
|
157
169
|
a failure emits `error` instead of lying. Mid-form partial saves are
|
|
@@ -219,7 +231,10 @@ Pass `emailValidation: true` to `init` for automatic mode: email answers are
|
|
|
219
231
|
validated as they're captured (debounced; per-address, so nothing is billed
|
|
220
232
|
twice) and the response carries the same `meta` verdict keys native forms
|
|
221
233
|
write (`isEmailValid`, `emailValidation`, `emailValidationFailed`) for
|
|
222
|
-
dashboards and workflows.
|
|
234
|
+
dashboards and workflows. Automatic mode ANNOTATES ONLY — it never blocks
|
|
235
|
+
navigation or submission (an invalid address still submits, with
|
|
236
|
+
`isEmailValid: false` on the response); to actually gate, call
|
|
237
|
+
`validateEmail` in your own navigation as above. Preview resolves synthetic valid verdicts with no
|
|
223
238
|
network. Offline callers wire it themselves: `createEmailValidator({
|
|
224
239
|
transport, environmentId })` and optionally `attachEmailValidation({ engine,
|
|
225
240
|
validator })`.
|
|
@@ -241,11 +256,41 @@ callers (caller-provided `steps`) wire it themselves: `attachEnrichment({
|
|
|
241
256
|
engine, transport, formId, sources })` with sources from
|
|
242
257
|
`enrichmentSourcesOf(config)`.
|
|
243
258
|
|
|
244
|
-
## Scheduler booking
|
|
259
|
+
## Scheduler booking
|
|
260
|
+
|
|
261
|
+
**Default path — one call, any provider.** `mountScheduler` reads the record's
|
|
262
|
+
scheduler question (provider, event URL/id, prefill links), renders a working
|
|
263
|
+
scheduler, tracks the lifecycle, and persists the booking into the response.
|
|
264
|
+
No `onComplete` wiring, no answer writing, no HTML binding:
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
import { mountScheduler } from "@parag.more.withsurface.com/forms-sdk";
|
|
268
|
+
|
|
269
|
+
const teardown = mountScheduler({
|
|
270
|
+
form, // the SurfaceForms.init handle
|
|
271
|
+
container: document.querySelector("#booking"),
|
|
272
|
+
// questionId: "q_booking", // only when the record has >1 scheduler question
|
|
273
|
+
onEvent: (e) => console.log(e.type), // optional; also on form.on("scheduler", …)
|
|
274
|
+
});
|
|
275
|
+
```
|
|
245
276
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
277
|
+
Works for every provider native forms support — Surface's own scheduler
|
|
278
|
+
(rendered through the SDK widget, default-styled; `styled: false` keeps it
|
|
279
|
+
bare), Calendly, Cal.com, SavvyCal, Chili Piper, HubSpot, RevenueHero, Zoom,
|
|
280
|
+
Reclaim.ai, Clari — embedding each third-party widget and listening to its
|
|
281
|
+
booking events exactly like native forms do. Lifecycle events
|
|
282
|
+
(`viewed` / `date_selected` / `slot_selected` / `form_opened` /
|
|
283
|
+
`booking_confirmed` / `booking_cancelled`) emit on `form.on("scheduler")` and
|
|
284
|
+
the `onEvent` option; the confirmed booking is written to the response
|
|
285
|
+
automatically (`persist: false` opts out). Caveats: Clari exposes no booking
|
|
286
|
+
signal (embed-only — the visitor advances manually); Calendly/Zoom/Reclaim
|
|
287
|
+
create the response at mount when none exists yet (their embeds carry the
|
|
288
|
+
`responseId` for booking↔response linkage). Full options + provider matrix:
|
|
289
|
+
`docs/reference/binding.md`.
|
|
290
|
+
|
|
291
|
+
**Custom-UI path (advanced).** When you're building your own booking UI for a
|
|
292
|
+
Surface scheduler, mount the SDK's unstyled widget directly — do NOT iframe
|
|
293
|
+
the hosted booking page, and do NOT bind the question in HTML (the
|
|
249
294
|
`onComplete` below writes the answer):
|
|
250
295
|
|
|
251
296
|
```js
|
|
@@ -269,7 +314,10 @@ The widget walks date → time → name/email form → booked. Labels are
|
|
|
269
314
|
human-readable by default (`Intl` in the visitor's locale; every month/date/slot
|
|
270
315
|
element also carries its raw value in `data-value` — `YYYY-MM`, `YYYY-MM-DD`,
|
|
271
316
|
ISO instant); pass `formatters: { month?, date?, time? }` to override the copy.
|
|
272
|
-
|
|
317
|
+
The widget itself injects no CSS — style it from your page. (`mountScheduler`'s
|
|
318
|
+
default styling is a stylesheet scoped under `.surface-scheduler` on ITS
|
|
319
|
+
container only; bare `mountBookingWidget` mounts are never restyled.) The
|
|
320
|
+
rendered tree:
|
|
273
321
|
|
|
274
322
|
```
|
|
275
323
|
div.surface-booking
|
package/package.json
CHANGED
|
@@ -53,7 +53,13 @@ references alongside this file — read the one that matches the job:
|
|
|
53
53
|
5. **Validate.** `validate_form_html` until clean. Scheduler/file questions
|
|
54
54
|
report as notes (no binding needed — code writes their answers). If your
|
|
55
55
|
navigation/submit is JS-driven, declare `data-surface-nav="js"` on any
|
|
56
|
-
element instead of adding fake buttons.
|
|
56
|
+
element instead of adding fake buttons. For a JSX/React page the check is a
|
|
57
|
+
one-shot against transcribed markup — ALSO commit a drift test: extract
|
|
58
|
+
every `data-question-id` / `data-step-id` / choice `value` from the
|
|
59
|
+
component source (regex over the file is fine) and assert them against the
|
|
60
|
+
binding map's ids saved in your contract module. Binding drift is silent
|
|
61
|
+
data loss; only a committed test catches the refactor that renames a
|
|
62
|
+
question wrapper.
|
|
57
63
|
6. **Preview before publishing.** `init({ preview: true, previewToken })`
|
|
58
64
|
serves the DRAFT record with zero network writes; the scheduler returns a
|
|
59
65
|
synthetic booking. Skip `simulate_response` — it evaluates record routing,
|
|
@@ -80,7 +86,9 @@ references alongside this file — read the one that matches the job:
|
|
|
80
86
|
reveals the current one after async init.
|
|
81
87
|
- **Teardown** — the finishing write is awaited (`submit()` resolving means the
|
|
82
88
|
server has it), but partial saves are beacons: `await form.flush()` before a
|
|
83
|
-
programmatic page close or your test harness records a partial.
|
|
89
|
+
programmatic page close or your test harness records a partial. In a React
|
|
90
|
+
effect cleanup call `destroy()` alone — a cleanup cannot await, and beacons
|
|
91
|
+
survive an SPA unmount; `flush()` is for closing the whole page/process.
|
|
84
92
|
- **Email validation fails open** — `{ isValid: true, failed: true }` on vendor
|
|
85
93
|
outage, and a fresh domain may resolve to a definitive verdict later. Check
|
|
86
94
|
`failed` to soft-warn; don't write tests expecting stable verdicts for new
|
|
@@ -90,10 +98,15 @@ references alongside this file — read the one that matches the job:
|
|
|
90
98
|
provider does nothing (`debug: true` warns).
|
|
91
99
|
- **Custom values on the response** — `form.mergeMeta({ qualificationScore })`;
|
|
92
100
|
reserved SDK keys are dropped. Don't invent external-state types.
|
|
93
|
-
- **Scheduler** —
|
|
94
|
-
the
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
- **Scheduler** — default to ONE call: `mountScheduler({ form, container })`
|
|
102
|
+
renders the record's scheduler (any provider — Surface widget default-styled,
|
|
103
|
+
or the third-party embed), tracks the lifecycle on `form.on("scheduler")`,
|
|
104
|
+
and persists the booking automatically. Never iframe the hosted booking
|
|
105
|
+
page. Build custom UI only when explicitly asked: `mountBookingWidget` +
|
|
106
|
+
`setAnswer({ ...toSchedulerAnswerState(b), type: "CalendlyScreen" })` in
|
|
107
|
+
`onComplete`. Traps: Clari emits no booking signal (embed-only);
|
|
108
|
+
Calendly/Zoom/Reclaim mounts create the response early (their embed URLs
|
|
109
|
+
carry the responseId); `styled: false` opts out of the injected scoped CSS.
|
|
97
110
|
- **Business logic is invisible to Surface tooling.** Your qualification/
|
|
98
111
|
branching thresholds live in page code no validator can see — test them in a
|
|
99
112
|
real browser walk (both branches, plus a boundary case).
|
|
@@ -116,14 +116,38 @@ code-driven. Full event list for your own listeners: `viewed`, `started`, `stepC
|
|
|
116
116
|
</div>
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
## Scheduler step
|
|
119
|
+
## Scheduler step
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
Default: one call. `mountScheduler` renders the record's scheduler question for ANY supported
|
|
122
|
+
provider (Surface's widget default-styled, or the record's third-party embed — Calendly,
|
|
123
|
+
Cal.com, SavvyCal, Chili Piper, HubSpot, RevenueHero, Zoom, Reclaim.ai, Clari), emits the
|
|
124
|
+
lifecycle on `form.on("scheduler")`, and persists the booking into the response by itself.
|
|
125
|
+
Never iframe the hosted booking page, and never bind the question in HTML:
|
|
122
126
|
|
|
123
127
|
```html
|
|
124
128
|
<section data-step-id="step_book" hidden>
|
|
125
129
|
<div id="booking"></div>
|
|
126
130
|
</section>
|
|
131
|
+
<script>
|
|
132
|
+
const teardown = SurfaceFormsSDK.mountScheduler({
|
|
133
|
+
form,
|
|
134
|
+
container: document.querySelector("#booking"),
|
|
135
|
+
onEvent(e) {
|
|
136
|
+
if (e.type === "booking_confirmed") form.submit(); // or next()/goToStep, per your flow
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
</script>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Note the booking itself needs no wiring — persistence is automatic (`persist: false` opts out);
|
|
143
|
+
`onEvent` here only drives navigation. `styled: false` skips the injected scoped stylesheet
|
|
144
|
+
(Surface provider). Clari is embed-only (no booking signal → drive navigation yourself).
|
|
145
|
+
In preview mode the Surface path books synthetically and nothing is written.
|
|
146
|
+
|
|
147
|
+
Custom booking UI (only when asked): mount the unstyled widget directly and write the answer
|
|
148
|
+
yourself —
|
|
149
|
+
|
|
150
|
+
```html
|
|
127
151
|
<script>
|
|
128
152
|
const { createSchedulerClient, createTransport, mountBookingWidget, toSchedulerAnswerState } =
|
|
129
153
|
SurfaceFormsSDK;
|
|
@@ -144,8 +168,7 @@ A `CalendlyScreen` with `calendarType: "Surface"` renders through the SDK's own
|
|
|
144
168
|
The widget emits semantic markup under stable `surface-booking-*` classes (full DOM tree in
|
|
145
169
|
`llms.txt`) and injects no CSS — style from the page. Labels are locale-formatted by default
|
|
146
170
|
with the raw value on each element's `data-value`; pass `formatters: { month?, date?, time? }`
|
|
147
|
-
to override the copy.
|
|
148
|
-
testable offline.
|
|
171
|
+
to override the copy.
|
|
149
172
|
|
|
150
173
|
## React adapter (SPA)
|
|
151
174
|
|
|
@@ -209,7 +232,9 @@ function QualifySteps({ form }: { form: SurfaceForm }) {
|
|
|
209
232
|
}
|
|
210
233
|
```
|
|
211
234
|
|
|
212
|
-
`state` is `useSyncExternalStore`-backed: referentially stable between engine mutations, safe to render from directly. `submit()`/`disqualify()` move `state.currentStepId` to the record's matching ending step, so the same step-switch renders the confirmation. On SPA route changes call `form.identify()` to re-attribute the visitor
|
|
235
|
+
`state` is `useSyncExternalStore`-backed: referentially stable between engine mutations, safe to render from directly. `state.answers` is keyed by stepId then questionId — the example reads the current step (`state.answers[state.currentStepId]`), but an EARLIER step's answer (booking prefill, a summary screen) is read by that step's id: `state.answers["step_role"]?.["q_role"]`. `submit()`/`disqualify()` move `state.currentStepId` to the record's matching ending step, so the same step-switch renders the confirmation. On SPA route changes call `form.identify()` to re-attribute the visitor.
|
|
236
|
+
|
|
237
|
+
React teardown rule: `destroy()` alone in the effect cleanup is correct — a cleanup cannot await, partial saves are beacons that survive unmount, and the finishing write was already awaited by `submit()` itself before the cleanup can run. Reserve `await form.flush()` for programmatic teardown where the process itself may die with a write still queued (a test harness closing the browser), not for ordinary SPA unmounts.
|
|
213
238
|
|
|
214
239
|
## Tracking setup
|
|
215
240
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|