@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.
@@ -1,228 +0,0 @@
1
- import { P as PrefillInfoItem, U as Unsubscribe } from './client-CAMlFA8s.js';
2
-
3
- /**
4
- * Pages embedded inside the Captello mobile app — `@captello/ulc-webview-sdk/app`.
5
- *
6
- * The mobile app hosts web pages (the meeting platform, Connexions, …) in an iframe and
7
- * offers them native services over `postMessage`: an auth token, the badge scanner,
8
- * back navigation, sharing. This module is the page-side client for that channel.
9
- *
10
- * It is the mirror image of {@link CaptelloWebview}: there your page is the host and
11
- * the Captello capture webview is the child; here the Captello *app* is the host and
12
- * your page is the child. The wire format differs too — match it exactly:
13
- * - Messages are posted as **plain objects**, not JSON strings.
14
- * - `type` values are SCREAMING_CASE.
15
- * - Requests (page → app) and responses (app → page) are disjoint enums.
16
- *
17
- * @example
18
- * import { CaptelloAppHost } from "@captello/ulc-webview-sdk/app";
19
- *
20
- * const app = new CaptelloAppHost();
21
- * const token = await app.requestAuthToken();
22
- * app.notifyReady();
23
- * const [person] = await app.openScanner();
24
- */
25
-
26
- /** Message `type` values a page sends to the Captello app. */
27
- declare enum AppRequestType {
28
- /** The page finished loading; the app hides its loading spinner. */
29
- AppReady = "APP_READY",
30
- /** A user-facing error; the app shows `message`. */
31
- Error = "ERROR",
32
- /** Close the page (the app dismisses the modal or pops the route). */
33
- NavigateBack = "NAVIGATE_BACK",
34
- /** Ask for a magic token; answered with {@link AppResponseType.AuthToken}. */
35
- RequestAuthToken = "REQUEST_AUTH_TOKEN",
36
- /**
37
- * Open the app's badge scanner. Answered with one
38
- * {@link AppResponseType.ScannerResult} per scanned person, then
39
- * {@link AppResponseType.ScannerClosed}.
40
- */
41
- OpenScanner = "OPEN_ULC_FORM_SCANNER",
42
- /** Open `url` in the system browser. */
43
- OpenUrl = "OPEN_URL",
44
- /** Copy `text` to the clipboard; acknowledged with `COPIED_SUCCESS` / `COPIED_ERROR`. */
45
- CopyText = "COPY_TEXT",
46
- /** Open the native share sheet. */
47
- ShareUrl = "SHARE_URL",
48
- /** Save a vCard to the contacts; acknowledged with `SAVE_VCARD_SUCCESS` / `SAVE_VCARD_ERROR`. */
49
- SaveVCard = "SAVE_VCARD",
50
- /** Add a wallet pass; acknowledged with `ADD_TO_WALLET_SUCCESS` / `ADD_TO_WALLET_ERROR`. */
51
- AddToWallet = "ADD_TO_WALLET",
52
- /** Ask the app to synchronize its data. */
53
- SyncApp = "SYNC_APP"
54
- }
55
- interface AppReadyRequest {
56
- type: AppRequestType.AppReady;
57
- }
58
- interface ErrorRequest {
59
- type: AppRequestType.Error;
60
- message: string;
61
- }
62
- interface NavigateBackRequest {
63
- type: AppRequestType.NavigateBack;
64
- }
65
- interface RequestAuthTokenRequest {
66
- type: AppRequestType.RequestAuthToken;
67
- }
68
- interface OpenScannerRequest {
69
- type: AppRequestType.OpenScanner;
70
- }
71
- interface OpenUrlRequest {
72
- type: AppRequestType.OpenUrl;
73
- url: string;
74
- }
75
- interface CopyTextRequest {
76
- type: AppRequestType.CopyText;
77
- text: string;
78
- }
79
- interface ShareUrlRequest {
80
- type: AppRequestType.ShareUrl;
81
- title: string;
82
- text: string;
83
- }
84
- interface SaveVCardRequest {
85
- type: AppRequestType.SaveVCard;
86
- /** vCard text. */
87
- text: string;
88
- }
89
- interface AddToWalletRequest {
90
- type: AppRequestType.AddToWallet;
91
- /** Base64 pass data, with or without a `data:` prefix. */
92
- text: string;
93
- }
94
- interface SyncAppRequest {
95
- type: AppRequestType.SyncApp;
96
- }
97
- /** Discriminated union of every message a page can send to the app. */
98
- type AppRequest = AppReadyRequest | ErrorRequest | NavigateBackRequest | RequestAuthTokenRequest | OpenScannerRequest | OpenUrlRequest | CopyTextRequest | ShareUrlRequest | SaveVCardRequest | AddToWalletRequest | SyncAppRequest;
99
- /** Message `type` values the Captello app sends to a page. */
100
- declare enum AppResponseType {
101
- /** Answer to {@link AppRequestType.RequestAuthToken}. */
102
- AuthToken = "AUTH_TOKEN",
103
- /** One scanned person (or a scanner failure) after {@link AppRequestType.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
- /** App → page: the magic token requested with {@link AppRequestType.RequestAuthToken}. */
115
- interface AuthTokenMessage {
116
- type: AppResponseType.AuthToken;
117
- token: string;
118
- }
119
- /**
120
- * App → page: 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: AppResponseType.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
- /** App → page: the scanner session ended. Follows the last {@link ScannerResultMessage}. */
144
- interface ScannerClosedMessage {
145
- type: AppResponseType.ScannerClosed;
146
- }
147
- interface AckMessage<T extends AppResponseType> {
148
- type: T;
149
- }
150
- /** Discriminated union of every message the app can send to a page. */
151
- type AppResponse = AuthTokenMessage | ScannerResultMessage | ScannerClosedMessage | AckMessage<AppResponseType.CopiedSuccess> | AckMessage<AppResponseType.CopiedError> | AckMessage<AppResponseType.SaveVCardSuccess> | AckMessage<AppResponseType.SaveVCardError> | AckMessage<AppResponseType.AddToWalletSuccess> | AckMessage<AppResponseType.AddToWalletError>;
152
- /** Maps each response `type` to its full message shape (used by {@link CaptelloAppHost.on}). */
153
- type AppResponseMap = {
154
- [M in AppResponse as M["type"]]: M;
155
- };
156
- /** One person captured by the app's scanner, as resolved by {@link CaptelloAppHost.openScanner}. */
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
- /** Rejection reason from {@link CaptelloAppHost.openScanner} when the app reports a failure. */
164
- declare class ScannerError extends Error {
165
- constructor(message: string);
166
- }
167
- /**
168
- * Parses a raw `MessageEvent.data` value into a typed {@link AppResponse}, or returns
169
- * `null` if it is not a recognized Captello app message.
170
- *
171
- * The app posts plain objects; a JSON string is accepted too for robustness.
172
- */
173
- declare function parseAppResponse(data: unknown): AppResponse | null;
174
- /** Listener for a specific app response type. */
175
- type AppResponseListener<T extends AppResponseType> = (message: AppResponseMap[T]) => void;
176
- interface CaptelloAppHostOptions {
177
- /** Window the app is listening on. Defaults to `window.parent`. */
178
- appWindow?: Window;
179
- /** Window to receive the app's responses on. Defaults to `window`. */
180
- pageWindow?: Window;
181
- /**
182
- * `targetOrigin` for outgoing `postMessage` calls. Defaults to `"*"`: the app's
183
- * webview origin differs per platform (`capacitor://localhost`, `http://localhost`),
184
- * and the page only ever runs inside the app when it uses this channel.
185
- */
186
- targetOrigin?: string;
187
- }
188
- /**
189
- * Page-side client for the Captello mobile app that embeds the page.
190
- *
191
- * Construct one per page and keep it for the page's lifetime. It attaches a single
192
- * `message` listener on construction; call {@link destroy} to remove it.
193
- */
194
- declare class CaptelloAppHost {
195
- private readonly appWindow;
196
- private readonly pageWindow;
197
- private readonly targetOrigin;
198
- private readonly listeners;
199
- private readonly boundHandler;
200
- private destroyed;
201
- constructor(options?: CaptelloAppHostOptions);
202
- /** Post a request to the app. Prefer the typed methods; this is the escape hatch. */
203
- send(request: AppRequest): void;
204
- /** Subscribe to a response type. Returns a handle with `unsubscribe()` (also callable). */
205
- on<T extends AppResponseType>(type: T, listener: AppResponseListener<T>): Unsubscribe;
206
- /** Tell the app the page is ready; it hides its loading spinner. */
207
- notifyReady(): void;
208
- /** Show a user-facing error in the app. */
209
- notifyError(message: string): void;
210
- /** Ask the app to close the page. */
211
- navigateBack(): void;
212
- /** Resolves with the magic token the app mints for the current user. */
213
- requestAuthToken(): Promise<string>;
214
- /**
215
- * Opens the app's badge scanner and resolves once the session ends with every
216
- * person it captured, in scan order — several for a group scan, none if the user
217
- * cancelled. Rejects with a {@link ScannerError} if the app reports a failure.
218
- *
219
- * A badge whose lookup returned nothing is still included, with its `badgeId` and
220
- * empty `fields`, so it can be linked to the attendee.
221
- */
222
- openScanner(): Promise<ScannedPerson[]>;
223
- /** Remove the `message` listener and drop every subscription. Safe to call more than once. */
224
- destroy(): void;
225
- private handleMessage;
226
- }
227
-
228
- export { type AppRequest, AppRequestType, type AppResponse, type AppResponseListener, type AppResponseMap, AppResponseType, type AuthTokenMessage, CaptelloAppHost, type CaptelloAppHostOptions, type ScannedPerson, type ScannerClosedMessage, ScannerError, type ScannerResultMessage, parseAppResponse };
package/dist/app-host.js DELETED
@@ -1,170 +0,0 @@
1
- // src/app-host.ts
2
- var AppRequestType = /* @__PURE__ */ ((AppRequestType2) => {
3
- AppRequestType2["AppReady"] = "APP_READY";
4
- AppRequestType2["Error"] = "ERROR";
5
- AppRequestType2["NavigateBack"] = "NAVIGATE_BACK";
6
- AppRequestType2["RequestAuthToken"] = "REQUEST_AUTH_TOKEN";
7
- AppRequestType2["OpenScanner"] = "OPEN_ULC_FORM_SCANNER";
8
- AppRequestType2["OpenUrl"] = "OPEN_URL";
9
- AppRequestType2["CopyText"] = "COPY_TEXT";
10
- AppRequestType2["ShareUrl"] = "SHARE_URL";
11
- AppRequestType2["SaveVCard"] = "SAVE_VCARD";
12
- AppRequestType2["AddToWallet"] = "ADD_TO_WALLET";
13
- AppRequestType2["SyncApp"] = "SYNC_APP";
14
- return AppRequestType2;
15
- })(AppRequestType || {});
16
- var AppResponseType = /* @__PURE__ */ ((AppResponseType2) => {
17
- AppResponseType2["AuthToken"] = "AUTH_TOKEN";
18
- AppResponseType2["ScannerResult"] = "ULC_FORM_SCANNER_RESULT";
19
- AppResponseType2["ScannerClosed"] = "ULC_FORM_SCANNER_CLOSED";
20
- AppResponseType2["CopiedSuccess"] = "COPIED_SUCCESS";
21
- AppResponseType2["CopiedError"] = "COPIED_ERROR";
22
- AppResponseType2["SaveVCardSuccess"] = "SAVE_VCARD_SUCCESS";
23
- AppResponseType2["SaveVCardError"] = "SAVE_VCARD_ERROR";
24
- AppResponseType2["AddToWalletSuccess"] = "ADD_TO_WALLET_SUCCESS";
25
- AppResponseType2["AddToWalletError"] = "ADD_TO_WALLET_ERROR";
26
- return AppResponseType2;
27
- })(AppResponseType || {});
28
- var ScannerError = class extends Error {
29
- constructor(message) {
30
- super(message);
31
- this.name = "ScannerError";
32
- }
33
- };
34
- var RESPONSE_TYPES = new Set(Object.values(AppResponseType));
35
- function isPlainObject(value) {
36
- return typeof value === "object" && value !== null && !Array.isArray(value);
37
- }
38
- function parseAppResponse(data) {
39
- let value = data;
40
- if (typeof value === "string") {
41
- try {
42
- value = JSON.parse(value);
43
- } catch {
44
- return null;
45
- }
46
- }
47
- if (!isPlainObject(value)) return null;
48
- if (typeof value["type"] !== "string" || !RESPONSE_TYPES.has(value["type"])) return null;
49
- return value;
50
- }
51
- function makeSubscription(off) {
52
- const handle = () => {
53
- off();
54
- };
55
- handle.unsubscribe = off;
56
- return handle;
57
- }
58
- var CaptelloAppHost = class {
59
- constructor(options = {}) {
60
- this.listeners = /* @__PURE__ */ new Map();
61
- this.boundHandler = (event) => this.handleMessage(event);
62
- this.destroyed = false;
63
- const pageWindow = options.pageWindow ?? (typeof window !== "undefined" ? window : void 0);
64
- if (!pageWindow) {
65
- throw new Error(
66
- "CaptelloAppHost: no page window available. Pass `pageWindow` when constructing outside a browser."
67
- );
68
- }
69
- this.pageWindow = pageWindow;
70
- this.appWindow = options.appWindow ?? pageWindow.parent;
71
- this.targetOrigin = options.targetOrigin ?? "*";
72
- this.pageWindow.addEventListener("message", this.boundHandler);
73
- }
74
- /** Post a request to the app. Prefer the typed methods; this is the escape hatch. */
75
- send(request) {
76
- if (this.destroyed) return;
77
- this.appWindow.postMessage(request, this.targetOrigin);
78
- }
79
- /** Subscribe to a response type. Returns a handle with `unsubscribe()` (also callable). */
80
- on(type, listener) {
81
- let set = this.listeners.get(type);
82
- if (!set) {
83
- set = /* @__PURE__ */ new Set();
84
- this.listeners.set(type, set);
85
- }
86
- set.add(listener);
87
- return makeSubscription(() => {
88
- set?.delete(listener);
89
- });
90
- }
91
- /** Tell the app the page is ready; it hides its loading spinner. */
92
- notifyReady() {
93
- this.send({ type: "APP_READY" /* AppReady */ });
94
- }
95
- /** Show a user-facing error in the app. */
96
- notifyError(message) {
97
- this.send({ type: "ERROR" /* Error */, message });
98
- }
99
- /** Ask the app to close the page. */
100
- navigateBack() {
101
- this.send({ type: "NAVIGATE_BACK" /* NavigateBack */ });
102
- }
103
- /** Resolves with the magic token the app mints for the current user. */
104
- requestAuthToken() {
105
- return new Promise((resolve) => {
106
- const off = this.on("AUTH_TOKEN" /* AuthToken */, (message) => {
107
- off();
108
- resolve(message.token);
109
- });
110
- this.send({ type: "REQUEST_AUTH_TOKEN" /* RequestAuthToken */ });
111
- });
112
- }
113
- /**
114
- * Opens the app's badge scanner and resolves once the session ends with every
115
- * person it captured, in scan order — several for a group scan, none if the user
116
- * cancelled. Rejects with a {@link ScannerError} if the app reports a failure.
117
- *
118
- * A badge whose lookup returned nothing is still included, with its `badgeId` and
119
- * empty `fields`, so it can be linked to the attendee.
120
- */
121
- openScanner() {
122
- return new Promise((resolve, reject) => {
123
- const people = [];
124
- const done = () => {
125
- offResult();
126
- offClosed();
127
- };
128
- const offResult = this.on("ULC_FORM_SCANNER_RESULT" /* ScannerResult */, (message) => {
129
- if (!Array.isArray(message.result)) {
130
- done();
131
- reject(new ScannerError(message.message || "Scan failed."));
132
- return;
133
- }
134
- people.push({
135
- badgeId: typeof message.badgeId === "string" ? message.badgeId : "",
136
- fields: message.result
137
- });
138
- if (!("badgeId" in message)) {
139
- done();
140
- resolve(people);
141
- }
142
- });
143
- const offClosed = this.on("ULC_FORM_SCANNER_CLOSED" /* ScannerClosed */, () => {
144
- done();
145
- resolve(people);
146
- });
147
- this.send({ type: "OPEN_ULC_FORM_SCANNER" /* OpenScanner */ });
148
- });
149
- }
150
- /** Remove the `message` listener and drop every subscription. Safe to call more than once. */
151
- destroy() {
152
- if (this.destroyed) return;
153
- this.destroyed = true;
154
- this.pageWindow.removeEventListener("message", this.boundHandler);
155
- this.listeners.clear();
156
- }
157
- handleMessage(event) {
158
- const message = parseAppResponse(event.data);
159
- if (!message) return;
160
- const set = this.listeners.get(message.type);
161
- if (!set) return;
162
- for (const listener of Array.from(set)) {
163
- listener(message);
164
- }
165
- }
166
- };
167
-
168
- export { AppRequestType, AppResponseType, CaptelloAppHost, ScannerError, parseAppResponse };
169
- //# sourceMappingURL=app-host.js.map
170
- //# sourceMappingURL=app-host.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/app-host.ts"],"names":["AppRequestType","AppResponseType"],"mappings":"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","file":"app-host.js","sourcesContent":["/**\n * Pages embedded inside the Captello mobile app — `@captello/ulc-webview-sdk/app`.\n *\n * The mobile app hosts web pages (the meeting platform, Connexions, …) in an iframe and\n * offers them native services over `postMessage`: an auth token, the badge scanner,\n * back navigation, sharing. This module is the page-side client for that channel.\n *\n * It is the mirror image of {@link CaptelloWebview}: there your page is the host and\n * the Captello capture webview is the child; here the Captello *app* is the host and\n * your page is the child. The wire format differs too — match it exactly:\n * - Messages are posted as **plain objects**, not JSON strings.\n * - `type` values are SCREAMING_CASE.\n * - Requests (page → app) and responses (app → page) are disjoint enums.\n *\n * @example\n * import { CaptelloAppHost } from \"@captello/ulc-webview-sdk/app\";\n *\n * const app = new CaptelloAppHost();\n * const token = await app.requestAuthToken();\n * app.notifyReady();\n * const [person] = await app.openScanner();\n */\n\nimport type { Unsubscribe } from \"./client\";\nimport type { PrefillInfoItem } from \"./messages\";\n\n/* ------------------------------------------------------------------ *\n * Requests: page → app\n * ------------------------------------------------------------------ */\n\n/** Message `type` values a page sends to the Captello app. */\nexport enum AppRequestType {\n /** The page finished loading; the app hides its loading spinner. */\n AppReady = \"APP_READY\",\n /** A user-facing error; the app shows `message`. */\n Error = \"ERROR\",\n /** Close the page (the app dismisses the modal or pops the route). */\n NavigateBack = \"NAVIGATE_BACK\",\n /** Ask for a magic token; answered with {@link AppResponseType.AuthToken}. */\n RequestAuthToken = \"REQUEST_AUTH_TOKEN\",\n /**\n * Open the app's badge scanner. Answered with one\n * {@link AppResponseType.ScannerResult} per scanned person, then\n * {@link AppResponseType.ScannerClosed}.\n */\n OpenScanner = \"OPEN_ULC_FORM_SCANNER\",\n /** Open `url` in the system browser. */\n OpenUrl = \"OPEN_URL\",\n /** Copy `text` to the clipboard; acknowledged with `COPIED_SUCCESS` / `COPIED_ERROR`. */\n CopyText = \"COPY_TEXT\",\n /** Open the native share sheet. */\n ShareUrl = \"SHARE_URL\",\n /** Save a vCard to the contacts; acknowledged with `SAVE_VCARD_SUCCESS` / `SAVE_VCARD_ERROR`. */\n SaveVCard = \"SAVE_VCARD\",\n /** Add a wallet pass; acknowledged with `ADD_TO_WALLET_SUCCESS` / `ADD_TO_WALLET_ERROR`. */\n AddToWallet = \"ADD_TO_WALLET\",\n /** Ask the app to synchronize its data. */\n SyncApp = \"SYNC_APP\",\n}\n\ninterface AppReadyRequest {\n type: AppRequestType.AppReady;\n}\ninterface ErrorRequest {\n type: AppRequestType.Error;\n message: string;\n}\ninterface NavigateBackRequest {\n type: AppRequestType.NavigateBack;\n}\ninterface RequestAuthTokenRequest {\n type: AppRequestType.RequestAuthToken;\n}\ninterface OpenScannerRequest {\n type: AppRequestType.OpenScanner;\n}\ninterface OpenUrlRequest {\n type: AppRequestType.OpenUrl;\n url: string;\n}\ninterface CopyTextRequest {\n type: AppRequestType.CopyText;\n text: string;\n}\ninterface ShareUrlRequest {\n type: AppRequestType.ShareUrl;\n title: string;\n text: string;\n}\ninterface SaveVCardRequest {\n type: AppRequestType.SaveVCard;\n /** vCard text. */\n text: string;\n}\ninterface AddToWalletRequest {\n type: AppRequestType.AddToWallet;\n /** Base64 pass data, with or without a `data:` prefix. */\n text: string;\n}\ninterface SyncAppRequest {\n type: AppRequestType.SyncApp;\n}\n\n/** Discriminated union of every message a page can send to the app. */\nexport type AppRequest =\n | AppReadyRequest\n | ErrorRequest\n | NavigateBackRequest\n | RequestAuthTokenRequest\n | OpenScannerRequest\n | OpenUrlRequest\n | CopyTextRequest\n | ShareUrlRequest\n | SaveVCardRequest\n | AddToWalletRequest\n | SyncAppRequest;\n\n/* ------------------------------------------------------------------ *\n * Responses: app → page\n * ------------------------------------------------------------------ */\n\n/** Message `type` values the Captello app sends to a page. */\nexport enum AppResponseType {\n /** Answer to {@link AppRequestType.RequestAuthToken}. */\n AuthToken = \"AUTH_TOKEN\",\n /** One scanned person (or a scanner failure) after {@link AppRequestType.OpenScanner}. */\n ScannerResult = \"ULC_FORM_SCANNER_RESULT\",\n /** The scanner session ended: every result was posted, or the user cancelled. */\n ScannerClosed = \"ULC_FORM_SCANNER_CLOSED\",\n CopiedSuccess = \"COPIED_SUCCESS\",\n CopiedError = \"COPIED_ERROR\",\n SaveVCardSuccess = \"SAVE_VCARD_SUCCESS\",\n SaveVCardError = \"SAVE_VCARD_ERROR\",\n AddToWalletSuccess = \"ADD_TO_WALLET_SUCCESS\",\n AddToWalletError = \"ADD_TO_WALLET_ERROR\",\n}\n\n/** App → page: the magic token requested with {@link AppRequestType.RequestAuthToken}. */\nexport interface AuthTokenMessage {\n type: AppResponseType.AuthToken;\n token: string;\n}\n\n/**\n * App → page: one scanned person, or a scanner failure.\n *\n * The app posts one of these per person the scanner captured — several for a group\n * scan — then a {@link ScannerClosedMessage}. `result` holds the looked-up attendee\n * fields in the same shape {@link PrefillInfoItem} uses, so they can be fed straight to\n * a capture form's `prefill({ info })`. It is empty (not absent) when the badge had no\n * lookup data; the badge can still be linked via `badgeId`.\n *\n * On failure `result` is absent and `message` carries the error text.\n */\nexport interface ScannerResultMessage {\n type: AppResponseType.ScannerResult;\n /**\n * Badge ID the person was scanned from. Empty for business cards and manual search.\n * Absent altogether on app builds that predate multi-person sessions — those post a\n * single result and never a {@link ScannerClosedMessage}.\n */\n badgeId?: string;\n /** Looked-up attendee fields (success). */\n result?: PrefillInfoItem[];\n /** Error text (failure) — `result` is absent. */\n message?: string;\n}\n\n/** App → page: the scanner session ended. Follows the last {@link ScannerResultMessage}. */\nexport interface ScannerClosedMessage {\n type: AppResponseType.ScannerClosed;\n}\n\ninterface AckMessage<T extends AppResponseType> {\n type: T;\n}\n\n/** Discriminated union of every message the app can send to a page. */\nexport type AppResponse =\n | AuthTokenMessage\n | ScannerResultMessage\n | ScannerClosedMessage\n | AckMessage<AppResponseType.CopiedSuccess>\n | AckMessage<AppResponseType.CopiedError>\n | AckMessage<AppResponseType.SaveVCardSuccess>\n | AckMessage<AppResponseType.SaveVCardError>\n | AckMessage<AppResponseType.AddToWalletSuccess>\n | AckMessage<AppResponseType.AddToWalletError>;\n\n/** Maps each response `type` to its full message shape (used by {@link CaptelloAppHost.on}). */\nexport type AppResponseMap = {\n [M in AppResponse as M[\"type\"]]: M;\n};\n\n/** One person captured by the app's scanner, as resolved by {@link CaptelloAppHost.openScanner}. */\nexport interface ScannedPerson {\n /** Badge ID the person was scanned from; empty for business cards and manual search. */\n badgeId: string;\n /** Looked-up attendee fields; empty when the badge had no lookup data. */\n fields: PrefillInfoItem[];\n}\n\n/** Rejection reason from {@link CaptelloAppHost.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\nconst RESPONSE_TYPES: ReadonlySet<string> = new Set(Object.values(AppResponseType));\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Parses a raw `MessageEvent.data` value into a typed {@link AppResponse}, or returns\n * `null` if it is not a recognized Captello app message.\n *\n * The app posts plain objects; a JSON string is accepted too for robustness.\n */\nexport function parseAppResponse(data: unknown): AppResponse | null {\n let value: unknown = data;\n if (typeof value === \"string\") {\n try {\n value = JSON.parse(value);\n } catch {\n return null;\n }\n }\n if (!isPlainObject(value)) return null;\n if (typeof value[\"type\"] !== \"string\" || !RESPONSE_TYPES.has(value[\"type\"])) return null;\n return value as unknown as AppResponse;\n}\n\n/* ------------------------------------------------------------------ *\n * Client\n * ------------------------------------------------------------------ */\n\n/** Listener for a specific app response type. */\nexport type AppResponseListener<T extends AppResponseType> = (message: AppResponseMap[T]) => void;\n\nexport interface CaptelloAppHostOptions {\n /** Window the app is listening on. Defaults to `window.parent`. */\n appWindow?: Window;\n /** Window to receive the app's responses on. Defaults to `window`. */\n pageWindow?: Window;\n /**\n * `targetOrigin` for outgoing `postMessage` calls. Defaults to `\"*\"`: the app's\n * webview origin differs per platform (`capacitor://localhost`, `http://localhost`),\n * and the page only ever runs inside the app when it uses this channel.\n */\n targetOrigin?: string;\n}\n\nfunction makeSubscription(off: () => void): Unsubscribe {\n const handle = (() => {\n off();\n }) as Unsubscribe;\n handle.unsubscribe = off;\n return handle;\n}\n\n/**\n * Page-side client for the Captello mobile app that embeds the page.\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 CaptelloAppHost {\n private readonly appWindow: Window;\n private readonly pageWindow: Window;\n private readonly targetOrigin: string;\n private readonly listeners = new Map<AppResponseType, Set<AppResponseListener<AppResponseType>>>();\n private readonly boundHandler = (event: MessageEvent) => this.handleMessage(event);\n private destroyed = false;\n\n constructor(options: CaptelloAppHostOptions = {}) {\n const pageWindow = options.pageWindow ?? (typeof window !== \"undefined\" ? window : undefined);\n if (!pageWindow) {\n throw new Error(\n \"CaptelloAppHost: no page window available. Pass `pageWindow` when constructing outside a browser.\",\n );\n }\n this.pageWindow = pageWindow;\n this.appWindow = options.appWindow ?? pageWindow.parent;\n this.targetOrigin = options.targetOrigin ?? \"*\";\n this.pageWindow.addEventListener(\"message\", this.boundHandler);\n }\n\n /** Post a request to the app. Prefer the typed methods; this is the escape hatch. */\n send(request: AppRequest): void {\n if (this.destroyed) return;\n this.appWindow.postMessage(request, this.targetOrigin);\n }\n\n /** Subscribe to a response type. Returns a handle with `unsubscribe()` (also callable). */\n on<T extends AppResponseType>(type: T, listener: AppResponseListener<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 AppResponseListener<AppResponseType>);\n return makeSubscription(() => {\n set?.delete(listener as AppResponseListener<AppResponseType>);\n });\n }\n\n /** Tell the app the page is ready; it hides its loading spinner. */\n notifyReady(): void {\n this.send({ type: AppRequestType.AppReady });\n }\n\n /** Show a user-facing error in the app. */\n notifyError(message: string): void {\n this.send({ type: AppRequestType.Error, message });\n }\n\n /** Ask the app to close the page. */\n navigateBack(): void {\n this.send({ type: AppRequestType.NavigateBack });\n }\n\n /** Resolves with the magic token the app mints for the current user. */\n requestAuthToken(): Promise<string> {\n return new Promise((resolve) => {\n const off = this.on(AppResponseType.AuthToken, (message) => {\n off();\n resolve(message.token);\n });\n this.send({ type: AppRequestType.RequestAuthToken });\n });\n }\n\n /**\n * Opens the 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(AppResponseType.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(AppResponseType.ScannerClosed, () => {\n done();\n resolve(people);\n });\n this.send({ type: AppRequestType.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.pageWindow.removeEventListener(\"message\", this.boundHandler);\n this.listeners.clear();\n }\n\n private handleMessage(event: MessageEvent): void {\n const message = parseAppResponse(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"]}