@captello/ulc-webview-sdk 1.2.0 → 1.3.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/CHANGELOG.md +27 -0
- package/README.md +61 -30
- package/dist/chunk-XI5MIDBA.js +60 -0
- package/dist/chunk-XI5MIDBA.js.map +1 -0
- package/dist/embedded-app.d.ts +93 -0
- package/dist/embedded-app.js +119 -0
- package/dist/embedded-app.js.map +1 -0
- package/dist/mobile-app-protocol-Bt-UDbu5.d.ts +176 -0
- package/dist/mobile-host.d.ts +85 -0
- package/dist/mobile-host.js +123 -0
- package/dist/mobile-host.js.map +1 -0
- package/dist/react.js.map +1 -1
- package/package.json +13 -6
- package/dist/app-host.d.ts +0 -228
- package/dist/app-host.js +0 -170
- package/dist/app-host.js.map +0 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { P as PrefillInfoItem } from './client-CAMlFA8s.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The message protocol between the **Captello mobile app** and an **application it
|
|
5
|
+
* embeds** (the meeting platform, Connexions, …).
|
|
6
|
+
*
|
|
7
|
+
* This is the second of the SDK's two channels, and the roles are the reverse of the
|
|
8
|
+
* capture-webview channel:
|
|
9
|
+
*
|
|
10
|
+
* | Channel | Host | Embedded | Client on each side |
|
|
11
|
+
* | ----------------------------------------- | ------------------- | ------------------- | ---------------------------------------------------- |
|
|
12
|
+
* | A host page embeds the capture webview | your page | capture webview | `CaptelloWebview` (host side) |
|
|
13
|
+
* | The mobile app embeds an application | Captello mobile app | your application | `EmbeddedAppClient` (app side), `MobileHostClient` (embedded side) |
|
|
14
|
+
*
|
|
15
|
+
* Wire format (match it exactly — it differs from the capture-webview channel):
|
|
16
|
+
* - Messages are posted as **plain objects**, not JSON strings.
|
|
17
|
+
* - `type` values are SCREAMING_CASE.
|
|
18
|
+
* - {@link MobileHostRequestType} (embedded application → mobile app) and
|
|
19
|
+
* {@link MobileHostResponseType} (mobile app → embedded application) are disjoint.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Message `type` values an embedded application sends to the Captello mobile app. */
|
|
23
|
+
declare enum MobileHostRequestType {
|
|
24
|
+
/** The application finished loading; the app hides its loading spinner. */
|
|
25
|
+
AppReady = "APP_READY",
|
|
26
|
+
/** A user-facing error; the app shows `message`. */
|
|
27
|
+
Error = "ERROR",
|
|
28
|
+
/** Close the application (the app dismisses the modal or pops the route). */
|
|
29
|
+
NavigateBack = "NAVIGATE_BACK",
|
|
30
|
+
/** Ask for a magic token; answered with {@link MobileHostResponseType.AuthToken}. */
|
|
31
|
+
RequestAuthToken = "REQUEST_AUTH_TOKEN",
|
|
32
|
+
/**
|
|
33
|
+
* Open the app's badge scanner. Answered with one
|
|
34
|
+
* {@link MobileHostResponseType.ScannerResult} per scanned person, then
|
|
35
|
+
* {@link MobileHostResponseType.ScannerClosed}.
|
|
36
|
+
*/
|
|
37
|
+
OpenScanner = "OPEN_ULC_FORM_SCANNER",
|
|
38
|
+
/** Open `url` in the system browser. */
|
|
39
|
+
OpenUrl = "OPEN_URL",
|
|
40
|
+
/** Copy `text` to the clipboard; acknowledged with `COPIED_SUCCESS` / `COPIED_ERROR`. */
|
|
41
|
+
CopyText = "COPY_TEXT",
|
|
42
|
+
/** Open the native share sheet. */
|
|
43
|
+
ShareUrl = "SHARE_URL",
|
|
44
|
+
/** Save a vCard to the contacts; acknowledged with `SAVE_VCARD_SUCCESS` / `SAVE_VCARD_ERROR`. */
|
|
45
|
+
SaveVCard = "SAVE_VCARD",
|
|
46
|
+
/** Add a wallet pass; acknowledged with `ADD_TO_WALLET_SUCCESS` / `ADD_TO_WALLET_ERROR`. */
|
|
47
|
+
AddToWallet = "ADD_TO_WALLET",
|
|
48
|
+
/** Ask the app to synchronize its data. */
|
|
49
|
+
SyncApp = "SYNC_APP"
|
|
50
|
+
}
|
|
51
|
+
interface AppReadyRequest {
|
|
52
|
+
type: MobileHostRequestType.AppReady;
|
|
53
|
+
}
|
|
54
|
+
interface ErrorRequest {
|
|
55
|
+
type: MobileHostRequestType.Error;
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
interface NavigateBackRequest {
|
|
59
|
+
type: MobileHostRequestType.NavigateBack;
|
|
60
|
+
}
|
|
61
|
+
interface RequestAuthTokenRequest {
|
|
62
|
+
type: MobileHostRequestType.RequestAuthToken;
|
|
63
|
+
}
|
|
64
|
+
interface OpenScannerRequest {
|
|
65
|
+
type: MobileHostRequestType.OpenScanner;
|
|
66
|
+
}
|
|
67
|
+
interface OpenUrlRequest {
|
|
68
|
+
type: MobileHostRequestType.OpenUrl;
|
|
69
|
+
url: string;
|
|
70
|
+
}
|
|
71
|
+
interface CopyTextRequest {
|
|
72
|
+
type: MobileHostRequestType.CopyText;
|
|
73
|
+
text: string;
|
|
74
|
+
}
|
|
75
|
+
interface ShareUrlRequest {
|
|
76
|
+
type: MobileHostRequestType.ShareUrl;
|
|
77
|
+
title: string;
|
|
78
|
+
text: string;
|
|
79
|
+
}
|
|
80
|
+
interface SaveVCardRequest {
|
|
81
|
+
type: MobileHostRequestType.SaveVCard;
|
|
82
|
+
/** vCard text. */
|
|
83
|
+
text: string;
|
|
84
|
+
}
|
|
85
|
+
interface AddToWalletRequest {
|
|
86
|
+
type: MobileHostRequestType.AddToWallet;
|
|
87
|
+
/** Base64 pass data, with or without a `data:` prefix. */
|
|
88
|
+
text: string;
|
|
89
|
+
}
|
|
90
|
+
interface SyncAppRequest {
|
|
91
|
+
type: MobileHostRequestType.SyncApp;
|
|
92
|
+
}
|
|
93
|
+
/** Discriminated union of every message an embedded application can send to the mobile app. */
|
|
94
|
+
type MobileHostRequest = AppReadyRequest | ErrorRequest | NavigateBackRequest | RequestAuthTokenRequest | OpenScannerRequest | OpenUrlRequest | CopyTextRequest | ShareUrlRequest | SaveVCardRequest | AddToWalletRequest | SyncAppRequest;
|
|
95
|
+
/** Maps each request `type` to its full message shape (used by {@link EmbeddedAppClient.onRequest}). */
|
|
96
|
+
type MobileHostRequestMap = {
|
|
97
|
+
[M in MobileHostRequest as M["type"]]: M;
|
|
98
|
+
};
|
|
99
|
+
/** Message `type` values the Captello mobile app sends to an embedded application. */
|
|
100
|
+
declare enum MobileHostResponseType {
|
|
101
|
+
/** Answer to {@link MobileHostRequestType.RequestAuthToken}. */
|
|
102
|
+
AuthToken = "AUTH_TOKEN",
|
|
103
|
+
/** One scanned person (or a scanner failure) after {@link MobileHostRequestType.OpenScanner}. */
|
|
104
|
+
ScannerResult = "ULC_FORM_SCANNER_RESULT",
|
|
105
|
+
/** The scanner session ended: every result was posted, or the user cancelled. */
|
|
106
|
+
ScannerClosed = "ULC_FORM_SCANNER_CLOSED",
|
|
107
|
+
CopiedSuccess = "COPIED_SUCCESS",
|
|
108
|
+
CopiedError = "COPIED_ERROR",
|
|
109
|
+
SaveVCardSuccess = "SAVE_VCARD_SUCCESS",
|
|
110
|
+
SaveVCardError = "SAVE_VCARD_ERROR",
|
|
111
|
+
AddToWalletSuccess = "ADD_TO_WALLET_SUCCESS",
|
|
112
|
+
AddToWalletError = "ADD_TO_WALLET_ERROR"
|
|
113
|
+
}
|
|
114
|
+
/** Mobile app → embedded application: the magic token requested with {@link MobileHostRequestType.RequestAuthToken}. */
|
|
115
|
+
interface AuthTokenMessage {
|
|
116
|
+
type: MobileHostResponseType.AuthToken;
|
|
117
|
+
token: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Mobile app → embedded application: one scanned person, or a scanner failure.
|
|
121
|
+
*
|
|
122
|
+
* The app posts one of these per person the scanner captured — several for a group
|
|
123
|
+
* scan — then a {@link ScannerClosedMessage}. `result` holds the looked-up attendee
|
|
124
|
+
* fields in the same shape {@link PrefillInfoItem} uses, so they can be fed straight to
|
|
125
|
+
* a capture form's `prefill({ info })`. It is empty (not absent) when the badge had no
|
|
126
|
+
* lookup data; the badge can still be linked via `badgeId`.
|
|
127
|
+
*
|
|
128
|
+
* On failure `result` is absent and `message` carries the error text.
|
|
129
|
+
*/
|
|
130
|
+
interface ScannerResultMessage {
|
|
131
|
+
type: MobileHostResponseType.ScannerResult;
|
|
132
|
+
/**
|
|
133
|
+
* Badge ID the person was scanned from. Empty for business cards and manual search.
|
|
134
|
+
* Absent altogether on app builds that predate multi-person sessions — those post a
|
|
135
|
+
* single result and never a {@link ScannerClosedMessage}.
|
|
136
|
+
*/
|
|
137
|
+
badgeId?: string;
|
|
138
|
+
/** Looked-up attendee fields (success). */
|
|
139
|
+
result?: PrefillInfoItem[];
|
|
140
|
+
/** Error text (failure) — `result` is absent. */
|
|
141
|
+
message?: string;
|
|
142
|
+
}
|
|
143
|
+
/** Mobile app → embedded application: the scanner session ended. Follows the last {@link ScannerResultMessage}. */
|
|
144
|
+
interface ScannerClosedMessage {
|
|
145
|
+
type: MobileHostResponseType.ScannerClosed;
|
|
146
|
+
}
|
|
147
|
+
interface AckMessage<T extends MobileHostResponseType> {
|
|
148
|
+
type: T;
|
|
149
|
+
}
|
|
150
|
+
/** Discriminated union of every message the mobile app can send to an embedded application. */
|
|
151
|
+
type MobileHostResponse = AuthTokenMessage | ScannerResultMessage | ScannerClosedMessage | AckMessage<MobileHostResponseType.CopiedSuccess> | AckMessage<MobileHostResponseType.CopiedError> | AckMessage<MobileHostResponseType.SaveVCardSuccess> | AckMessage<MobileHostResponseType.SaveVCardError> | AckMessage<MobileHostResponseType.AddToWalletSuccess> | AckMessage<MobileHostResponseType.AddToWalletError>;
|
|
152
|
+
/** Maps each response `type` to its full message shape (used by {@link MobileHostClient.on}). */
|
|
153
|
+
type MobileHostResponseMap = {
|
|
154
|
+
[M in MobileHostResponse as M["type"]]: M;
|
|
155
|
+
};
|
|
156
|
+
/** One person captured by the mobile app's scanner. */
|
|
157
|
+
interface ScannedPerson {
|
|
158
|
+
/** Badge ID the person was scanned from; empty for business cards and manual search. */
|
|
159
|
+
badgeId: string;
|
|
160
|
+
/** Looked-up attendee fields; empty when the badge had no lookup data. */
|
|
161
|
+
fields: PrefillInfoItem[];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Parses a raw `MessageEvent.data` value into a typed {@link MobileHostRequest}, or
|
|
165
|
+
* returns `null` if it is not a recognized request. Plain objects are the wire format;
|
|
166
|
+
* a JSON string is accepted too for robustness.
|
|
167
|
+
*/
|
|
168
|
+
declare function parseMobileHostRequest(data: unknown): MobileHostRequest | null;
|
|
169
|
+
/**
|
|
170
|
+
* Parses a raw `MessageEvent.data` value into a typed {@link MobileHostResponse}, or
|
|
171
|
+
* returns `null` if it is not a recognized response. Plain objects are the wire format;
|
|
172
|
+
* a JSON string is accepted too for robustness.
|
|
173
|
+
*/
|
|
174
|
+
declare function parseMobileHostResponse(data: unknown): MobileHostResponse | null;
|
|
175
|
+
|
|
176
|
+
export { type AuthTokenMessage as A, type MobileHostRequest as M, type ScannedPerson as S, MobileHostRequestType as a, type MobileHostRequestMap as b, type MobileHostResponse as c, type MobileHostResponseMap as d, MobileHostResponseType as e, type ScannerClosedMessage as f, type ScannerResultMessage as g, parseMobileHostResponse as h, parseMobileHostRequest as p };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { U as Unsubscribe } from './client-CAMlFA8s.js';
|
|
2
|
+
import { M as MobileHostRequest, e as MobileHostResponseType, d as MobileHostResponseMap, S as ScannedPerson } from './mobile-app-protocol-Bt-UDbu5.js';
|
|
3
|
+
export { A as AuthTokenMessage, b as MobileHostRequestMap, a as MobileHostRequestType, c as MobileHostResponse, f as ScannerClosedMessage, g as ScannerResultMessage, p as parseMobileHostRequest, h as parseMobileHostResponse } from './mobile-app-protocol-Bt-UDbu5.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `@captello/ulc-webview-sdk/mobile-host` — for an **application embedded inside the
|
|
7
|
+
* Captello mobile app** (the meeting platform, Connexions, …).
|
|
8
|
+
*
|
|
9
|
+
* The mobile app hosts the application in an iframe and offers it native services over
|
|
10
|
+
* `postMessage`: an auth token, the badge scanner, back navigation, sharing.
|
|
11
|
+
* {@link MobileHostClient} is the embedded application's client for that channel; the
|
|
12
|
+
* mobile app's own side is `EmbeddedAppClient` (`@captello/ulc-webview-sdk/embedded-app`).
|
|
13
|
+
*
|
|
14
|
+
* Not to be confused with `CaptelloWebview`, which is for a page that embeds the
|
|
15
|
+
* capture webview — there your page is the host. See {@link MobileHostRequestType} for
|
|
16
|
+
* the wire format, which also differs (plain objects, SCREAMING_CASE).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* import { MobileHostClient } from "@captello/ulc-webview-sdk/mobile-host";
|
|
20
|
+
*
|
|
21
|
+
* const mobileHost = new MobileHostClient();
|
|
22
|
+
* const token = await mobileHost.requestAuthToken();
|
|
23
|
+
* mobileHost.notifyReady();
|
|
24
|
+
* const [person] = await mobileHost.openScanner();
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Rejection reason from {@link MobileHostClient.openScanner} when the app reports a failure. */
|
|
28
|
+
declare class ScannerError extends Error {
|
|
29
|
+
constructor(message: string);
|
|
30
|
+
}
|
|
31
|
+
/** Listener for a specific mobile-app response type. */
|
|
32
|
+
type MobileHostResponseListener<T extends MobileHostResponseType> = (message: MobileHostResponseMap[T]) => void;
|
|
33
|
+
interface MobileHostClientOptions {
|
|
34
|
+
/** Window the mobile app is listening on. Defaults to `window.parent`. */
|
|
35
|
+
mobileHostWindow?: Window;
|
|
36
|
+
/** Window to receive the mobile app's responses on. Defaults to `window`. */
|
|
37
|
+
embeddedWindow?: Window;
|
|
38
|
+
/**
|
|
39
|
+
* `targetOrigin` for outgoing `postMessage` calls. Defaults to `"*"`: the mobile app's
|
|
40
|
+
* webview origin differs per platform (`capacitor://localhost`, `http://localhost`),
|
|
41
|
+
* and an application only uses this channel when it is running inside the app.
|
|
42
|
+
*/
|
|
43
|
+
targetOrigin?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Embedded-application-side client for the Captello mobile app that hosts it.
|
|
47
|
+
*
|
|
48
|
+
* Construct one per page and keep it for the page's lifetime. It attaches a single
|
|
49
|
+
* `message` listener on construction; call {@link destroy} to remove it.
|
|
50
|
+
*/
|
|
51
|
+
declare class MobileHostClient {
|
|
52
|
+
private readonly mobileHostWindow;
|
|
53
|
+
private readonly embeddedWindow;
|
|
54
|
+
private readonly targetOrigin;
|
|
55
|
+
private readonly listeners;
|
|
56
|
+
private readonly boundHandler;
|
|
57
|
+
private destroyed;
|
|
58
|
+
constructor(options?: MobileHostClientOptions);
|
|
59
|
+
/** Post a request to the mobile app. Prefer the typed methods; this is the escape hatch. */
|
|
60
|
+
send(request: MobileHostRequest): void;
|
|
61
|
+
/** Subscribe to a response type. Returns a handle with `unsubscribe()` (also callable). */
|
|
62
|
+
on<T extends MobileHostResponseType>(type: T, listener: MobileHostResponseListener<T>): Unsubscribe;
|
|
63
|
+
/** Tell the mobile app the application is ready; it hides its loading spinner. */
|
|
64
|
+
notifyReady(): void;
|
|
65
|
+
/** Show a user-facing error in the mobile app. */
|
|
66
|
+
notifyError(message: string): void;
|
|
67
|
+
/** Ask the mobile app to close the application. */
|
|
68
|
+
navigateBack(): void;
|
|
69
|
+
/** Resolves with the magic token the mobile app mints for the current user. */
|
|
70
|
+
requestAuthToken(): Promise<string>;
|
|
71
|
+
/**
|
|
72
|
+
* Opens the mobile app's badge scanner and resolves once the session ends with every
|
|
73
|
+
* person it captured, in scan order — several for a group scan, none if the user
|
|
74
|
+
* cancelled. Rejects with a {@link ScannerError} if the app reports a failure.
|
|
75
|
+
*
|
|
76
|
+
* A badge whose lookup returned nothing is still included, with its `badgeId` and
|
|
77
|
+
* empty `fields`, so it can be linked to the attendee.
|
|
78
|
+
*/
|
|
79
|
+
openScanner(): Promise<ScannedPerson[]>;
|
|
80
|
+
/** Remove the `message` listener and drop every subscription. Safe to call more than once. */
|
|
81
|
+
destroy(): void;
|
|
82
|
+
private handleMessage;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { MobileHostClient, type MobileHostClientOptions, MobileHostRequest, type MobileHostResponseListener, MobileHostResponseMap, MobileHostResponseType, ScannedPerson, ScannerError };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { makeSubscription, parseMobileHostResponse } from './chunk-XI5MIDBA.js';
|
|
2
|
+
export { MobileHostRequestType, MobileHostResponseType, parseMobileHostRequest, parseMobileHostResponse } from './chunk-XI5MIDBA.js';
|
|
3
|
+
|
|
4
|
+
// src/mobile-host.ts
|
|
5
|
+
var ScannerError = class extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ScannerError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var MobileHostClient = class {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
14
|
+
this.boundHandler = (event) => this.handleMessage(event);
|
|
15
|
+
this.destroyed = false;
|
|
16
|
+
const embeddedWindow = options.embeddedWindow ?? (typeof window !== "undefined" ? window : void 0);
|
|
17
|
+
if (!embeddedWindow) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"MobileHostClient: no window available. Pass `embeddedWindow` when constructing outside a browser."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
this.embeddedWindow = embeddedWindow;
|
|
23
|
+
this.mobileHostWindow = options.mobileHostWindow ?? embeddedWindow.parent;
|
|
24
|
+
this.targetOrigin = options.targetOrigin ?? "*";
|
|
25
|
+
this.embeddedWindow.addEventListener("message", this.boundHandler);
|
|
26
|
+
}
|
|
27
|
+
/** Post a request to the mobile app. Prefer the typed methods; this is the escape hatch. */
|
|
28
|
+
send(request) {
|
|
29
|
+
if (this.destroyed) return;
|
|
30
|
+
this.mobileHostWindow.postMessage(request, this.targetOrigin);
|
|
31
|
+
}
|
|
32
|
+
/** Subscribe to a response type. Returns a handle with `unsubscribe()` (also callable). */
|
|
33
|
+
on(type, listener) {
|
|
34
|
+
let set = this.listeners.get(type);
|
|
35
|
+
if (!set) {
|
|
36
|
+
set = /* @__PURE__ */ new Set();
|
|
37
|
+
this.listeners.set(type, set);
|
|
38
|
+
}
|
|
39
|
+
set.add(listener);
|
|
40
|
+
return makeSubscription(() => {
|
|
41
|
+
set?.delete(listener);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Tell the mobile app the application is ready; it hides its loading spinner. */
|
|
45
|
+
notifyReady() {
|
|
46
|
+
this.send({ type: "APP_READY" /* AppReady */ });
|
|
47
|
+
}
|
|
48
|
+
/** Show a user-facing error in the mobile app. */
|
|
49
|
+
notifyError(message) {
|
|
50
|
+
this.send({ type: "ERROR" /* Error */, message });
|
|
51
|
+
}
|
|
52
|
+
/** Ask the mobile app to close the application. */
|
|
53
|
+
navigateBack() {
|
|
54
|
+
this.send({ type: "NAVIGATE_BACK" /* NavigateBack */ });
|
|
55
|
+
}
|
|
56
|
+
/** Resolves with the magic token the mobile app mints for the current user. */
|
|
57
|
+
requestAuthToken() {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const off = this.on("AUTH_TOKEN" /* AuthToken */, (message) => {
|
|
60
|
+
off();
|
|
61
|
+
resolve(message.token);
|
|
62
|
+
});
|
|
63
|
+
this.send({ type: "REQUEST_AUTH_TOKEN" /* RequestAuthToken */ });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Opens the mobile app's badge scanner and resolves once the session ends with every
|
|
68
|
+
* person it captured, in scan order — several for a group scan, none if the user
|
|
69
|
+
* cancelled. Rejects with a {@link ScannerError} if the app reports a failure.
|
|
70
|
+
*
|
|
71
|
+
* A badge whose lookup returned nothing is still included, with its `badgeId` and
|
|
72
|
+
* empty `fields`, so it can be linked to the attendee.
|
|
73
|
+
*/
|
|
74
|
+
openScanner() {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const people = [];
|
|
77
|
+
const done = () => {
|
|
78
|
+
offResult();
|
|
79
|
+
offClosed();
|
|
80
|
+
};
|
|
81
|
+
const offResult = this.on("ULC_FORM_SCANNER_RESULT" /* ScannerResult */, (message) => {
|
|
82
|
+
if (!Array.isArray(message.result)) {
|
|
83
|
+
done();
|
|
84
|
+
reject(new ScannerError(message.message || "Scan failed."));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
people.push({
|
|
88
|
+
badgeId: typeof message.badgeId === "string" ? message.badgeId : "",
|
|
89
|
+
fields: message.result
|
|
90
|
+
});
|
|
91
|
+
if (!("badgeId" in message)) {
|
|
92
|
+
done();
|
|
93
|
+
resolve(people);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const offClosed = this.on("ULC_FORM_SCANNER_CLOSED" /* ScannerClosed */, () => {
|
|
97
|
+
done();
|
|
98
|
+
resolve(people);
|
|
99
|
+
});
|
|
100
|
+
this.send({ type: "OPEN_ULC_FORM_SCANNER" /* OpenScanner */ });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Remove the `message` listener and drop every subscription. Safe to call more than once. */
|
|
104
|
+
destroy() {
|
|
105
|
+
if (this.destroyed) return;
|
|
106
|
+
this.destroyed = true;
|
|
107
|
+
this.embeddedWindow.removeEventListener("message", this.boundHandler);
|
|
108
|
+
this.listeners.clear();
|
|
109
|
+
}
|
|
110
|
+
handleMessage(event) {
|
|
111
|
+
const message = parseMobileHostResponse(event.data);
|
|
112
|
+
if (!message) return;
|
|
113
|
+
const set = this.listeners.get(message.type);
|
|
114
|
+
if (!set) return;
|
|
115
|
+
for (const listener of Array.from(set)) {
|
|
116
|
+
listener(message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export { MobileHostClient, ScannerError };
|
|
122
|
+
//# sourceMappingURL=mobile-host.js.map
|
|
123
|
+
//# sourceMappingURL=mobile-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mobile-host.ts"],"names":[],"mappings":"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","file":"mobile-host.js","sourcesContent":["/**\n * `@captello/ulc-webview-sdk/mobile-host` — for an **application embedded inside the\n * Captello mobile app** (the meeting platform, Connexions, …).\n *\n * The mobile app hosts the application in an iframe and offers it native services over\n * `postMessage`: an auth token, the badge scanner, back navigation, sharing.\n * {@link MobileHostClient} is the embedded application's client for that channel; the\n * mobile app's own side is `EmbeddedAppClient` (`@captello/ulc-webview-sdk/embedded-app`).\n *\n * Not to be confused with `CaptelloWebview`, which is for a page that embeds the\n * capture webview — there your page is the host. See {@link MobileHostRequestType} for\n * the wire format, which also differs (plain objects, SCREAMING_CASE).\n *\n * @example\n * import { MobileHostClient } from \"@captello/ulc-webview-sdk/mobile-host\";\n *\n * const mobileHost = new MobileHostClient();\n * const token = await mobileHost.requestAuthToken();\n * mobileHost.notifyReady();\n * const [person] = await mobileHost.openScanner();\n */\n\nimport type { Unsubscribe } from \"./client\";\nimport {\n makeSubscription,\n MobileHostRequestType,\n MobileHostResponseType,\n parseMobileHostResponse,\n} from \"./mobile-app-protocol\";\nimport type { MobileHostRequest, MobileHostResponseMap, ScannedPerson } from \"./mobile-app-protocol\";\n\nexport {\n MobileHostRequestType,\n MobileHostResponseType,\n parseMobileHostRequest,\n parseMobileHostResponse,\n} from \"./mobile-app-protocol\";\nexport type {\n MobileHostRequest,\n MobileHostRequestMap,\n MobileHostResponse,\n MobileHostResponseMap,\n AuthTokenMessage,\n ScannerResultMessage,\n ScannerClosedMessage,\n ScannedPerson,\n} from \"./mobile-app-protocol\";\n\n/** Rejection reason from {@link MobileHostClient.openScanner} when the app reports a failure. */\nexport class ScannerError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ScannerError\";\n }\n}\n\n/** Listener for a specific mobile-app response type. */\nexport type MobileHostResponseListener<T extends MobileHostResponseType> = (message: MobileHostResponseMap[T]) => void;\n\nexport interface MobileHostClientOptions {\n /** Window the mobile app is listening on. Defaults to `window.parent`. */\n mobileHostWindow?: Window;\n /** Window to receive the mobile app's responses on. Defaults to `window`. */\n embeddedWindow?: Window;\n /**\n * `targetOrigin` for outgoing `postMessage` calls. Defaults to `\"*\"`: the mobile app's\n * webview origin differs per platform (`capacitor://localhost`, `http://localhost`),\n * and an application only uses this channel when it is running inside the app.\n */\n targetOrigin?: string;\n}\n\n/**\n * Embedded-application-side client for the Captello mobile app that hosts it.\n *\n * Construct one per page and keep it for the page's lifetime. It attaches a single\n * `message` listener on construction; call {@link destroy} to remove it.\n */\nexport class MobileHostClient {\n private readonly mobileHostWindow: Window;\n private readonly embeddedWindow: Window;\n private readonly targetOrigin: string;\n private readonly listeners = new Map<\n MobileHostResponseType,\n Set<MobileHostResponseListener<MobileHostResponseType>>\n >();\n private readonly boundHandler = (event: MessageEvent) => this.handleMessage(event);\n private destroyed = false;\n\n constructor(options: MobileHostClientOptions = {}) {\n const embeddedWindow = options.embeddedWindow ?? (typeof window !== \"undefined\" ? window : undefined);\n if (!embeddedWindow) {\n throw new Error(\n \"MobileHostClient: no window available. Pass `embeddedWindow` when constructing outside a browser.\",\n );\n }\n this.embeddedWindow = embeddedWindow;\n this.mobileHostWindow = options.mobileHostWindow ?? embeddedWindow.parent;\n this.targetOrigin = options.targetOrigin ?? \"*\";\n this.embeddedWindow.addEventListener(\"message\", this.boundHandler);\n }\n\n /** Post a request to the mobile app. Prefer the typed methods; this is the escape hatch. */\n send(request: MobileHostRequest): void {\n if (this.destroyed) return;\n this.mobileHostWindow.postMessage(request, this.targetOrigin);\n }\n\n /** Subscribe to a response type. Returns a handle with `unsubscribe()` (also callable). */\n on<T extends MobileHostResponseType>(type: T, listener: MobileHostResponseListener<T>): Unsubscribe {\n let set = this.listeners.get(type);\n if (!set) {\n set = new Set();\n this.listeners.set(type, set);\n }\n set.add(listener as MobileHostResponseListener<MobileHostResponseType>);\n return makeSubscription(() => {\n set?.delete(listener as MobileHostResponseListener<MobileHostResponseType>);\n });\n }\n\n /** Tell the mobile app the application is ready; it hides its loading spinner. */\n notifyReady(): void {\n this.send({ type: MobileHostRequestType.AppReady });\n }\n\n /** Show a user-facing error in the mobile app. */\n notifyError(message: string): void {\n this.send({ type: MobileHostRequestType.Error, message });\n }\n\n /** Ask the mobile app to close the application. */\n navigateBack(): void {\n this.send({ type: MobileHostRequestType.NavigateBack });\n }\n\n /** Resolves with the magic token the mobile app mints for the current user. */\n requestAuthToken(): Promise<string> {\n return new Promise((resolve) => {\n const off = this.on(MobileHostResponseType.AuthToken, (message) => {\n off();\n resolve(message.token);\n });\n this.send({ type: MobileHostRequestType.RequestAuthToken });\n });\n }\n\n /**\n * Opens the mobile app's badge scanner and resolves once the session ends with every\n * person it captured, in scan order — several for a group scan, none if the user\n * cancelled. Rejects with a {@link ScannerError} if the app reports a failure.\n *\n * A badge whose lookup returned nothing is still included, with its `badgeId` and\n * empty `fields`, so it can be linked to the attendee.\n */\n openScanner(): Promise<ScannedPerson[]> {\n return new Promise((resolve, reject) => {\n const people: ScannedPerson[] = [];\n const done = () => {\n offResult();\n offClosed();\n };\n const offResult = this.on(MobileHostResponseType.ScannerResult, (message) => {\n if (!Array.isArray(message.result)) {\n done();\n reject(new ScannerError(message.message || \"Scan failed.\"));\n return;\n }\n people.push({\n badgeId: typeof message.badgeId === \"string\" ? message.badgeId : \"\",\n fields: message.result,\n });\n // App builds that predate multi-person sessions post one result, without a\n // badgeId, and never a ScannerClosed — settle on it.\n if (!(\"badgeId\" in message)) {\n done();\n resolve(people);\n }\n });\n const offClosed = this.on(MobileHostResponseType.ScannerClosed, () => {\n done();\n resolve(people);\n });\n this.send({ type: MobileHostRequestType.OpenScanner });\n });\n }\n\n /** Remove the `message` listener and drop every subscription. Safe to call more than once. */\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.embeddedWindow.removeEventListener(\"message\", this.boundHandler);\n this.listeners.clear();\n }\n\n private handleMessage(event: MessageEvent): void {\n const message = parseMobileHostResponse(event.data);\n if (!message) return;\n const set = this.listeners.get(message.type);\n if (!set) return;\n for (const listener of Array.from(set)) {\n listener(message);\n }\n }\n}\n"]}
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react.tsx"],"names":[],"mappings":"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","file":"react.js","sourcesContent":["/**\n * React adapter for the Captello webview SDK — `@captello/ulc-webview-sdk/react`.\n *\n * Two entry points, same engine:\n * - {@link CaptelloForm} — a turnkey `<iframe>` component. Drop it in with an `embedUrl`\n * and message callbacks; it renders the frame, shows your `loading` / `error` overlays,\n * and exposes the senders via an imperative `ref`. This is the shortest path.\n * - {@link useCaptelloWebview} — the underlying hook, for when you want to own the markup.\n *\n * {@link useCaptelloWebview} owns a {@link CaptelloWebview} for the lifetime of an\n * iframe: it creates the client once the iframe mounts, wires the outbound messages\n * you care about to typed callbacks, tracks readiness, and destroys the client on\n * unmount. You get back `iframeProps` to spread onto your `<iframe>` (or a bare `ref`),\n * an `isReady` flag, and stable senders (`submit`, `reset`, `prefill`, …).\n *\n * Sends made before the form loads are queued by the client and flushed on\n * `form_load_complete`, so you can call `prefill(...)` as soon as you have data —\n * no need to gate on readiness yourself. To seed a form declaratively, pass\n * `defaultFormValues` instead and skip the `prefill(...)` wiring entirely.\n *\n * Callbacks are held in a ref and always called fresh, so you do NOT need to memoize\n * them — passing inline arrow functions will not re-subscribe or re-create the client.\n *\n * `react` is an optional peer dependency; importing this entry point requires React 18+.\n *\n * @example\n * function UlcForm({ token, onSubmitted }: { token: string; onSubmitted: (b: SubmissionBody) => void }) {\n * const { iframeProps, isReady, submit } = useCaptelloWebview({\n * embedUrl: {\n * baseUrl: \"https://capture.captello.com\",\n * eventWebAccessToken: token,\n * mode: FormMode.Submit,\n * launcher: LauncherType.EventGenWeb,\n * },\n * onSubmissionBody: onSubmitted,\n * });\n * return (\n * <>\n * {!isReady && <Spinner />}\n * <iframe {...iframeProps} title=\"UlcForm\" allow=\"camera; microphone\" />\n * <button onClick={submit}>Submit</button>\n * </>\n * );\n * }\n */\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type IframeHTMLAttributes,\n type ReactElement,\n type ReactNode,\n type Ref,\n type RefCallback,\n} from \"react\";\n\nimport { CaptelloWebview } from \"./client\";\nimport type { CaptelloWebviewOptions, TranscribeScannerRequestHandler, Unsubscribe } from \"./client\";\nimport { buildEmbedUrl } from \"./embed-url\";\nimport type { EmbedUrlOptions } from \"./embed-url\";\nimport { OutboundMessageType } from \"./messages\";\nimport type {\n OutboundMessage,\n PrefillInfoItem,\n SubmissionBody,\n SubmissionPrefill,\n ValidationTarget,\n} from \"./messages\";\n\n/**\n * Per-message-type callback props accepted by {@link useCaptelloWebview}.\n *\n * Each callback receives the message's **payload**, not the message envelope — the\n * callback name already carries the `type`, so there is nothing to discriminate on.\n * Messages that carry no payload take no argument.\n *\n * {@link CaptelloWebviewCallbacks.onAnyMessage} is the exception: it fires for every\n * type, so it gets the whole message including the `type` discriminator.\n */\nexport interface CaptelloWebviewCallbacks {\n /** The form finished loading and rendering. Safe to interact with it after this. */\n onFormLoadComplete?: () => void;\n /** A user-facing error occurred. Receives the translated, display-ready text. */\n onFormErrorMessage?: (message: string) => void;\n /** Receives the full submission body, for the host to persist / forward. */\n onSubmissionBody?: (body: SubmissionBody) => void;\n /** The form was submitted successfully. Receives whether it created or updated. */\n onFormSubmitSuccess?: (action: \"create\" | \"update\") => void;\n /** Connexions: the host should perform the profile redirect (embed mode). */\n onConnexionsProfileRedirect?: () => void;\n /** Connexions: the host should trigger the vCard download (embed mode). */\n onConnexionsDownloadVcard?: () => void;\n /** Catch-all: the full message, including `type`. Called after the specific handler above. */\n onAnyMessage?: (message: OutboundMessage) => void;\n}\n\n/** Embed-URL config: a base URL plus {@link EmbedUrlOptions}. */\nexport interface EmbedUrlConfig extends EmbedUrlOptions {\n /**\n * The capture **origin**, e.g. `\"https://capture.captello.com\"`. The SDK appends the\n * capture path for you, so the origin, a trailing slash, or the full\n * `…/capture/submission` URL all work — see {@link buildEmbedUrl}.\n */\n baseUrl: string;\n}\n\n/**\n * Values to seed a form with on load — see {@link UseCaptelloWebviewOptions.defaultFormValues}.\n *\n * `submission` is typed as {@link SubmissionPrefill} (every field optional) so a partial\n * object assembled from your own data is valid, as is a `submission.data` array fetched\n * from the submissions API or a whole {@link SubmissionBody} echoed back from\n * `onSubmissionBody`.\n */\nexport interface DefaultFormValues {\n /**\n * Submitted values under `data`, as either the submissions API's array (see\n * `SubmissionPrefillDataItem`) or a flat `DraftSubmissionData` record.\n */\n submission?: SubmissionPrefill;\n /** Field values matched by `ll_field_unique_identifier` (e.g. `\"Email\"`). */\n info?: PrefillInfoItem[];\n}\n\n/**\n * Options for {@link useCaptelloWebview}: the embed config, message callbacks, and the\n * usual client options.\n *\n * `embedUrl` is required — the hook builds the URL from it, derives `targetOrigin`, and\n * returns it as `iframeProps.src`. Any `targetOrigin` you pass is ignored; drop to\n * {@link CaptelloWebview} directly if you need to own both the URL and the origin.\n */\nexport interface UseCaptelloWebviewOptions extends Omit<CaptelloWebviewOptions, \"targetOrigin\">, CaptelloWebviewCallbacks {\n /** Build the iframe URL and derive `targetOrigin` from it. Sets `iframeProps.src`. */\n embedUrl: EmbedUrlConfig;\n /**\n * Values to populate the form with as soon as it is ready. Saves you from wiring a\n * `ref` and calling `prefill(...)` from an effect just to seed the form.\n *\n * Sent as the *first* outbound message, so a later explicit `prefill(...)` wins.\n * Read once when the client attaches — changing the value afterwards does **not**\n * re-populate the form (these are defaults, not controlled values); call `prefill(...)`\n * for that. No memoization needed: an inline object literal is fine.\n *\n * @example\n * defaultFormValues={{ info: [{ ll_field_unique_identifier: \"Email\", value: user.email }] }}\n */\n defaultFormValues?: DefaultFormValues;\n /**\n * Answer the form's transcribe requests (its transcribe button, shown when the\n * embed URL sets `showTranscribeButton`). Same semantics as\n * {@link CaptelloWebview.onTranscribeScannerRequest}: return the fields (a promise\n * is fine) and they are sent back as the result, or answer through the `reply`\n * second argument when the work is callback-style; a thrown `Error`'s message is\n * shown to the user. Must be set from the first render (it is wired when the iframe\n * attaches); the latest function is always the one invoked, so inline closures are fine.\n */\n onTranscribeScannerRequest?: TranscribeScannerRequestHandler;\n}\n\n/** Readiness of the embedded form. */\nexport type CaptelloWebviewStatus = \"loading\" | \"ready\" | \"error\";\n\n/** Props to spread onto the `<iframe>` — the ref plus the `embedUrl`-derived `src`. */\nexport interface CaptelloIframeProps {\n ref: RefCallback<HTMLIFrameElement | null>;\n src: string;\n}\n\n/** What {@link useCaptelloWebview} returns. */\nexport interface UseCaptelloWebviewResult {\n /** Spread onto your iframe: `<iframe {...iframeProps} />`. Carries the `embedUrl`-derived `src`. */\n iframeProps: CaptelloIframeProps;\n /** The iframe ref callback (same as `iframeProps.ref`), if you'd rather wire `src` yourself. */\n ref: RefCallback<HTMLIFrameElement | null>;\n /** `true` once the form has reported `form_load_complete`. */\n isReady: boolean;\n /** `\"loading\"` → `\"ready\"`; flips to `\"error\"` if a `form_error_message` arrives. */\n status: CaptelloWebviewStatus;\n /** The live client, or `null` before the iframe mounts. For escape-hatch use. */\n getClient: () => CaptelloWebview | null;\n submit: () => void;\n reset: () => void;\n updateDraft: () => void;\n triggerValidation: (target: ValidationTarget) => void;\n prefill: (data: { submission?: SubmissionPrefill; info?: PrefillInfoItem[] }) => void;\n submitAndWait: (timeoutMs?: number) => Promise<SubmissionBody>;\n}\n\n/**\n * Unwraps `message` to its payload and calls the matching callback.\n *\n * Exhaustive over {@link OutboundMessageType}: adding a message type without handling it\n * here is a compile error, so a new type can't silently go undelivered.\n */\nfunction dispatchToCallback(callbacks: CaptelloWebviewCallbacks, message: OutboundMessage): void {\n switch (message.type) {\n case OutboundMessageType.FormLoadComplete:\n callbacks.onFormLoadComplete?.();\n break;\n case OutboundMessageType.FormErrorMessage:\n callbacks.onFormErrorMessage?.(message.data);\n break;\n case OutboundMessageType.SubmissionBody:\n callbacks.onSubmissionBody?.(message.data);\n break;\n case OutboundMessageType.FormSubmitSuccess:\n callbacks.onFormSubmitSuccess?.(message.action);\n break;\n case OutboundMessageType.ConnexionsProfileRedirect:\n callbacks.onConnexionsProfileRedirect?.();\n break;\n case OutboundMessageType.ConnexionsDownloadVcard:\n callbacks.onConnexionsDownloadVcard?.();\n break;\n case OutboundMessageType.TranscribeScannerRequest:\n // Not a fire-and-forget callback: requests are answered through the\n // request/response path registered from `onTranscribeScannerRequest` (see\n // `useCaptelloWebview`). `onAnyMessage` still observes them.\n break;\n default: {\n const exhaustive: never = message;\n void exhaustive;\n }\n }\n}\n\n/**\n * Binds a {@link CaptelloWebview} to an iframe's lifecycle. See the module doc for usage.\n */\nexport function useCaptelloWebview(options: UseCaptelloWebviewOptions): UseCaptelloWebviewResult {\n const { embedUrl, matchSource, queueUntilReady, hostWindow } = options;\n\n // Build the iframe URL and scope messaging to its origin. Recomputed on every render\n // (cheap), but only the derived origin feeds `attach`'s deps, so a same-origin URL\n // change doesn't tear the client down.\n const src = buildEmbedUrl(embedUrl.baseUrl, embedUrl);\n const targetOrigin = new URL(src).origin;\n\n // Latest options/callbacks, read fresh inside listeners so callers needn't memoize.\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const clientRef = useRef<CaptelloWebview | null>(null);\n const frameRef = useRef<HTMLIFrameElement | null>(null);\n const teardown = useRef<(() => void) | null>(null);\n\n const [status, setStatus] = useState<CaptelloWebviewStatus>(\"loading\");\n\n const attach = useCallback(\n (frame: HTMLIFrameElement | null) => {\n // Tear down any previous client (ref changed or unmounting).\n teardown.current?.();\n teardown.current = null;\n clientRef.current = null;\n frameRef.current = frame;\n setStatus(\"loading\");\n\n if (!frame) return;\n\n const client = new CaptelloWebview(frame, {\n targetOrigin,\n hostWindow: optionsRef.current.hostWindow,\n matchSource: optionsRef.current.matchSource,\n queueUntilReady: optionsRef.current.queueUntilReady,\n });\n clientRef.current = client;\n\n const offs: Unsubscribe[] = [];\n\n // Seed the form with `defaultFormValues`. With `queueUntilReady` (the default)\n // this lands first in the outbox and flushes on load, so an explicit prefill()\n // made later still wins. With queueing off there's no outbox to ride in on, so\n // wait for the form to report in — subscribed before the callback loop below,\n // to seed before the caller's onFormLoadComplete runs.\n const defaults = optionsRef.current.defaultFormValues;\n if (defaults && (defaults.submission != null || defaults.info != null)) {\n if (optionsRef.current.queueUntilReady === false) {\n offs.push(\n client.once(OutboundMessageType.FormLoadComplete, () => {\n try {\n client.prefill(defaults);\n } catch {\n /* iframe detached between load and seed — drop silently */\n }\n }),\n );\n } else {\n client.prefill(defaults);\n }\n }\n\n for (const type of Object.values(OutboundMessageType)) {\n offs.push(\n client.on(type, (message) => {\n if (type === OutboundMessageType.FormLoadComplete) setStatus(\"ready\");\n else if (type === OutboundMessageType.FormErrorMessage) setStatus(\"error\");\n\n dispatchToCallback(optionsRef.current, message);\n optionsRef.current.onAnyMessage?.(message);\n }),\n );\n }\n\n // Wire the transcribe request/response path. Registered only when a handler\n // is configured at attach time; the latest handler is read per request so\n // inline closures stay fresh.\n if (optionsRef.current.onTranscribeScannerRequest) {\n offs.push(\n client.onTranscribeScannerRequest((request, reply) => {\n const handler = optionsRef.current.onTranscribeScannerRequest;\n if (!handler) {\n // Handler removed mid-flight — fail the request rather than\n // leaving the form's button hanging until its timeout.\n throw new Error(\"Transcription failed.\");\n }\n // `reply` is forwarded so a callback-style handler can answer\n // through it instead of returning a promise.\n return handler(request, reply);\n }),\n );\n }\n\n teardown.current = () => {\n for (const off of offs) off();\n client.destroy();\n };\n },\n // Re-create the client only when connection-level inputs change.\n // Callbacks are read via optionsRef, so they intentionally aren't deps.\n [targetOrigin, matchSource, queueUntilReady, hostWindow],\n );\n\n useEffect(() => {\n if (frameRef.current) attach(frameRef.current);\n return () => {\n teardown.current?.();\n teardown.current = null;\n clientRef.current = null;\n };\n }, [attach]);\n\n const getClient = useCallback(() => clientRef.current, []);\n\n const submit = useCallback(() => clientRef.current?.submit(), []);\n const reset = useCallback(() => clientRef.current?.reset(), []);\n const updateDraft = useCallback(() => clientRef.current?.updateDraft(), []);\n const triggerValidation = useCallback(\n (target: ValidationTarget) => clientRef.current?.triggerValidation(target),\n [],\n );\n const prefill = useCallback(\n (data: { submission?: SubmissionPrefill; info?: PrefillInfoItem[] }) => clientRef.current?.prefill(data),\n [],\n );\n const submitAndWait = useCallback((timeoutMs?: number) => {\n const client = clientRef.current;\n if (!client) {\n return Promise.reject(new Error(\"CaptelloWebview: iframe is not mounted yet.\"));\n }\n return client.submitAndWait(timeoutMs);\n }, []);\n\n const iframeProps: CaptelloIframeProps = { ref: attach, src };\n\n return {\n iframeProps,\n ref: attach,\n isReady: status === \"ready\",\n status,\n getClient,\n submit,\n reset,\n updateDraft,\n triggerValidation,\n prefill,\n submitAndWait,\n };\n}\n\n/* ------------------------------------------------------------------ *\n * <CaptelloForm /> — the turnkey component\n * ------------------------------------------------------------------ */\n\n/** Default iframe permissions for a capture form (business-card camera scan, mic, geo). */\nconst DEFAULT_ALLOW = \"camera; microphone; geolocation\";\n\n/** Wrapper is the positioning context for the loading / error overlays. */\nconst WRAPPER_STYLE: CSSProperties = { position: \"relative\" };\n\n/** The iframe fills the wrapper; size the component, not this. */\nconst IFRAME_STYLE: CSSProperties = { display: \"block\", width: \"100%\", height: \"100%\", border: 0 };\n\n/** Centers the `loading` / `error` node over the iframe. */\nconst OVERLAY_STYLE: CSSProperties = {\n position: \"absolute\",\n inset: 0,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n};\n\n/**\n * Imperative handle exposed on {@link CaptelloForm}'s `ref` — the same senders the hook\n * returns, plus the current status and the underlying `<iframe>` node. Lets a parent\n * drive the form (e.g. an external submit button) without lifting state.\n */\nexport interface CaptelloFormHandle\n extends Pick<\n UseCaptelloWebviewResult,\n \"submit\" | \"reset\" | \"updateDraft\" | \"triggerValidation\" | \"prefill\" | \"submitAndWait\" | \"getClient\"\n > {\n /** Current readiness: `\"loading\" | \"ready\" | \"error\"`. */\n readonly status: CaptelloWebviewStatus;\n /** `true` once the form has reported `form_load_complete`. */\n readonly isReady: boolean;\n /** The underlying `<iframe>` DOM node, or `null` before it mounts. */\n getIframe: () => HTMLIFrameElement | null;\n}\n\n/**\n * Props for {@link CaptelloForm}: every {@link UseCaptelloWebviewOptions} option (embed\n * config + message callbacks + client options) plus rendering conveniences.\n */\nexport interface CaptelloFormProps extends UseCaptelloWebviewOptions {\n /** `className` for the wrapper element. */\n className?: string;\n /** `style` for the wrapper element — size the form here. The component adds `position: relative`; your values win. */\n style?: CSSProperties;\n /** `id` for the wrapper element. */\n id?: string;\n /**\n * Attributes spread onto the `<iframe>` — `title`, `allow`, `sandbox`, `name`, etc.\n * Defaults: `title=\"Captello form\"`, `allow=\"camera; microphone; geolocation\"`.\n * `src` is ignored: it comes from `embedUrl`.\n */\n iframeProps?: Omit<IframeHTMLAttributes<HTMLIFrameElement>, \"ref\">;\n /** Rendered, centered over the iframe, while it is loading. The iframe stays mounted underneath. */\n loading?: ReactNode;\n /**\n * Rendered, centered over the iframe, when the form reports `form_error_message`.\n * Pass a function to receive the translated, display-ready error text.\n */\n error?: ReactNode | ((message: string | undefined) => ReactNode);\n /**\n * Inline controls rendered after the form. A function receives the live api\n * (status + senders), so you can wire a submit button without a `ref`.\n */\n children?: ReactNode | ((api: UseCaptelloWebviewResult) => ReactNode);\n}\n\nfunction CaptelloFormImpl(props: CaptelloFormProps, ref: Ref<CaptelloFormHandle>): ReactElement {\n const {\n className,\n style,\n id,\n iframeProps,\n loading,\n error,\n children,\n onFormLoadComplete,\n onFormErrorMessage,\n ...options\n } = props;\n\n // The translated error text from the last form_error_message, for the `error` render.\n const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);\n\n const api = useCaptelloWebview({\n ...options,\n // Wrap the two status-bearing callbacks to track the error text, then forward to\n // the caller's handler. The hook reads callbacks fresh, so these inline wrappers\n // don't re-subscribe or re-create the client.\n onFormLoadComplete: () => {\n setErrorMessage(undefined);\n onFormLoadComplete?.();\n },\n onFormErrorMessage: (message) => {\n setErrorMessage(message);\n onFormErrorMessage?.(message);\n },\n });\n\n // Merge the hook's iframe ref with our own node ref so getIframe() can return the DOM node.\n const nodeRef = useRef<HTMLIFrameElement | null>(null);\n const hookRef = api.iframeProps.ref;\n const setIframe = useCallback<RefCallback<HTMLIFrameElement | null>>(\n (node) => {\n nodeRef.current = node;\n hookRef(node);\n },\n [hookRef],\n );\n\n useImperativeHandle(\n ref,\n () => ({\n submit: api.submit,\n reset: api.reset,\n updateDraft: api.updateDraft,\n triggerValidation: api.triggerValidation,\n prefill: api.prefill,\n submitAndWait: api.submitAndWait,\n getClient: api.getClient,\n getIframe: () => nodeRef.current,\n status: api.status,\n isReady: api.isReady,\n }),\n [api],\n );\n\n const showLoading = api.status === \"loading\" && loading != null;\n const showError = api.status === \"error\" && error != null;\n\n return (\n <>\n <div className={className} id={id} style={{ ...WRAPPER_STYLE, ...style }}>\n <iframe\n title=\"Captello form\"\n allow={DEFAULT_ALLOW}\n {...iframeProps}\n ref={setIframe}\n src={api.iframeProps.src}\n style={{ ...IFRAME_STYLE, ...iframeProps?.style }}\n />\n {showLoading ? <div style={OVERLAY_STYLE}>{loading}</div> : null}\n {showError ? (\n <div style={OVERLAY_STYLE}>{typeof error === \"function\" ? error(errorMessage) : error}</div>\n ) : null}\n </div>\n {typeof children === \"function\" ? children(api) : children}\n </>\n );\n}\n\n/**\n * Turnkey component for embedding a Captello capture form — the shortest path to a\n * working integration. Renders the `<iframe>`, wires {@link useCaptelloWebview} to it,\n * shows your `loading` / `error` overlays, and forwards a {@link CaptelloFormHandle} on\n * `ref` so a parent can `submit()` / `prefill()` without lifting state.\n *\n * `embedUrl` is required and the form fills its wrapper — size the form via `className` /\n * `style` (an iframe has no intrinsic height). Reach for {@link useCaptelloWebview} instead\n * when you need to own the markup.\n *\n * @example\n * function UlcForm({ token, email }: { token: string; email: string }) {\n * const ref = useRef<CaptelloFormHandle>(null);\n * return (\n * <CaptelloForm\n * ref={ref}\n * style={{ height: 600 }}\n * embedUrl={{\n * baseUrl: \"https://capture.captello.com\",\n * eventWebAccessToken: token,\n * mode: FormMode.Submit,\n * launcher: LauncherType.EventGenWeb,\n * }}\n * defaultFormValues={{ info: [{ ll_field_unique_identifier: \"Email\", value: email }] }}\n * onSubmissionBody={save}\n * loading={<Spinner />}\n * error={(msg) => <ErrorBanner>{msg}</ErrorBanner>}\n * >\n * {({ isReady }) => <button disabled={!isReady} onClick={() => ref.current?.submit()}>Submit</button>}\n * </CaptelloForm>\n * );\n * }\n */\nexport const CaptelloForm = forwardRef(CaptelloFormImpl);\nCaptelloForm.displayName = \"CaptelloForm\";\n\nexport { CaptelloWebview, SubmissionError, SubmissionTimeoutError } from \"./client\";\nexport type { Unsubscribe } from \"./client\";\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/react.tsx"],"names":[],"mappings":"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","file":"react.js","sourcesContent":["/**\n * React adapter for the Captello webview SDK — `@captello/ulc-webview-sdk/react`.\n *\n * Two entry points, same engine:\n * - {@link CaptelloForm} — a turnkey `<iframe>` component. Drop it in with an `embedUrl`\n * and message callbacks; it renders the frame, shows your `loading` / `error` overlays,\n * and exposes the senders via an imperative `ref`. This is the shortest path.\n * - {@link useCaptelloWebview} — the underlying hook, for when you want to own the markup.\n *\n * {@link useCaptelloWebview} owns a {@link CaptelloWebview} for the lifetime of an\n * iframe: it creates the client once the iframe mounts, wires the outbound messages\n * you care about to typed callbacks, tracks readiness, and destroys the client on\n * unmount. You get back `iframeProps` to spread onto your `<iframe>` (or a bare `ref`),\n * an `isReady` flag, and stable senders (`submit`, `reset`, `prefill`, …).\n *\n * Sends made before the form loads are queued by the client and flushed on\n * `form_load_complete`, so you can call `prefill(...)` as soon as you have data —\n * no need to gate on readiness yourself. To seed a form declaratively, pass\n * `defaultFormValues` instead and skip the `prefill(...)` wiring entirely.\n *\n * Callbacks are held in a ref and always called fresh, so you do NOT need to memoize\n * them — passing inline arrow functions will not re-subscribe or re-create the client.\n *\n * `react` is an optional peer dependency; importing this entry point requires React 18+.\n *\n * @example\n * function UlcForm({ token, onSubmitted }: { token: string; onSubmitted: (b: SubmissionBody) => void }) {\n * const { iframeProps, isReady, submit } = useCaptelloWebview({\n * embedUrl: {\n * baseUrl: \"https://capture.captello.com\",\n * eventWebAccessToken: token,\n * mode: FormMode.Submit,\n * launcher: LauncherType.EventGenWeb,\n * },\n * onSubmissionBody: onSubmitted,\n * });\n * return (\n * <>\n * {!isReady && <Spinner />}\n * <iframe {...iframeProps} title=\"UlcForm\" allow=\"camera; microphone\" />\n * <button onClick={submit}>Submit</button>\n * </>\n * );\n * }\n */\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n type CSSProperties,\n type IframeHTMLAttributes,\n type ReactElement,\n type ReactNode,\n type Ref,\n type RefCallback,\n} from \"react\";\n\nimport { CaptelloWebview } from \"./client\";\nimport type { CaptelloWebviewOptions, TranscribeScannerRequestHandler, Unsubscribe } from \"./client\";\nimport { buildEmbedUrl } from \"./embed-url\";\nimport type { EmbedUrlOptions } from \"./embed-url\";\nimport { OutboundMessageType } from \"./messages\";\nimport type { OutboundMessage, PrefillInfoItem, SubmissionBody, SubmissionPrefill, ValidationTarget } from \"./messages\";\n\n/**\n * Per-message-type callback props accepted by {@link useCaptelloWebview}.\n *\n * Each callback receives the message's **payload**, not the message envelope — the\n * callback name already carries the `type`, so there is nothing to discriminate on.\n * Messages that carry no payload take no argument.\n *\n * {@link CaptelloWebviewCallbacks.onAnyMessage} is the exception: it fires for every\n * type, so it gets the whole message including the `type` discriminator.\n */\nexport interface CaptelloWebviewCallbacks {\n /** The form finished loading and rendering. Safe to interact with it after this. */\n onFormLoadComplete?: () => void;\n /** A user-facing error occurred. Receives the translated, display-ready text. */\n onFormErrorMessage?: (message: string) => void;\n /** Receives the full submission body, for the host to persist / forward. */\n onSubmissionBody?: (body: SubmissionBody) => void;\n /** The form was submitted successfully. Receives whether it created or updated. */\n onFormSubmitSuccess?: (action: \"create\" | \"update\") => void;\n /** Connexions: the host should perform the profile redirect (embed mode). */\n onConnexionsProfileRedirect?: () => void;\n /** Connexions: the host should trigger the vCard download (embed mode). */\n onConnexionsDownloadVcard?: () => void;\n /** Catch-all: the full message, including `type`. Called after the specific handler above. */\n onAnyMessage?: (message: OutboundMessage) => void;\n}\n\n/** Embed-URL config: a base URL plus {@link EmbedUrlOptions}. */\nexport interface EmbedUrlConfig extends EmbedUrlOptions {\n /**\n * The capture **origin**, e.g. `\"https://capture.captello.com\"`. The SDK appends the\n * capture path for you, so the origin, a trailing slash, or the full\n * `…/capture/submission` URL all work — see {@link buildEmbedUrl}.\n */\n baseUrl: string;\n}\n\n/**\n * Values to seed a form with on load — see {@link UseCaptelloWebviewOptions.defaultFormValues}.\n *\n * `submission` is typed as {@link SubmissionPrefill} (every field optional) so a partial\n * object assembled from your own data is valid, as is a `submission.data` array fetched\n * from the submissions API or a whole {@link SubmissionBody} echoed back from\n * `onSubmissionBody`.\n */\nexport interface DefaultFormValues {\n /**\n * Submitted values under `data`, as either the submissions API's array (see\n * `SubmissionPrefillDataItem`) or a flat `DraftSubmissionData` record.\n */\n submission?: SubmissionPrefill;\n /** Field values matched by `ll_field_unique_identifier` (e.g. `\"Email\"`). */\n info?: PrefillInfoItem[];\n}\n\n/**\n * Options for {@link useCaptelloWebview}: the embed config, message callbacks, and the\n * usual client options.\n *\n * `embedUrl` is required — the hook builds the URL from it, derives `targetOrigin`, and\n * returns it as `iframeProps.src`. Any `targetOrigin` you pass is ignored; drop to\n * {@link CaptelloWebview} directly if you need to own both the URL and the origin.\n */\nexport interface UseCaptelloWebviewOptions\n extends Omit<CaptelloWebviewOptions, \"targetOrigin\">,\n CaptelloWebviewCallbacks {\n /** Build the iframe URL and derive `targetOrigin` from it. Sets `iframeProps.src`. */\n embedUrl: EmbedUrlConfig;\n /**\n * Values to populate the form with as soon as it is ready. Saves you from wiring a\n * `ref` and calling `prefill(...)` from an effect just to seed the form.\n *\n * Sent as the *first* outbound message, so a later explicit `prefill(...)` wins.\n * Read once when the client attaches — changing the value afterwards does **not**\n * re-populate the form (these are defaults, not controlled values); call `prefill(...)`\n * for that. No memoization needed: an inline object literal is fine.\n *\n * @example\n * defaultFormValues={{ info: [{ ll_field_unique_identifier: \"Email\", value: user.email }] }}\n */\n defaultFormValues?: DefaultFormValues;\n /**\n * Answer the form's transcribe requests (its transcribe button, shown when the\n * embed URL sets `showTranscribeButton`). Same semantics as\n * {@link CaptelloWebview.onTranscribeScannerRequest}: return the fields (a promise\n * is fine) and they are sent back as the result, or answer through the `reply`\n * second argument when the work is callback-style; a thrown `Error`'s message is\n * shown to the user. Must be set from the first render (it is wired when the iframe\n * attaches); the latest function is always the one invoked, so inline closures are fine.\n */\n onTranscribeScannerRequest?: TranscribeScannerRequestHandler;\n}\n\n/** Readiness of the embedded form. */\nexport type CaptelloWebviewStatus = \"loading\" | \"ready\" | \"error\";\n\n/** Props to spread onto the `<iframe>` — the ref plus the `embedUrl`-derived `src`. */\nexport interface CaptelloIframeProps {\n ref: RefCallback<HTMLIFrameElement | null>;\n src: string;\n}\n\n/** What {@link useCaptelloWebview} returns. */\nexport interface UseCaptelloWebviewResult {\n /** Spread onto your iframe: `<iframe {...iframeProps} />`. Carries the `embedUrl`-derived `src`. */\n iframeProps: CaptelloIframeProps;\n /** The iframe ref callback (same as `iframeProps.ref`), if you'd rather wire `src` yourself. */\n ref: RefCallback<HTMLIFrameElement | null>;\n /** `true` once the form has reported `form_load_complete`. */\n isReady: boolean;\n /** `\"loading\"` → `\"ready\"`; flips to `\"error\"` if a `form_error_message` arrives. */\n status: CaptelloWebviewStatus;\n /** The live client, or `null` before the iframe mounts. For escape-hatch use. */\n getClient: () => CaptelloWebview | null;\n submit: () => void;\n reset: () => void;\n updateDraft: () => void;\n triggerValidation: (target: ValidationTarget) => void;\n prefill: (data: { submission?: SubmissionPrefill; info?: PrefillInfoItem[] }) => void;\n submitAndWait: (timeoutMs?: number) => Promise<SubmissionBody>;\n}\n\n/**\n * Unwraps `message` to its payload and calls the matching callback.\n *\n * Exhaustive over {@link OutboundMessageType}: adding a message type without handling it\n * here is a compile error, so a new type can't silently go undelivered.\n */\nfunction dispatchToCallback(callbacks: CaptelloWebviewCallbacks, message: OutboundMessage): void {\n switch (message.type) {\n case OutboundMessageType.FormLoadComplete:\n callbacks.onFormLoadComplete?.();\n break;\n case OutboundMessageType.FormErrorMessage:\n callbacks.onFormErrorMessage?.(message.data);\n break;\n case OutboundMessageType.SubmissionBody:\n callbacks.onSubmissionBody?.(message.data);\n break;\n case OutboundMessageType.FormSubmitSuccess:\n callbacks.onFormSubmitSuccess?.(message.action);\n break;\n case OutboundMessageType.ConnexionsProfileRedirect:\n callbacks.onConnexionsProfileRedirect?.();\n break;\n case OutboundMessageType.ConnexionsDownloadVcard:\n callbacks.onConnexionsDownloadVcard?.();\n break;\n case OutboundMessageType.TranscribeScannerRequest:\n // Not a fire-and-forget callback: requests are answered through the\n // request/response path registered from `onTranscribeScannerRequest` (see\n // `useCaptelloWebview`). `onAnyMessage` still observes them.\n break;\n default: {\n const exhaustive: never = message;\n void exhaustive;\n }\n }\n}\n\n/**\n * Binds a {@link CaptelloWebview} to an iframe's lifecycle. See the module doc for usage.\n */\nexport function useCaptelloWebview(options: UseCaptelloWebviewOptions): UseCaptelloWebviewResult {\n const { embedUrl, matchSource, queueUntilReady, hostWindow } = options;\n\n // Build the iframe URL and scope messaging to its origin. Recomputed on every render\n // (cheap), but only the derived origin feeds `attach`'s deps, so a same-origin URL\n // change doesn't tear the client down.\n const src = buildEmbedUrl(embedUrl.baseUrl, embedUrl);\n const targetOrigin = new URL(src).origin;\n\n // Latest options/callbacks, read fresh inside listeners so callers needn't memoize.\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const clientRef = useRef<CaptelloWebview | null>(null);\n const frameRef = useRef<HTMLIFrameElement | null>(null);\n const teardown = useRef<(() => void) | null>(null);\n\n const [status, setStatus] = useState<CaptelloWebviewStatus>(\"loading\");\n\n const attach = useCallback(\n (frame: HTMLIFrameElement | null) => {\n // Tear down any previous client (ref changed or unmounting).\n teardown.current?.();\n teardown.current = null;\n clientRef.current = null;\n frameRef.current = frame;\n setStatus(\"loading\");\n\n if (!frame) return;\n\n const client = new CaptelloWebview(frame, {\n targetOrigin,\n hostWindow: optionsRef.current.hostWindow,\n matchSource: optionsRef.current.matchSource,\n queueUntilReady: optionsRef.current.queueUntilReady,\n });\n clientRef.current = client;\n\n const offs: Unsubscribe[] = [];\n\n // Seed the form with `defaultFormValues`. With `queueUntilReady` (the default)\n // this lands first in the outbox and flushes on load, so an explicit prefill()\n // made later still wins. With queueing off there's no outbox to ride in on, so\n // wait for the form to report in — subscribed before the callback loop below,\n // to seed before the caller's onFormLoadComplete runs.\n const defaults = optionsRef.current.defaultFormValues;\n if (defaults && (defaults.submission != null || defaults.info != null)) {\n if (optionsRef.current.queueUntilReady === false) {\n offs.push(\n client.once(OutboundMessageType.FormLoadComplete, () => {\n try {\n client.prefill(defaults);\n } catch {\n /* iframe detached between load and seed — drop silently */\n }\n }),\n );\n } else {\n client.prefill(defaults);\n }\n }\n\n for (const type of Object.values(OutboundMessageType)) {\n offs.push(\n client.on(type, (message) => {\n if (type === OutboundMessageType.FormLoadComplete) setStatus(\"ready\");\n else if (type === OutboundMessageType.FormErrorMessage) setStatus(\"error\");\n\n dispatchToCallback(optionsRef.current, message);\n optionsRef.current.onAnyMessage?.(message);\n }),\n );\n }\n\n // Wire the transcribe request/response path. Registered only when a handler\n // is configured at attach time; the latest handler is read per request so\n // inline closures stay fresh.\n if (optionsRef.current.onTranscribeScannerRequest) {\n offs.push(\n client.onTranscribeScannerRequest((request, reply) => {\n const handler = optionsRef.current.onTranscribeScannerRequest;\n if (!handler) {\n // Handler removed mid-flight — fail the request rather than\n // leaving the form's button hanging until its timeout.\n throw new Error(\"Transcription failed.\");\n }\n // `reply` is forwarded so a callback-style handler can answer\n // through it instead of returning a promise.\n return handler(request, reply);\n }),\n );\n }\n\n teardown.current = () => {\n for (const off of offs) off();\n client.destroy();\n };\n },\n // Re-create the client only when connection-level inputs change.\n // Callbacks are read via optionsRef, so they intentionally aren't deps.\n [targetOrigin, matchSource, queueUntilReady, hostWindow],\n );\n\n useEffect(() => {\n if (frameRef.current) attach(frameRef.current);\n return () => {\n teardown.current?.();\n teardown.current = null;\n clientRef.current = null;\n };\n }, [attach]);\n\n const getClient = useCallback(() => clientRef.current, []);\n\n const submit = useCallback(() => clientRef.current?.submit(), []);\n const reset = useCallback(() => clientRef.current?.reset(), []);\n const updateDraft = useCallback(() => clientRef.current?.updateDraft(), []);\n const triggerValidation = useCallback(\n (target: ValidationTarget) => clientRef.current?.triggerValidation(target),\n [],\n );\n const prefill = useCallback(\n (data: { submission?: SubmissionPrefill; info?: PrefillInfoItem[] }) => clientRef.current?.prefill(data),\n [],\n );\n const submitAndWait = useCallback((timeoutMs?: number) => {\n const client = clientRef.current;\n if (!client) {\n return Promise.reject(new Error(\"CaptelloWebview: iframe is not mounted yet.\"));\n }\n return client.submitAndWait(timeoutMs);\n }, []);\n\n const iframeProps: CaptelloIframeProps = { ref: attach, src };\n\n return {\n iframeProps,\n ref: attach,\n isReady: status === \"ready\",\n status,\n getClient,\n submit,\n reset,\n updateDraft,\n triggerValidation,\n prefill,\n submitAndWait,\n };\n}\n\n/* ------------------------------------------------------------------ *\n * <CaptelloForm /> — the turnkey component\n * ------------------------------------------------------------------ */\n\n/** Default iframe permissions for a capture form (business-card camera scan, mic, geo). */\nconst DEFAULT_ALLOW = \"camera; microphone; geolocation\";\n\n/** Wrapper is the positioning context for the loading / error overlays. */\nconst WRAPPER_STYLE: CSSProperties = { position: \"relative\" };\n\n/** The iframe fills the wrapper; size the component, not this. */\nconst IFRAME_STYLE: CSSProperties = { display: \"block\", width: \"100%\", height: \"100%\", border: 0 };\n\n/** Centers the `loading` / `error` node over the iframe. */\nconst OVERLAY_STYLE: CSSProperties = {\n position: \"absolute\",\n inset: 0,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n};\n\n/**\n * Imperative handle exposed on {@link CaptelloForm}'s `ref` — the same senders the hook\n * returns, plus the current status and the underlying `<iframe>` node. Lets a parent\n * drive the form (e.g. an external submit button) without lifting state.\n */\nexport interface CaptelloFormHandle\n extends Pick<\n UseCaptelloWebviewResult,\n \"submit\" | \"reset\" | \"updateDraft\" | \"triggerValidation\" | \"prefill\" | \"submitAndWait\" | \"getClient\"\n > {\n /** Current readiness: `\"loading\" | \"ready\" | \"error\"`. */\n readonly status: CaptelloWebviewStatus;\n /** `true` once the form has reported `form_load_complete`. */\n readonly isReady: boolean;\n /** The underlying `<iframe>` DOM node, or `null` before it mounts. */\n getIframe: () => HTMLIFrameElement | null;\n}\n\n/**\n * Props for {@link CaptelloForm}: every {@link UseCaptelloWebviewOptions} option (embed\n * config + message callbacks + client options) plus rendering conveniences.\n */\nexport interface CaptelloFormProps extends UseCaptelloWebviewOptions {\n /** `className` for the wrapper element. */\n className?: string;\n /** `style` for the wrapper element — size the form here. The component adds `position: relative`; your values win. */\n style?: CSSProperties;\n /** `id` for the wrapper element. */\n id?: string;\n /**\n * Attributes spread onto the `<iframe>` — `title`, `allow`, `sandbox`, `name`, etc.\n * Defaults: `title=\"Captello form\"`, `allow=\"camera; microphone; geolocation\"`.\n * `src` is ignored: it comes from `embedUrl`.\n */\n iframeProps?: Omit<IframeHTMLAttributes<HTMLIFrameElement>, \"ref\">;\n /** Rendered, centered over the iframe, while it is loading. The iframe stays mounted underneath. */\n loading?: ReactNode;\n /**\n * Rendered, centered over the iframe, when the form reports `form_error_message`.\n * Pass a function to receive the translated, display-ready error text.\n */\n error?: ReactNode | ((message: string | undefined) => ReactNode);\n /**\n * Inline controls rendered after the form. A function receives the live api\n * (status + senders), so you can wire a submit button without a `ref`.\n */\n children?: ReactNode | ((api: UseCaptelloWebviewResult) => ReactNode);\n}\n\nfunction CaptelloFormImpl(props: CaptelloFormProps, ref: Ref<CaptelloFormHandle>): ReactElement {\n const {\n className,\n style,\n id,\n iframeProps,\n loading,\n error,\n children,\n onFormLoadComplete,\n onFormErrorMessage,\n ...options\n } = props;\n\n // The translated error text from the last form_error_message, for the `error` render.\n const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);\n\n const api = useCaptelloWebview({\n ...options,\n // Wrap the two status-bearing callbacks to track the error text, then forward to\n // the caller's handler. The hook reads callbacks fresh, so these inline wrappers\n // don't re-subscribe or re-create the client.\n onFormLoadComplete: () => {\n setErrorMessage(undefined);\n onFormLoadComplete?.();\n },\n onFormErrorMessage: (message) => {\n setErrorMessage(message);\n onFormErrorMessage?.(message);\n },\n });\n\n // Merge the hook's iframe ref with our own node ref so getIframe() can return the DOM node.\n const nodeRef = useRef<HTMLIFrameElement | null>(null);\n const hookRef = api.iframeProps.ref;\n const setIframe = useCallback<RefCallback<HTMLIFrameElement | null>>(\n (node) => {\n nodeRef.current = node;\n hookRef(node);\n },\n [hookRef],\n );\n\n useImperativeHandle(\n ref,\n () => ({\n submit: api.submit,\n reset: api.reset,\n updateDraft: api.updateDraft,\n triggerValidation: api.triggerValidation,\n prefill: api.prefill,\n submitAndWait: api.submitAndWait,\n getClient: api.getClient,\n getIframe: () => nodeRef.current,\n status: api.status,\n isReady: api.isReady,\n }),\n [api],\n );\n\n const showLoading = api.status === \"loading\" && loading != null;\n const showError = api.status === \"error\" && error != null;\n\n return (\n <>\n <div className={className} id={id} style={{ ...WRAPPER_STYLE, ...style }}>\n <iframe\n title=\"Captello form\"\n allow={DEFAULT_ALLOW}\n {...iframeProps}\n ref={setIframe}\n src={api.iframeProps.src}\n style={{ ...IFRAME_STYLE, ...iframeProps?.style }}\n />\n {showLoading ? <div style={OVERLAY_STYLE}>{loading}</div> : null}\n {showError ? (\n <div style={OVERLAY_STYLE}>{typeof error === \"function\" ? error(errorMessage) : error}</div>\n ) : null}\n </div>\n {typeof children === \"function\" ? children(api) : children}\n </>\n );\n}\n\n/**\n * Turnkey component for embedding a Captello capture form — the shortest path to a\n * working integration. Renders the `<iframe>`, wires {@link useCaptelloWebview} to it,\n * shows your `loading` / `error` overlays, and forwards a {@link CaptelloFormHandle} on\n * `ref` so a parent can `submit()` / `prefill()` without lifting state.\n *\n * `embedUrl` is required and the form fills its wrapper — size the form via `className` /\n * `style` (an iframe has no intrinsic height). Reach for {@link useCaptelloWebview} instead\n * when you need to own the markup.\n *\n * @example\n * function UlcForm({ token, email }: { token: string; email: string }) {\n * const ref = useRef<CaptelloFormHandle>(null);\n * return (\n * <CaptelloForm\n * ref={ref}\n * style={{ height: 600 }}\n * embedUrl={{\n * baseUrl: \"https://capture.captello.com\",\n * eventWebAccessToken: token,\n * mode: FormMode.Submit,\n * launcher: LauncherType.EventGenWeb,\n * }}\n * defaultFormValues={{ info: [{ ll_field_unique_identifier: \"Email\", value: email }] }}\n * onSubmissionBody={save}\n * loading={<Spinner />}\n * error={(msg) => <ErrorBanner>{msg}</ErrorBanner>}\n * >\n * {({ isReady }) => <button disabled={!isReady} onClick={() => ref.current?.submit()}>Submit</button>}\n * </CaptelloForm>\n * );\n * }\n */\nexport const CaptelloForm = forwardRef(CaptelloFormImpl);\nCaptelloForm.displayName = \"CaptelloForm\";\n\nexport { CaptelloWebview, SubmissionError, SubmissionTimeoutError } from \"./client\";\nexport type { Unsubscribe } from \"./client\";\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@captello/ulc-webview-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Typed SDK for embedding the Captello capture webview: message protocol, host client, and embed-URL builder.",
|
|
5
5
|
"author": "Lead Liaison",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
"react": [
|
|
17
17
|
"./dist/react.d.ts"
|
|
18
18
|
],
|
|
19
|
-
"
|
|
20
|
-
"./dist/
|
|
19
|
+
"mobile-host": [
|
|
20
|
+
"./dist/mobile-host.d.ts"
|
|
21
|
+
],
|
|
22
|
+
"embedded-app": [
|
|
23
|
+
"./dist/embedded-app.d.ts"
|
|
21
24
|
]
|
|
22
25
|
}
|
|
23
26
|
},
|
|
@@ -34,9 +37,13 @@
|
|
|
34
37
|
"types": "./dist/react.d.ts",
|
|
35
38
|
"import": "./dist/react.js"
|
|
36
39
|
},
|
|
37
|
-
"./
|
|
38
|
-
"types": "./dist/
|
|
39
|
-
"import": "./dist/
|
|
40
|
+
"./mobile-host": {
|
|
41
|
+
"types": "./dist/mobile-host.d.ts",
|
|
42
|
+
"import": "./dist/mobile-host.js"
|
|
43
|
+
},
|
|
44
|
+
"./embedded-app": {
|
|
45
|
+
"types": "./dist/embedded-app.d.ts",
|
|
46
|
+
"import": "./dist/embedded-app.js"
|
|
40
47
|
},
|
|
41
48
|
"./package.json": "./package.json"
|
|
42
49
|
},
|