@captello/ulc-webview-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +424 -0
- package/dist/chunk-ETF52K7K.js +91 -0
- package/dist/chunk-ETF52K7K.js.map +1 -0
- package/dist/chunk-ZMYMZK2A.js +328 -0
- package/dist/chunk-ZMYMZK2A.js.map +1 -0
- package/dist/client-3IBxKbIE.d.cts +561 -0
- package/dist/client-3IBxKbIE.d.ts +561 -0
- package/dist/index.cjs +594 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +121 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +166 -0
- package/dist/index.js.map +1 -0
- package/dist/promises.cjs +366 -0
- package/dist/promises.cjs.map +1 -0
- package/dist/promises.d.cts +72 -0
- package/dist/promises.d.ts +72 -0
- package/dist/promises.js +51 -0
- package/dist/promises.js.map +1 -0
- package/dist/react.cjs +476 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +108 -0
- package/dist/react.d.ts +108 -0
- package/dist/react.js +112 -0
- package/dist/react.js.map +1 -0
- package/package.json +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lead Liaison, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# @captello/ulc-webview-sdk
|
|
2
|
+
|
|
3
|
+
Typed SDK for embedding the **Captello capture webview** in a host application
|
|
4
|
+
(React, plain JS, or any framework). It encodes the exact `postMessage` contract
|
|
5
|
+
the webview speaks, so integrators don't have to reverse-engineer message strings.
|
|
6
|
+
|
|
7
|
+
It provides:
|
|
8
|
+
|
|
9
|
+
1. **The message protocol** — enums and discriminated-union types for every
|
|
10
|
+
message the webview emits and accepts.
|
|
11
|
+
2. **`CaptelloWebview`** — a host-side client that wraps an `<iframe>` and handles
|
|
12
|
+
the send/receive wire details (JSON-string encoding, origin/source filtering).
|
|
13
|
+
3. **`buildEmbedUrl`** — a typed builder for the webview's query-string contract.
|
|
14
|
+
4. **Submission rendering** — `toDisplayPairs` / `parseVisibleSubmissionsData` to turn a
|
|
15
|
+
submission's `visible_submissions_data` into typed key/value rows.
|
|
16
|
+
5. **Promise helpers** (`@captello/ulc-webview-sdk/promises`) — one-shot `await`-style
|
|
17
|
+
utilities like `submitForm(iframe)` and `waitForFormLoad(iframe)`.
|
|
18
|
+
6. **React adapter** (`@captello/ulc-webview-sdk/react`) — a `useCaptelloWebview` hook that
|
|
19
|
+
manages the client lifecycle and returns an iframe ref plus typed senders.
|
|
20
|
+
|
|
21
|
+
The core package is framework-agnostic with no runtime dependencies; React is an
|
|
22
|
+
optional peer dependency used only by the `/react` entry point.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add @captello/ulc-webview-sdk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import {
|
|
34
|
+
CaptelloWebview,
|
|
35
|
+
buildEmbedUrl,
|
|
36
|
+
targetOriginFromUrl,
|
|
37
|
+
FormMode,
|
|
38
|
+
LauncherType,
|
|
39
|
+
OutboundMessageType,
|
|
40
|
+
} from "@captello/ulc-webview-sdk";
|
|
41
|
+
|
|
42
|
+
// 1. Build the embed URL.
|
|
43
|
+
const src = buildEmbedUrl("https://capture.captello.com/", {
|
|
44
|
+
formId: 1234,
|
|
45
|
+
mode: FormMode.Submit,
|
|
46
|
+
launcher: LauncherType.EventGenWeb,
|
|
47
|
+
language: "en",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// 2. Point an iframe at it.
|
|
51
|
+
const iframe = document.createElement("iframe");
|
|
52
|
+
iframe.src = src;
|
|
53
|
+
document.body.appendChild(iframe);
|
|
54
|
+
|
|
55
|
+
// 3. Wire up the client (scope messages to the webview's origin).
|
|
56
|
+
const webview = new CaptelloWebview(iframe, {
|
|
57
|
+
targetOrigin: targetOriginFromUrl(src),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
webview.on(OutboundMessageType.FormLoadComplete, () => {
|
|
61
|
+
console.log("form is ready");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
webview.on(OutboundMessageType.SubmissionBody, (msg) => {
|
|
65
|
+
// Embedded forms hand the submission to the host instead of submitting directly.
|
|
66
|
+
persist(msg.data);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
webview.on(OutboundMessageType.FormErrorMessage, (msg) => {
|
|
70
|
+
showToast(msg.data); // already translated & display-ready
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// 4. Drive the form programmatically.
|
|
74
|
+
submitButton.onclick = () => webview.submit();
|
|
75
|
+
|
|
76
|
+
// 5. Tear down when the iframe is removed.
|
|
77
|
+
webview.destroy();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Submit and await the result
|
|
81
|
+
|
|
82
|
+
For the common "click submit, then act on the outcome" flow, use `submitAndWait()`
|
|
83
|
+
instead of wiring `submit()` to separate listeners. It resolves with the submission
|
|
84
|
+
body on `submission_body`, rejects with a `SubmissionError` on `form_error_message`,
|
|
85
|
+
and rejects with a `SubmissionTimeoutError` if neither arrives in time — cleaning up
|
|
86
|
+
its listeners in every case.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { SubmissionError } from "@captello/ulc-webview-sdk";
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const body = await webview.submitAndWait(); // default 60s timeout
|
|
93
|
+
await persistSubmission(body);
|
|
94
|
+
closeDialog();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err instanceof SubmissionError) {
|
|
97
|
+
showToast(err.message); // translated, display-ready
|
|
98
|
+
} else {
|
|
99
|
+
// SubmissionTimeoutError or a send failure
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## React — `@captello/ulc-webview-sdk/react`
|
|
105
|
+
|
|
106
|
+
The React adapter is the smoothest way to integrate. `useCaptelloWebview` owns a
|
|
107
|
+
`CaptelloWebview` for the iframe's lifetime: it builds the embed URL, creates the client
|
|
108
|
+
when the iframe mounts, wires outbound messages to typed callbacks, tracks readiness,
|
|
109
|
+
and destroys the client on unmount. You get back `iframeProps` to spread, an `isReady`
|
|
110
|
+
flag, and stable senders — so a typical form is just the hook plus an `<iframe>`.
|
|
111
|
+
|
|
112
|
+
`react` is an optional peer dependency (React 18+).
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { FormMode, LauncherType, ActionButtonPosition, type SubmissionBody } from "@captello/ulc-webview-sdk";
|
|
116
|
+
import { useCaptelloWebview } from "@captello/ulc-webview-sdk/react";
|
|
117
|
+
|
|
118
|
+
function UlcForm({
|
|
119
|
+
eventWebAccessToken,
|
|
120
|
+
onSubmitted,
|
|
121
|
+
}: {
|
|
122
|
+
eventWebAccessToken: string;
|
|
123
|
+
onSubmitted: (body: SubmissionBody) => void;
|
|
124
|
+
}) {
|
|
125
|
+
const { iframeProps, isReady, submit, prefillInfo } = useCaptelloWebview({
|
|
126
|
+
embedUrl: {
|
|
127
|
+
baseUrl: "https://capture.captello.com/capture/submission",
|
|
128
|
+
eventWebAccessToken,
|
|
129
|
+
actionButtonPosition: ActionButtonPosition.Hidden, // b=2
|
|
130
|
+
mode: FormMode.Submit,
|
|
131
|
+
launcher: LauncherType.EventGenWeb,
|
|
132
|
+
},
|
|
133
|
+
onSubmissionBody: (m) => onSubmitted(m.data),
|
|
134
|
+
onFormErrorMessage: (m) => showToast(m.data),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<>
|
|
139
|
+
{!isReady && <Spinner />}
|
|
140
|
+
<iframe {...iframeProps} title="UlcForm" allow="camera; microphone" />
|
|
141
|
+
<button onClick={submit}>Submit</button>
|
|
142
|
+
</>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
What the hook handles for you:
|
|
148
|
+
|
|
149
|
+
- **URL + origin.** Pass `embedUrl: { baseUrl, ...EmbedUrlOptions }` and the hook builds
|
|
150
|
+
the URL, derives `targetOrigin`, and returns it as `iframeProps.src` — no separate
|
|
151
|
+
`buildEmbedUrl` / `targetOriginFromUrl` / manual `src` wiring to keep in sync. (Prefer
|
|
152
|
+
to own the `src`? Omit `embedUrl`, pass `targetOrigin`, and use the returned `ref`.)
|
|
153
|
+
- **Readiness.** `isReady` and `status` (`"loading" | "ready" | "error"`) — no manual
|
|
154
|
+
`useState` + `onFormLoadComplete` for a spinner.
|
|
155
|
+
- **Prefill timing.** Sends made before the form loads are queued and flushed on
|
|
156
|
+
`form_load_complete`, so you can `prefillInfo(...)` as soon as you have data — no
|
|
157
|
+
gating on readiness, and no silently-dropped messages.
|
|
158
|
+
- **No memoization.** Callbacks are read fresh via a ref, so inline arrow functions
|
|
159
|
+
won't re-subscribe or re-create the client. The client is recreated only when
|
|
160
|
+
`targetOrigin` (or `embedUrl`) / `hostWindow` / `matchSource` / `queueUntilReady` change.
|
|
161
|
+
- **Stable senders** (`submit`, `reset`, `updateDraft`, `triggerValidation`,
|
|
162
|
+
`prefillInfo`, `prefillSubmission`, `prefillSubmissionAndInfo`, `submitAndWait`) — safe
|
|
163
|
+
in deps or passed to children. `getClient()` returns the live client for escape hatches.
|
|
164
|
+
|
|
165
|
+
### Callback props
|
|
166
|
+
|
|
167
|
+
`useCaptelloWebview` accepts a callback per outbound message — `onFormLoadComplete`,
|
|
168
|
+
`onFormErrorMessage`, `onSubmissionBody`, `onFormSubmitSuccess`,
|
|
169
|
+
`onConnexionsProfileRedirect`, `onConnexionsDownloadVcard` — plus `onAnyMessage` (fires
|
|
170
|
+
for every message, after the specific handler). All are optional. Connection options
|
|
171
|
+
(`embedUrl` or `targetOrigin`, `hostWindow`, `matchSource`, `queueUntilReady`) go in the
|
|
172
|
+
same object.
|
|
173
|
+
|
|
174
|
+
### Without the adapter
|
|
175
|
+
|
|
176
|
+
If you don't want the hook, create a `CaptelloWebview` yourself in an effect against an
|
|
177
|
+
iframe ref, subscribe with `.on(...)`, and call `.destroy()` on unmount. Hold the client
|
|
178
|
+
in a ref (or context) so sibling components can drive it without querying the DOM.
|
|
179
|
+
|
|
180
|
+
## The message contract
|
|
181
|
+
|
|
182
|
+
**Wire format:** every message is a JSON **string** with a `type` discriminator.
|
|
183
|
+
The SDK handles this for you — `CaptelloWebview` `JSON.stringify`s outgoing messages
|
|
184
|
+
(the webview parses inbound data with `JSON.parse`, so a raw object would be silently
|
|
185
|
+
ignored) and parses + validates incoming ones. Direction is named from the webview's
|
|
186
|
+
point of view.
|
|
187
|
+
|
|
188
|
+
### Outbound — webview → host (you listen)
|
|
189
|
+
|
|
190
|
+
| `type` | Constant | Payload | Meaning |
|
|
191
|
+
| ----------------------------- | ----------------------------------------------- | ---------------------- | --------------------------------------------- |
|
|
192
|
+
| `form_load_complete` | `OutboundMessageType.FormLoadComplete` | — | Form finished loading/rendering. |
|
|
193
|
+
| `form_error_message` | `OutboundMessageType.FormErrorMessage` | `data: string` | Translated, display-ready error message. |
|
|
194
|
+
| `submission_body` | `OutboundMessageType.SubmissionBody` | `data: SubmissionBody` | Full submission for the host to persist. |
|
|
195
|
+
| `form_submit_success` | `OutboundMessageType.FormSubmitSuccess` | — | Submission succeeded (kiosk / quick-capture). |
|
|
196
|
+
| `connexions_profile_redirect` | `OutboundMessageType.ConnexionsProfileRedirect` | — | Host should perform the profile redirect. |
|
|
197
|
+
| `connexions_download_vcard` | `OutboundMessageType.ConnexionsDownloadVcard` | — | Host should trigger the vCard download. |
|
|
198
|
+
|
|
199
|
+
### Inbound — host → webview (you send)
|
|
200
|
+
|
|
201
|
+
| `type` | Method | Notes |
|
|
202
|
+
| -------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
|
203
|
+
| `submit_form` | `webview.submit()` | Submit as if the user pressed the button. |
|
|
204
|
+
| `reset_form` | `webview.reset()` | Clear all entered values. |
|
|
205
|
+
| `update_draft` | `webview.updateDraft()` | Switch to draft-update mode. |
|
|
206
|
+
| `trigger_validation` | `webview.triggerValidation(target)` | `target`: `"email" \| "invitation_code" \| "all"`. |
|
|
207
|
+
| `form_prefill` | `webview.prefillSubmission(...)` / `prefillInfo(...)` / `prefillSubmissionAndInfo(...)` | Three payload shapes (see below). |
|
|
208
|
+
|
|
209
|
+
### Prefill variants (`form_prefill`)
|
|
210
|
+
|
|
211
|
+
`form_prefill` carries a `data_type` selecting the payload shape:
|
|
212
|
+
|
|
213
|
+
- `PrefillDataType.UlcSubmission` → `prefillSubmission(submission)` — a `SubmissionPrefill`
|
|
214
|
+
(a received `SubmissionBody` round-tripped, or a partial `{ data?, ...} ` you assemble).
|
|
215
|
+
- `PrefillDataType.Info` → `prefillInfo(items)` — a list of `PrefillInfoItem`. The webview
|
|
216
|
+
matches each item by `ll_field_unique_identifier` (e.g. `"FirstName"`, `"Email"`);
|
|
217
|
+
`ll_field_id` is optional metadata (number or string) and `value` may be a string or boolean.
|
|
218
|
+
- `PrefillDataType.UlcSubmissionAndInfo` → `prefillSubmissionAndInfo({ submission, info })`.
|
|
219
|
+
|
|
220
|
+
## Rendering a submission as key/value pairs
|
|
221
|
+
|
|
222
|
+
A `submission_body` payload includes `visible_submissions_data` — one entry per filled,
|
|
223
|
+
visible element, discriminated by `element_type` (so `element_value`'s shape depends on
|
|
224
|
+
the type: strings, string arrays, composites like name/address, order quantities, etc.).
|
|
225
|
+
|
|
226
|
+
To turn that into rows for the UI, use `toDisplayPairs` (parse + flatten in one call):
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { toDisplayPairs, OutboundMessageType } from "@captello/ulc-webview-sdk";
|
|
230
|
+
|
|
231
|
+
webview.on(OutboundMessageType.SubmissionBody, (msg) => {
|
|
232
|
+
const rows = toDisplayPairs(msg.data);
|
|
233
|
+
// rows: { elementId: string; label: string; value: string }[]
|
|
234
|
+
rows.forEach((r) => addRow(r.label, r.value));
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`toDisplayPairs` (and the lower-level `parseVisibleSubmissionsData`) accept `unknown` —
|
|
239
|
+
pass the whole `submission_body` data, the `visible_submissions_data` array itself, or
|
|
240
|
+
any value; entries that don't match the contract (structural/unknown types, missing
|
|
241
|
+
fields) are dropped, so the result is always safe to map over.
|
|
242
|
+
|
|
243
|
+
For custom rendering, parse to typed items and narrow on `element_type`:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { parseVisibleSubmissionsData, FormElementType } from "@captello/ulc-webview-sdk";
|
|
247
|
+
|
|
248
|
+
for (const item of parseVisibleSubmissionsData(msg.data)) {
|
|
249
|
+
if (item.element_type === FormElementType.checkbox) {
|
|
250
|
+
item.element_value; // string[] | OrderCheckboxSubmissionData — precisely typed
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`toDisplayValue(item, options?)` flattens one item; `options` accepts `listSeparator`
|
|
256
|
+
(default `", "`), `booleanLabels` (default `{ true: "Yes", false: "No" }`), and
|
|
257
|
+
`emptyText` (default `""`).
|
|
258
|
+
|
|
259
|
+
## `buildEmbedUrl(baseUrl, options)`
|
|
260
|
+
|
|
261
|
+
Maps friendly option names onto the webview's short query keys. Existing params on
|
|
262
|
+
`baseUrl` are preserved; options override matching keys.
|
|
263
|
+
|
|
264
|
+
| Option | Query key | Notes |
|
|
265
|
+
| --------------------------- | ----------- | --------------------------------------------------------------------------------- |
|
|
266
|
+
| `formId` | `f` | |
|
|
267
|
+
| `submissionToken` | `s` | |
|
|
268
|
+
| `stationId` | `st` | |
|
|
269
|
+
| `mode` | `m` | `FormMode` enum. |
|
|
270
|
+
| `eventWebAccessToken` | `e` | |
|
|
271
|
+
| `activationId` | `a` | |
|
|
272
|
+
| `language` | `l` | |
|
|
273
|
+
| `actionButtonPosition` | `b` | `ActionButtonPosition` enum: `Fixed` (`"0"`), `Bottom` (`"1"`), `Hidden` (`"2"`). |
|
|
274
|
+
| `formType` | `form_type` | `"template" \| "device"`. |
|
|
275
|
+
| `launcher` | `launcher` | `LauncherType` enum. |
|
|
276
|
+
| `submissionType` | `t` | `"normal" \| "drafted"`. |
|
|
277
|
+
| `submitButtonBottomPadding` | `sbbp` | |
|
|
278
|
+
| `useIn` | `useIn` | `"outbound" \| "inbound" \| "notes"`. |
|
|
279
|
+
| `platform` | `platform` | `"web" \| "mobile"`. |
|
|
280
|
+
| `hideEmail` | `he` | Boolean → `"1"` when true, omitted when false. |
|
|
281
|
+
| `connexionsEmbedMode` | `cem` | Boolean → `"1"`. |
|
|
282
|
+
| `emro` | `emro` | Boolean → `"1"`. Edit mode read-only. |
|
|
283
|
+
| `extraParams` | (verbatim) | Appended as-is; `undefined`/`null` skipped. |
|
|
284
|
+
|
|
285
|
+
## `CaptelloWebview` API
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
new CaptelloWebview(iframe, {
|
|
289
|
+
targetOrigin?: string; // recommend the webview origin; defaults to "*"
|
|
290
|
+
hostWindow?: Window; // defaults to global window
|
|
291
|
+
matchSource?: boolean; // default true: only accept messages from this iframe
|
|
292
|
+
queueUntilReady?: boolean; // default true: buffer sends until form_load_complete
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
- `isReady` — `true` once the form has reported `form_load_complete`.
|
|
297
|
+
- `on(type, listener) => unsubscribe` — subscribe to one outbound type.
|
|
298
|
+
- `once(type, listener) => unsubscribe` — fire at most once.
|
|
299
|
+
- `onAny(listener) => unsubscribe` — every outbound message.
|
|
300
|
+
- `submit()`, `reset()`, `updateDraft()`, `triggerValidation(target)` — inbound helpers.
|
|
301
|
+
- `submitAndWait(timeoutMs?)` — submit and await `submission_body` / `form_error_message` (see above).
|
|
302
|
+
- `prefillSubmission(...)`, `prefillInfo(...)`, `prefillSubmissionAndInfo(...)`.
|
|
303
|
+
- `send(message)` — low-level escape hatch for any `InboundMessage`.
|
|
304
|
+
- `destroy()` — detach the listener and drop subscriptions (idempotent).
|
|
305
|
+
|
|
306
|
+
**Send queueing.** With `queueUntilReady` (default `true`), any send before the webview
|
|
307
|
+
reports `form_load_complete` is buffered and flushed, in order, on load — so calling
|
|
308
|
+
`prefillInfo(...)` right after mount won't be silently dropped. A client that attaches
|
|
309
|
+
_after_ the form already loaded won't observe `form_load_complete`; either create the
|
|
310
|
+
client with the iframe, or pass `queueUntilReady: false` to send immediately. The
|
|
311
|
+
one-shot `/promises` helpers set `queueUntilReady: false` automatically.
|
|
312
|
+
|
|
313
|
+
### Security note
|
|
314
|
+
|
|
315
|
+
Always set `targetOrigin` to the webview's origin in production. With the default
|
|
316
|
+
`"*"`, the client accepts messages from any origin and posts without an origin check —
|
|
317
|
+
acceptable only for trusted/local development. `targetOriginFromUrl(embedUrl)` derives
|
|
318
|
+
it from the URL you built.
|
|
319
|
+
|
|
320
|
+
## Promise helpers — `@captello/ulc-webview-sdk/promises`
|
|
321
|
+
|
|
322
|
+
A separate entry point with one-shot, `await`-style helpers for imperative flows.
|
|
323
|
+
Where `CaptelloWebview` is a long-lived client you subscribe to, these take an iframe
|
|
324
|
+
directly, create a short-lived client internally, wait for the relevant message, and
|
|
325
|
+
tear it down — convenient when you just want to "submit and get the body" without
|
|
326
|
+
managing a client instance.
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { submitForm, waitForFormLoad, waitForMessage, SubmissionError } from "@captello/ulc-webview-sdk/promises";
|
|
330
|
+
import { OutboundMessageType } from "@captello/ulc-webview-sdk";
|
|
331
|
+
|
|
332
|
+
await waitForFormLoad(iframe, { targetOrigin }); // resolves on form_load_complete
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const body = await submitForm(iframe, { targetOrigin }); // submit + await result
|
|
336
|
+
await persist(body);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (err instanceof SubmissionError) showToast(err.message);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// generic: await the next message of any outbound type
|
|
342
|
+
const msg = await waitForMessage(iframe, OutboundMessageType.SubmissionBody, { targetOrigin });
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
| Helper | Resolves / rejects |
|
|
346
|
+
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
|
|
347
|
+
| `submitForm(frame, opts?)` | resolves `SubmissionBody`; rejects `SubmissionError` (on `form_error_message`) or `SubmissionTimeoutError` |
|
|
348
|
+
| `waitForFormLoad(frame, opts?)` | resolves `void` on `form_load_complete`; rejects `MessageTimeoutError` |
|
|
349
|
+
| `waitForMessage(frame, type, opts?)` | resolves the typed message; rejects `MessageTimeoutError` |
|
|
350
|
+
|
|
351
|
+
`opts` is `{ targetOrigin?, hostWindow?, matchSource?, timeoutMs? }` (`timeoutMs`
|
|
352
|
+
defaults to 60_000; `0`/`Infinity` waits indefinitely). All helpers remove their
|
|
353
|
+
internal listener before settling, including on timeout.
|
|
354
|
+
|
|
355
|
+
> Note: these catch a _future_ message. If the form may already have loaded before you
|
|
356
|
+
> call `waitForFormLoad` (e.g. you attach late), create a long-lived `CaptelloWebview`
|
|
357
|
+
> before the iframe navigates instead.
|
|
358
|
+
|
|
359
|
+
## Migrating an existing integration
|
|
360
|
+
|
|
361
|
+
Host apps that integrated before this SDK typically hand-rolled the same three pieces:
|
|
362
|
+
their own copy of the message-type strings, a manual `URLSearchParams` builder with the
|
|
363
|
+
short keys, and ad-hoc `window.addEventListener("message")` / `iframe.contentWindow.postMessage`
|
|
364
|
+
calls. Replace them as follows.
|
|
365
|
+
|
|
366
|
+
**1. Message-type constants → SDK enums.** Delete local copies (e.g. `UlcFormActionTypeSent`,
|
|
367
|
+
`UlcFormActionTypeReceived`, `UlcFormDataType`) and import `InboundMessageType`,
|
|
368
|
+
`OutboundMessageType`, `PrefillDataType`.
|
|
369
|
+
|
|
370
|
+
**2. Manual URL building → `buildEmbedUrl`.**
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
// before
|
|
374
|
+
const src = `${base}/capture/submission?e=${token}&b=2&m=submit&launcher=event_gen_web` + (code ? `&l=${code}` : "");
|
|
375
|
+
|
|
376
|
+
// after
|
|
377
|
+
const src = buildEmbedUrl(`${base}/capture/submission`, {
|
|
378
|
+
eventWebAccessToken: token,
|
|
379
|
+
actionButtonPosition: ActionButtonPosition.Hidden,
|
|
380
|
+
mode: FormMode.Submit,
|
|
381
|
+
launcher: LauncherType.EventGenWeb,
|
|
382
|
+
language: code || undefined,
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**3. Manual listeners → `client.on(...)`.** Replace the `messageListener` +
|
|
387
|
+
`safeJsonParse(e.data)` + `if (type === ...)` chain with typed subscriptions; the SDK
|
|
388
|
+
parses, validates origin/source, and cleans up on `destroy()`.
|
|
389
|
+
|
|
390
|
+
**4. Hand-rolled submit promise → `submitAndWait()`.** A common pattern is a custom
|
|
391
|
+
promise that posts `submit_form`, listens for `submission_body`/`form_error_message`,
|
|
392
|
+
dedupes listeners, and times out. That whole helper collapses to:
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
const body = await client.submitAndWait(); // throws SubmissionError on form_error_message
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**5. `document.querySelector("#ulcForm").contentWindow.postMessage(...)` → client methods.**
|
|
399
|
+
Hold the `CaptelloWebview` instance in a ref/context and call `submit()` / `reset()` /
|
|
400
|
+
`prefillInfo()` instead of re-querying the DOM and stringifying messages by hand.
|
|
401
|
+
|
|
402
|
+
**Prefill notes for migrators.** `prefillInfo` items are matched by
|
|
403
|
+
`ll_field_unique_identifier`; `ll_field_id` is optional (number or string) and `value`
|
|
404
|
+
may be a string or boolean — so existing payloads with numeric ids and boolean values
|
|
405
|
+
type-check as-is. `prefillSubmission` accepts a loose `SubmissionPrefill`, so a
|
|
406
|
+
previously-received `SubmissionBody` (or a partial `{ email?, data?, ... }`) can be passed
|
|
407
|
+
back directly.
|
|
408
|
+
|
|
409
|
+
**Out of scope.** If your app is _itself_ embedded inside the webview shell and talks to
|
|
410
|
+
_its_ parent via `window.parent.postMessage` (e.g. relaying `email`/`clientId`, or custom
|
|
411
|
+
`NAVIGATE_BACK` / scanner messages), that is a separate channel from the capture-form
|
|
412
|
+
contract — keep that code; this SDK only models host ↔ capture-webview messaging.
|
|
413
|
+
|
|
414
|
+
## Development
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
pnpm install # from the repo root (pnpm workspace) or this package
|
|
418
|
+
pnpm build # bundle ESM + CJS + .d.ts into dist/
|
|
419
|
+
pnpm typecheck
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## License
|
|
423
|
+
|
|
424
|
+
MIT © Lead Liaison, LLC. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/embed-url.ts
|
|
2
|
+
var EmbedParam = /* @__PURE__ */ ((EmbedParam2) => {
|
|
3
|
+
EmbedParam2["FormId"] = "f";
|
|
4
|
+
EmbedParam2["SubmissionToken"] = "s";
|
|
5
|
+
EmbedParam2["StationId"] = "st";
|
|
6
|
+
EmbedParam2["Mode"] = "m";
|
|
7
|
+
EmbedParam2["EventWebAccessToken"] = "e";
|
|
8
|
+
EmbedParam2["ActivationId"] = "a";
|
|
9
|
+
EmbedParam2["Language"] = "l";
|
|
10
|
+
EmbedParam2["ActionButtonPosition"] = "b";
|
|
11
|
+
EmbedParam2["FormType"] = "form_type";
|
|
12
|
+
EmbedParam2["Launcher"] = "launcher";
|
|
13
|
+
EmbedParam2["SubmissionType"] = "t";
|
|
14
|
+
EmbedParam2["SubmitButtonBottomPadding"] = "sbbp";
|
|
15
|
+
EmbedParam2["Platform"] = "platform";
|
|
16
|
+
EmbedParam2["HideEmail"] = "he";
|
|
17
|
+
EmbedParam2["ConnexionsEmbedMode"] = "cem";
|
|
18
|
+
EmbedParam2["Emro"] = "emro";
|
|
19
|
+
EmbedParam2["UseIn"] = "useIn";
|
|
20
|
+
return EmbedParam2;
|
|
21
|
+
})(EmbedParam || {});
|
|
22
|
+
var FormMode = /* @__PURE__ */ ((FormMode2) => {
|
|
23
|
+
FormMode2["Preview"] = "preview";
|
|
24
|
+
FormMode2["Submit"] = "submit";
|
|
25
|
+
FormMode2["Edit"] = "edit";
|
|
26
|
+
FormMode2["View"] = "view";
|
|
27
|
+
return FormMode2;
|
|
28
|
+
})(FormMode || {});
|
|
29
|
+
var LauncherType = /* @__PURE__ */ ((LauncherType2) => {
|
|
30
|
+
LauncherType2["EventGenMobile"] = "event_gen_mobile";
|
|
31
|
+
LauncherType2["EventGenWeb"] = "event_gen_web";
|
|
32
|
+
LauncherType2["WebApp"] = "webapp";
|
|
33
|
+
LauncherType2["Mmp"] = "MMP";
|
|
34
|
+
return LauncherType2;
|
|
35
|
+
})(LauncherType || {});
|
|
36
|
+
var ActionButtonPosition = /* @__PURE__ */ ((ActionButtonPosition2) => {
|
|
37
|
+
ActionButtonPosition2["Fixed"] = "0";
|
|
38
|
+
ActionButtonPosition2["Bottom"] = "1";
|
|
39
|
+
ActionButtonPosition2["Hidden"] = "2";
|
|
40
|
+
return ActionButtonPosition2;
|
|
41
|
+
})(ActionButtonPosition || {});
|
|
42
|
+
var OPTION_TO_PARAM = [
|
|
43
|
+
["formId", "f" /* FormId */],
|
|
44
|
+
["submissionToken", "s" /* SubmissionToken */],
|
|
45
|
+
["stationId", "st" /* StationId */],
|
|
46
|
+
["mode", "m" /* Mode */],
|
|
47
|
+
["eventWebAccessToken", "e" /* EventWebAccessToken */],
|
|
48
|
+
["activationId", "a" /* ActivationId */],
|
|
49
|
+
["language", "l" /* Language */],
|
|
50
|
+
["actionButtonPosition", "b" /* ActionButtonPosition */],
|
|
51
|
+
["formType", "form_type" /* FormType */],
|
|
52
|
+
["launcher", "launcher" /* Launcher */],
|
|
53
|
+
["submissionType", "t" /* SubmissionType */],
|
|
54
|
+
["submitButtonBottomPadding", "sbbp" /* SubmitButtonBottomPadding */],
|
|
55
|
+
["useIn", "useIn" /* UseIn */],
|
|
56
|
+
["platform", "platform" /* Platform */]
|
|
57
|
+
];
|
|
58
|
+
var BOOLEAN_OPTION_TO_PARAM = [
|
|
59
|
+
["hideEmail", "he" /* HideEmail */],
|
|
60
|
+
["connexionsEmbedMode", "cem" /* ConnexionsEmbedMode */],
|
|
61
|
+
["emro", "emro" /* Emro */]
|
|
62
|
+
];
|
|
63
|
+
function buildEmbedUrl(baseUrl, options = {}) {
|
|
64
|
+
const url = new URL(baseUrl);
|
|
65
|
+
for (const [optionKey, paramKey] of OPTION_TO_PARAM) {
|
|
66
|
+
const value = options[optionKey];
|
|
67
|
+
if (value !== void 0 && value !== null && value !== "") {
|
|
68
|
+
url.searchParams.set(paramKey, String(value));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const [optionKey, paramKey] of BOOLEAN_OPTION_TO_PARAM) {
|
|
72
|
+
if (options[optionKey]) {
|
|
73
|
+
url.searchParams.set(paramKey, "1");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (options.extraParams) {
|
|
77
|
+
for (const [key, value] of Object.entries(options.extraParams)) {
|
|
78
|
+
if (value !== void 0 && value !== null) {
|
|
79
|
+
url.searchParams.set(key, String(value));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return url.toString();
|
|
84
|
+
}
|
|
85
|
+
function targetOriginFromUrl(embedUrl) {
|
|
86
|
+
return new URL(embedUrl).origin;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { ActionButtonPosition, EmbedParam, FormMode, LauncherType, buildEmbedUrl, targetOriginFromUrl };
|
|
90
|
+
//# sourceMappingURL=chunk-ETF52K7K.js.map
|
|
91
|
+
//# sourceMappingURL=chunk-ETF52K7K.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/embed-url.ts"],"names":["EmbedParam","FormMode","LauncherType","ActionButtonPosition"],"mappings":"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","file":"chunk-ETF52K7K.js","sourcesContent":["/**\n * Builder for the Captello capture webview embed URL.\n *\n * The webview reads its configuration from query-string params. The short keys\n * below are the contract the webview expects (see the webview's `PARAMS` enum);\n * this builder maps friendly option names onto those keys so hosts never have to\n * hard-code `\"f\"`, `\"m\"`, etc.\n */\n\n/** Query-param keys understood by the webview. */\nexport enum EmbedParam {\n FormId = \"f\",\n SubmissionToken = \"s\",\n StationId = \"st\",\n Mode = \"m\",\n EventWebAccessToken = \"e\",\n ActivationId = \"a\",\n Language = \"l\",\n ActionButtonPosition = \"b\",\n FormType = \"form_type\",\n Launcher = \"launcher\",\n SubmissionType = \"t\",\n SubmitButtonBottomPadding = \"sbbp\",\n Platform = \"platform\",\n HideEmail = \"he\",\n ConnexionsEmbedMode = \"cem\",\n /** Edit mode read-only: locks email-mapped and invitation-code elements. */\n Emro = \"emro\",\n /** Context for filtering form-fill actions (e.g. MMP outbound/inbound/notes). */\n UseIn = \"useIn\",\n}\n\n/** Form render mode (the webview's `FormMode`). */\nexport enum FormMode {\n Preview = \"preview\",\n Submit = \"submit\",\n Edit = \"edit\",\n View = \"view\",\n}\n\n/** Identifies the host embedding the webview (the webview's `LAUNCHER_TYPES`). */\nexport enum LauncherType {\n EventGenMobile = \"event_gen_mobile\",\n EventGenWeb = \"event_gen_web\",\n WebApp = \"webapp\",\n Mmp = \"MMP\",\n}\n\n/** `form_type` discriminator. */\nexport type FormType = \"template\" | \"device\";\n\n/** `t` (submission type) discriminator. */\nexport type SubmissionType = \"normal\" | \"drafted\";\n\n/**\n * Action-button position param (`b`), mirroring the webview's `CTABtnPosition`.\n * The wire values are numeric strings; use {@link ActionButtonPosition} for the\n * readable names.\n */\nexport enum ActionButtonPosition {\n Fixed = \"0\",\n Bottom = \"1\",\n Hidden = \"2\",\n}\n\n/** Context for filtering form-fill actions, sent as the `useIn` param. */\nexport type UseInContext = \"outbound\" | \"inbound\" | \"notes\";\n\n/**\n * Options for {@link buildEmbedUrl}. Every field is optional; only the ones you set\n * are written to the URL. `formId` is effectively required for a real embed but is\n * left optional so callers can build preview/partial URLs.\n */\nexport interface EmbedUrlOptions {\n formId?: string | number;\n submissionToken?: string;\n stationId?: string | number;\n mode?: FormMode;\n eventWebAccessToken?: string;\n activationId?: string | number;\n /** Two-letter language code, e.g. `\"en\"`, `\"de\"`. */\n language?: string;\n actionButtonPosition?: ActionButtonPosition;\n formType?: FormType;\n launcher?: LauncherType;\n submissionType?: SubmissionType;\n submitButtonBottomPadding?: string | number;\n /** Context for filtering form-fill actions (the `useIn` param). */\n useIn?: UseInContext;\n platform?: \"web\" | \"mobile\";\n hideEmail?: boolean;\n /** Connexions embed mode: suppress in-webview redirect/vCard download. */\n connexionsEmbedMode?: boolean;\n /** Edit mode read-only. */\n emro?: boolean;\n /**\n * Extra query params to append verbatim (e.g. prospect tracking params the\n * webview forwards on submit). Values are stringified; `undefined`/`null` skipped.\n */\n extraParams?: Record<string, string | number | boolean | undefined | null>;\n}\n\n// Maps each option onto its query key. Order here defines the order params are\n// written, which keeps generated URLs stable and diffable.\nconst OPTION_TO_PARAM: ReadonlyArray<[keyof EmbedUrlOptions, EmbedParam]> = [\n [\"formId\", EmbedParam.FormId],\n [\"submissionToken\", EmbedParam.SubmissionToken],\n [\"stationId\", EmbedParam.StationId],\n [\"mode\", EmbedParam.Mode],\n [\"eventWebAccessToken\", EmbedParam.EventWebAccessToken],\n [\"activationId\", EmbedParam.ActivationId],\n [\"language\", EmbedParam.Language],\n [\"actionButtonPosition\", EmbedParam.ActionButtonPosition],\n [\"formType\", EmbedParam.FormType],\n [\"launcher\", EmbedParam.Launcher],\n [\"submissionType\", EmbedParam.SubmissionType],\n [\"submitButtonBottomPadding\", EmbedParam.SubmitButtonBottomPadding],\n [\"useIn\", EmbedParam.UseIn],\n [\"platform\", EmbedParam.Platform],\n];\n\n// Boolean flags are encoded as \"1\" when true and omitted when false/unset, matching\n// how the webview reads them (`Boolean(queryParams[key])` / presence checks).\nconst BOOLEAN_OPTION_TO_PARAM: ReadonlyArray<[keyof EmbedUrlOptions, EmbedParam]> = [\n [\"hideEmail\", EmbedParam.HideEmail],\n [\"connexionsEmbedMode\", EmbedParam.ConnexionsEmbedMode],\n [\"emro\", EmbedParam.Emro],\n];\n\n/**\n * Builds an absolute embed URL from a base capture URL and typed options.\n *\n * Existing query params on `baseUrl` are preserved; options override params with\n * the same key. The base may be any capture origin/path, e.g.\n * `\"https://capture.captello.com/\"`.\n *\n * @example\n * buildEmbedUrl(\"https://capture.captello.com/\", {\n * formId: 1234,\n * mode: FormMode.Submit,\n * launcher: LauncherType.EventGenWeb,\n * language: \"en\",\n * });\n * // → \"https://capture.captello.com/?f=1234&m=submit&launcher=event_gen_web&l=en\"\n */\nexport function buildEmbedUrl(baseUrl: string, options: EmbedUrlOptions = {}): string {\n const url = new URL(baseUrl);\n\n for (const [optionKey, paramKey] of OPTION_TO_PARAM) {\n const value = options[optionKey];\n if (value !== undefined && value !== null && value !== \"\") {\n url.searchParams.set(paramKey, String(value));\n }\n }\n\n for (const [optionKey, paramKey] of BOOLEAN_OPTION_TO_PARAM) {\n if (options[optionKey]) {\n url.searchParams.set(paramKey, \"1\");\n }\n }\n\n if (options.extraParams) {\n for (const [key, value] of Object.entries(options.extraParams)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n\n return url.toString();\n}\n\n/**\n * Extracts the origin of an embed URL — convenient for passing as the client's\n * `targetOrigin` so messages are scoped to the webview's origin.\n *\n * @example targetOriginFromUrl(\"https://capture.captello.com/?f=1\") // \"https://capture.captello.com\"\n */\nexport function targetOriginFromUrl(embedUrl: string): string {\n return new URL(embedUrl).origin;\n}\n"]}
|