@parag.more.withsurface.com/forms-sdk 0.1.4 → 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 -2737
- package/dist/index.mjs +1 -2714
- package/dist/react/index.cjs +1 -44
- package/dist/react/index.mjs +1 -21
- package/dist/surface-forms-sdk.min.js +21 -2
- package/dist/types/bind/bindForm.d.ts +2 -36
- package/dist/types/bind/bookingWidget.d.ts +29 -17
- 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 -4
- package/dist/types/core/client/responses.d.ts +3 -15
- package/dist/types/core/client/runtimeConfig.d.ts +40 -32
- package/dist/types/core/client/scheduler.d.ts +4 -27
- package/dist/types/core/client/transport.d.ts +4 -28
- package/dist/types/core/engine/answerAudit.d.ts +3 -12
- package/dist/types/core/engine/answerStates.d.ts +4 -17
- package/dist/types/core/engine/formEngine.d.ts +16 -53
- package/dist/types/core/engine/redirect.d.ts +1 -23
- package/dist/types/core/engine/responsePayload.d.ts +4 -28
- package/dist/types/core/engine/schedulerEvents.d.ts +40 -0
- package/dist/types/core/engine/urlData.d.ts +2 -17
- package/dist/types/core/engine/urlPrefill.d.ts +1 -9
- package/dist/types/core/events/dropoffDetection.d.ts +1 -14
- package/dist/types/core/events/emitter.d.ts +1 -17
- package/dist/types/core/events/formJourneyTracker.d.ts +1 -19
- package/dist/types/core/events/pushEvents.d.ts +2 -16
- package/dist/types/core/events/startedDetection.d.ts +1 -10
- package/dist/types/core/events/viewDetection.d.ts +1 -14
- package/dist/types/core/identity/browserFingerprint.d.ts +3 -19
- package/dist/types/core/identity/ensureIdentified.d.ts +1 -16
- package/dist/types/core/identity/identify.d.ts +1 -15
- package/dist/types/core/identity/journey.d.ts +3 -16
- package/dist/types/core/identity/journey.testStubs.d.ts +1 -2
- package/dist/types/core/identity/leadDataCache.d.ts +1 -16
- package/dist/types/enrichment/attachEnrichment.d.ts +1 -21
- package/dist/types/index.d.ts +5 -3
- package/dist/types/react/index.d.ts +0 -1
- package/dist/types/react/useSurfaceForm.d.ts +1 -11
- package/dist/types/surfaceForms.d.ts +28 -73
- package/dist/types/tracking/attachTracking.d.ts +1 -22
- package/dist/types/tracking/conversionRules.d.ts +3 -12
- package/dist/types/tracking/vendors.d.ts +3 -22
- package/dist/types/validation/attachEmailValidation.d.ts +0 -1
- 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/docs/reference/identity.md +2 -3
- 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/index.cjs.map +0 -7
- package/dist/index.mjs.map +0 -7
- package/dist/react/index.cjs.map +0 -7
- package/dist/react/index.mjs.map +0 -7
- package/dist/surface-forms-sdk.min.js.map +0 -7
- package/dist/types/bind/bindForm.d.ts.map +0 -1
- package/dist/types/bind/bindForm.test.d.ts +0 -2
- package/dist/types/bind/bindForm.test.d.ts.map +0 -1
- package/dist/types/bind/bookingWidget.d.ts.map +0 -1
- package/dist/types/bind/bookingWidget.test.d.ts +0 -2
- package/dist/types/bind/bookingWidget.test.d.ts.map +0 -1
- package/dist/types/core/client/emailValidation.d.ts.map +0 -1
- package/dist/types/core/client/emailValidation.test.d.ts +0 -2
- package/dist/types/core/client/emailValidation.test.d.ts.map +0 -1
- package/dist/types/core/client/responses.d.ts.map +0 -1
- package/dist/types/core/client/responses.test.d.ts +0 -2
- package/dist/types/core/client/responses.test.d.ts.map +0 -1
- package/dist/types/core/client/runtimeConfig.contract.test.d.ts +0 -2
- package/dist/types/core/client/runtimeConfig.contract.test.d.ts.map +0 -1
- package/dist/types/core/client/runtimeConfig.d.ts.map +0 -1
- package/dist/types/core/client/runtimeConfig.test.d.ts +0 -2
- package/dist/types/core/client/runtimeConfig.test.d.ts.map +0 -1
- package/dist/types/core/client/scheduler.contract.test.d.ts +0 -2
- package/dist/types/core/client/scheduler.contract.test.d.ts.map +0 -1
- package/dist/types/core/client/scheduler.d.ts.map +0 -1
- package/dist/types/core/client/scheduler.test.d.ts +0 -2
- package/dist/types/core/client/scheduler.test.d.ts.map +0 -1
- package/dist/types/core/client/transport.d.ts.map +0 -1
- package/dist/types/core/client/transport.test.d.ts +0 -2
- package/dist/types/core/client/transport.test.d.ts.map +0 -1
- package/dist/types/core/engine/answerAudit.d.ts.map +0 -1
- package/dist/types/core/engine/answerAudit.test.d.ts +0 -2
- package/dist/types/core/engine/answerAudit.test.d.ts.map +0 -1
- package/dist/types/core/engine/answerStates.contract.test.d.ts +0 -2
- package/dist/types/core/engine/answerStates.contract.test.d.ts.map +0 -1
- package/dist/types/core/engine/answerStates.d.ts.map +0 -1
- package/dist/types/core/engine/formEngine.d.ts.map +0 -1
- package/dist/types/core/engine/formEngine.test.d.ts +0 -2
- package/dist/types/core/engine/formEngine.test.d.ts.map +0 -1
- package/dist/types/core/engine/redirect.d.ts.map +0 -1
- package/dist/types/core/engine/redirect.test.d.ts +0 -2
- package/dist/types/core/engine/redirect.test.d.ts.map +0 -1
- package/dist/types/core/engine/responsePayload.contract.test.d.ts +0 -2
- package/dist/types/core/engine/responsePayload.contract.test.d.ts.map +0 -1
- package/dist/types/core/engine/responsePayload.d.ts.map +0 -1
- package/dist/types/core/engine/responsePayload.test.d.ts +0 -2
- package/dist/types/core/engine/responsePayload.test.d.ts.map +0 -1
- package/dist/types/core/engine/urlData.contract.test.d.ts +0 -2
- package/dist/types/core/engine/urlData.contract.test.d.ts.map +0 -1
- package/dist/types/core/engine/urlData.d.ts.map +0 -1
- package/dist/types/core/engine/urlData.test.d.ts +0 -2
- package/dist/types/core/engine/urlData.test.d.ts.map +0 -1
- package/dist/types/core/engine/urlPrefill.d.ts.map +0 -1
- package/dist/types/core/engine/urlPrefill.test.d.ts +0 -2
- package/dist/types/core/engine/urlPrefill.test.d.ts.map +0 -1
- package/dist/types/core/events/dropoffDetection.d.ts.map +0 -1
- package/dist/types/core/events/dropoffDetection.test.d.ts +0 -2
- package/dist/types/core/events/dropoffDetection.test.d.ts.map +0 -1
- package/dist/types/core/events/emitter.d.ts.map +0 -1
- package/dist/types/core/events/emitter.test.d.ts +0 -2
- package/dist/types/core/events/emitter.test.d.ts.map +0 -1
- package/dist/types/core/events/formJourneyTracker.contract.test.d.ts +0 -2
- package/dist/types/core/events/formJourneyTracker.contract.test.d.ts.map +0 -1
- package/dist/types/core/events/formJourneyTracker.d.ts.map +0 -1
- package/dist/types/core/events/formJourneyTracker.test.d.ts +0 -2
- package/dist/types/core/events/formJourneyTracker.test.d.ts.map +0 -1
- package/dist/types/core/events/pushEvents.contract.test.d.ts +0 -2
- package/dist/types/core/events/pushEvents.contract.test.d.ts.map +0 -1
- package/dist/types/core/events/pushEvents.d.ts.map +0 -1
- package/dist/types/core/events/pushEvents.test.d.ts +0 -2
- package/dist/types/core/events/pushEvents.test.d.ts.map +0 -1
- package/dist/types/core/events/startedDetection.d.ts.map +0 -1
- package/dist/types/core/events/startedDetection.test.d.ts +0 -2
- package/dist/types/core/events/startedDetection.test.d.ts.map +0 -1
- package/dist/types/core/events/viewDetection.d.ts.map +0 -1
- package/dist/types/core/events/viewDetection.test.d.ts +0 -2
- package/dist/types/core/events/viewDetection.test.d.ts.map +0 -1
- package/dist/types/core/identity/browserFingerprint.d.ts.map +0 -1
- package/dist/types/core/identity/browserFingerprint.test.d.ts +0 -2
- package/dist/types/core/identity/browserFingerprint.test.d.ts.map +0 -1
- package/dist/types/core/identity/ensureIdentified.d.ts.map +0 -1
- package/dist/types/core/identity/ensureIdentified.test.d.ts +0 -2
- package/dist/types/core/identity/ensureIdentified.test.d.ts.map +0 -1
- package/dist/types/core/identity/identify.contract.test.d.ts +0 -2
- package/dist/types/core/identity/identify.contract.test.d.ts.map +0 -1
- package/dist/types/core/identity/identify.d.ts.map +0 -1
- package/dist/types/core/identity/identify.test.d.ts +0 -2
- package/dist/types/core/identity/identify.test.d.ts.map +0 -1
- package/dist/types/core/identity/journey.contract.test.d.ts +0 -2
- package/dist/types/core/identity/journey.contract.test.d.ts.map +0 -1
- package/dist/types/core/identity/journey.d.ts.map +0 -1
- package/dist/types/core/identity/journey.test.d.ts +0 -2
- package/dist/types/core/identity/journey.test.d.ts.map +0 -1
- package/dist/types/core/identity/journey.testStubs.d.ts.map +0 -1
- package/dist/types/core/identity/leadDataCache.d.ts.map +0 -1
- package/dist/types/core/identity/leadDataCache.test.d.ts +0 -2
- package/dist/types/core/identity/leadDataCache.test.d.ts.map +0 -1
- package/dist/types/enrichment/attachEnrichment.d.ts.map +0 -1
- package/dist/types/enrichment/attachEnrichment.test.d.ts +0 -2
- package/dist/types/enrichment/attachEnrichment.test.d.ts.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/react/index.d.ts.map +0 -1
- package/dist/types/react/ssr.test.d.ts +0 -2
- package/dist/types/react/ssr.test.d.ts.map +0 -1
- package/dist/types/react/useSurfaceForm.d.ts.map +0 -1
- package/dist/types/react/useSurfaceForm.test.d.ts +0 -2
- package/dist/types/react/useSurfaceForm.test.d.ts.map +0 -1
- package/dist/types/surfaceForms.d.ts.map +0 -1
- package/dist/types/surfaceForms.test.d.ts +0 -2
- package/dist/types/surfaceForms.test.d.ts.map +0 -1
- package/dist/types/tracking/attachTracking.d.ts.map +0 -1
- package/dist/types/tracking/attachTracking.test.d.ts +0 -2
- package/dist/types/tracking/attachTracking.test.d.ts.map +0 -1
- package/dist/types/tracking/conversionRules.contract.test.d.ts +0 -2
- package/dist/types/tracking/conversionRules.contract.test.d.ts.map +0 -1
- package/dist/types/tracking/conversionRules.d.ts.map +0 -1
- package/dist/types/tracking/conversionRules.test.d.ts +0 -2
- package/dist/types/tracking/conversionRules.test.d.ts.map +0 -1
- package/dist/types/tracking/vendors.d.ts.map +0 -1
- package/dist/types/tracking/vendors.test.d.ts +0 -2
- package/dist/types/tracking/vendors.test.d.ts.map +0 -1
- package/dist/types/validation/attachEmailValidation.d.ts.map +0 -1
- package/dist/types/validation/attachEmailValidation.test.d.ts +0 -2
- package/dist/types/validation/attachEmailValidation.test.d.ts.map +0 -1
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Ad-conversion rule engine (forms-sdk-plan.md row 55): a dependency-free port
|
|
3
|
-
* of form-render's `adConversions/{evaluateRules,fireConversion}.ts` and the
|
|
4
|
-
* `ZAdConversionRule` settings schema. The SDK always runs first-party, so the
|
|
5
|
-
* native parent-page/postMessage/ack hop disappears — matching rules fire
|
|
6
|
-
* directly on this window. Rule semantics, provider payloads, and the
|
|
7
|
-
* `responseId` dedupe key are identical by construction; the contract test
|
|
8
|
-
* validates these local types against the real Zod schema.
|
|
9
|
-
*/
|
|
1
|
+
|
|
10
2
|
export type AdConversionProvider = "x" | "meta" | "openai" | "ga4" | "linkedin";
|
|
11
3
|
export type AdConversionTrigger = "reached_qualified" | "form_submitted" | "reached_step" | "form_started" | "step_submitted" | "email_provided" | "meeting_booked";
|
|
12
4
|
export type AdConversionCondition = {
|
|
@@ -77,11 +69,10 @@ export declare const DEFAULT_OPENAI_EVENT_NAME = "lead_created";
|
|
|
77
69
|
export declare const matchesCondition: (condition: AdConversionCondition, params: TrackingParams) => boolean;
|
|
78
70
|
export declare const matchesConditions: (conditions: AdConversionCondition[], params: TrackingParams, operator?: "and" | "or") => boolean;
|
|
79
71
|
export declare const ruleToConversionEvent: (rule: AdConversionRule) => ConversionEvent | null;
|
|
80
|
-
|
|
72
|
+
|
|
81
73
|
export declare const selectFiringEvents: (rules: AdConversionRule[], trigger: AdConversionTrigger, params: TrackingParams, stepId?: string) => {
|
|
82
74
|
rule: AdConversionRule;
|
|
83
75
|
conversion: ConversionEvent;
|
|
84
76
|
}[];
|
|
85
|
-
|
|
77
|
+
|
|
86
78
|
export declare const fireConversion: (win: Window, conversion: ConversionEvent, ctx: FireContext) => boolean;
|
|
87
|
-
//# sourceMappingURL=conversionRules.d.ts.map
|
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Customer-tag bootstrap + lifecycle emission (forms-sdk-plan.md rows 51–54,
|
|
3
|
-
* 58): GTM container, GA4, Meta Pixel, and HubSpot from the form's settings,
|
|
4
|
-
* with the same don't-double-inject guards as the native runtime and the
|
|
5
|
-
* parent tag. No React, no react-gtm-module / react-facebook-pixel — direct
|
|
6
|
-
* global bootstraps like the parent tag's providers.
|
|
7
|
-
*
|
|
8
|
-
* Three native quirks are FIXED here by decision (2026-08-10, memory +
|
|
9
|
-
* plan rows 51–52) — the parity harness whitelists them as intentional diffs:
|
|
10
|
-
* - GTM `SurfaceFormSubmitEvent` carries `formId` (native omits it).
|
|
11
|
-
* - `stepId` in `SurfaceFormStepSubmitted` is the real step id (native sends
|
|
12
|
-
* the step *name*) — migration note for name-keyed GTM triggers.
|
|
13
|
-
* - GA4 config does NOT hardcode `debug_mode: true`.
|
|
14
|
-
*/
|
|
1
|
+
|
|
15
2
|
export type LifecycleUrlData = {
|
|
16
3
|
url: string;
|
|
17
4
|
urlParams: Record<string, string>;
|
|
@@ -25,22 +12,16 @@ export type VendorSettings = {
|
|
|
25
12
|
};
|
|
26
13
|
};
|
|
27
14
|
export type StepSubmittedPayload = LifecycleUrlData & {
|
|
28
|
-
|
|
15
|
+
|
|
29
16
|
stepId: string;
|
|
30
17
|
formId: string;
|
|
31
18
|
emailProvided: boolean;
|
|
32
19
|
meetingBooked: boolean;
|
|
33
20
|
};
|
|
34
|
-
|
|
35
|
-
* One instance per SurfaceForms.init (no module-level singletons — §2.7):
|
|
36
|
-
* injects the configured vendors immediately, exposes the three lifecycle
|
|
37
|
-
* emitters with native event names/payload keys (they are customers' GTM
|
|
38
|
-
* trigger contracts).
|
|
39
|
-
*/
|
|
21
|
+
|
|
40
22
|
export declare const createVendorTracker: (win: Window, settings: VendorSettings) => {
|
|
41
23
|
formStarted: (formId: string, urlData: LifecycleUrlData) => void;
|
|
42
24
|
stepSubmitted: (payload: StepSubmittedPayload) => void;
|
|
43
25
|
formCompleted: (formId: string, urlData: LifecycleUrlData) => void;
|
|
44
26
|
};
|
|
45
27
|
export type VendorTracker = ReturnType<typeof createVendorTracker>;
|
|
46
|
-
//# sourceMappingURL=vendors.d.ts.map
|
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
|
|
|
@@ -5,9 +5,8 @@ Exhaustive reference for the `core/identity` domain. Map of everything: [`../api
|
|
|
5
5
|
## Fingerprint (`core/identity/browserFingerprint`)
|
|
6
6
|
|
|
7
7
|
- **`getBrowserFingerprint(environmentId): Promise<Fingerprint>`** — the canonical Surface
|
|
8
|
-
fingerprint.
|
|
9
|
-
|
|
10
|
-
your own variant — a different serialization splits one visitor into two leads.
|
|
8
|
+
fingerprint. Its serialization is a server-side identity contract: always use this function
|
|
9
|
+
as-is — a hand-rolled variant produces a different `id` and splits one visitor into two leads.
|
|
11
10
|
- **`getHash(input): Promise<string>`** — SHA-256 hex via Web Crypto.
|
|
12
11
|
|
|
13
12
|
```ts
|
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).
|