@lorikeetai/react-native-sdk 1.2.0 → 2.0.0-alpha.2

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/dist/index.js CHANGED
@@ -1,224 +1,855 @@
1
- import { forwardRef, useState, useRef, useEffect } from 'react';
2
- import { StyleSheet, Linking, View, Text, TouchableOpacity } from 'react-native';
3
- import { SafeAreaView } from 'react-native-safe-area-context';
4
- import { WebView } from 'react-native-webview';
5
- import Svg, { Path, Line } from 'react-native-svg';
6
- import { jsx, jsxs } from 'react/jsx-runtime';
1
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
2
+ import { AppState, Linking, StyleSheet, Text, TouchableOpacity, View } from "react-native";
3
+ import { SafeAreaView } from "react-native-safe-area-context";
4
+ import { WebView } from "react-native-webview";
5
+ import Svg, { Line, Path } from "react-native-svg";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
7
 
8
- var __defProp = Object.defineProperty;
9
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
10
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
8
+ //#region src/bridge.ts
9
+ /**
10
+ * The public key, base URL and token are one unit, and a conversation belongs to
11
+ * the credentials that created it. Stamping an observation with them is what
12
+ * stops a remount resuming one environment's ticket against another's key.
13
+ */
14
+ const conversationIdentity = ({ authToken, baseUrl, publicKey }) => `${publicKey} ${baseUrl !== null && baseUrl !== void 0 ? baseUrl : ""} ${authToken}`;
15
+ /**
16
+ * A remount has to resume the conversation it was in, or the recovery abandons
17
+ * the ticket it was meant to rescue. Only usable when the credentials still
18
+ * match the ones that produced it.
19
+ */
20
+ const resumableConversation = (observed, identity, intendedTicketId) => {
21
+ if (observed === null || observed.identity !== identity || observed.intendedTicketId !== intendedTicketId || observed.ticketId === void 0) return null;
22
+ return {
23
+ session: observed.session,
24
+ ticketId: observed.ticketId
25
+ };
26
+ };
27
+
28
+ //#endregion
29
+ //#region src/encode.ts
30
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
31
+ /**
32
+ * Base64 of a string's UTF-8 bytes.
33
+ *
34
+ * The chat page decodes this as UTF-8 (`Buffer.from(value, 'base64')`), so
35
+ * UTF-8 is the contract. The global `btoa` cannot meet it: it accepts Latin-1
36
+ * only, so a typographic apostrophe or an emoji in a title throws
37
+ * "Found invalid character when converting to base64" and takes the render with
38
+ * it. Hermes also had no `btoa` at all before React Native 0.74, which is
39
+ * inside this package's supported peer range.
40
+ */
41
+ function base64EncodeUtf8(value) {
42
+ const bytes = encodeURIComponent(value).replace(/%([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
43
+ let encoded = "";
44
+ for (let index = 0; index < bytes.length; index += 3) {
45
+ const first = bytes.charCodeAt(index);
46
+ const second = index + 1 < bytes.length ? bytes.charCodeAt(index + 1) : void 0;
47
+ const third = index + 2 < bytes.length ? bytes.charCodeAt(index + 2) : void 0;
48
+ encoded += BASE64_ALPHABET[first >> 2];
49
+ encoded += BASE64_ALPHABET[(first & 3) << 4 | (second !== null && second !== void 0 ? second : 0) >> 4];
50
+ encoded += second === void 0 ? "=" : BASE64_ALPHABET[(second & 15) << 2 | (third !== null && third !== void 0 ? third : 0) >> 6];
51
+ encoded += third === void 0 ? "=" : BASE64_ALPHABET[third & 63];
52
+ }
53
+ return encoded;
54
+ }
55
+
56
+ //#endregion
57
+ //#region \0@oxc-project+runtime@0.147.0/helpers/esm/typeof.js
58
+ function _typeof(o) {
59
+ "@babel/helpers - typeof";
60
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
61
+ return typeof o;
62
+ } : function(o) {
63
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
64
+ }, _typeof(o);
65
+ }
66
+
67
+ //#endregion
68
+ //#region \0@oxc-project+runtime@0.147.0/helpers/esm/toPrimitive.js
69
+ function toPrimitive(t, r) {
70
+ if ("object" != _typeof(t) || !t) return t;
71
+ var e = t[Symbol.toPrimitive];
72
+ if (void 0 !== e) {
73
+ var i = e.call(t, r || "default");
74
+ if ("object" != _typeof(i)) return i;
75
+ throw new TypeError("@@toPrimitive must return a primitive value.");
76
+ }
77
+ return ("string" === r ? String : Number)(t);
78
+ }
11
79
 
12
- // src/client.ts
80
+ //#endregion
81
+ //#region \0@oxc-project+runtime@0.147.0/helpers/esm/toPropertyKey.js
82
+ function toPropertyKey(t) {
83
+ var i = toPrimitive(t, "string");
84
+ return "symbol" == _typeof(i) ? i : i + "";
85
+ }
86
+
87
+ //#endregion
88
+ //#region \0@oxc-project+runtime@0.147.0/helpers/esm/defineProperty.js
89
+ function _defineProperty(e, r, t) {
90
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
91
+ value: t,
92
+ enumerable: !0,
93
+ configurable: !0,
94
+ writable: !0
95
+ }) : e[r] = t, e;
96
+ }
97
+
98
+ //#endregion
99
+ //#region src/client.ts
13
100
  var LorikeetClient = class {
14
- constructor({ customisations, api_base, public_key }) {
15
- __publicField(this, "baseUrl", "https://chat.lorikeetcx.ai");
16
- // Fallback URL
17
- __publicField(this, "customisations", null);
18
- __publicField(this, "publicKey");
19
- __publicField(this, "getWebviewUrl", ({
20
- customer_email,
21
- customer_name,
22
- token,
23
- ticket_id
24
- }) => {
25
- const url = new URL(this.baseUrl);
26
- url.pathname = ticket_id ? `/tickets/${this.publicKey}/${ticket_id}` : "/widget";
27
- url.searchParams.append("public_key", this.publicKey);
28
- if (customer_email) {
29
- url.searchParams.append("customer_email", customer_email);
30
- }
31
- if (customer_name) {
32
- url.searchParams.append("customer_name", customer_name);
33
- }
34
- url.searchParams.append("token", token);
35
- if (this.customisations) {
36
- url.searchParams.append(
37
- "customisations",
38
- btoa(JSON.stringify(this.customisations))
39
- );
40
- }
41
- return url.toString();
42
- });
43
- if (api_base) {
44
- this.baseUrl = api_base;
45
- }
46
- this.publicKey = public_key;
47
- if (customisations) {
48
- this.customisations = customisations;
49
- }
50
- }
101
+ constructor({ customisations, api_base, metadata, public_key }) {
102
+ _defineProperty(this, "baseUrl", "https://chat.lorikeetcx.ai");
103
+ _defineProperty(this, "customisations", null);
104
+ _defineProperty(this, "metadata", null);
105
+ _defineProperty(this, "publicKey", void 0);
106
+ _defineProperty(this, "getWebviewUrl", ({ customer_email, customer_name, token, ticket_id, session }) => {
107
+ const url = new URL(this.baseUrl);
108
+ url.pathname = ticket_id ? `/tickets/${this.publicKey}/${ticket_id}` : "/widget";
109
+ url.searchParams.append("public_key", this.publicKey);
110
+ if (customer_email) url.searchParams.append("customer_email", customer_email);
111
+ if (customer_name) url.searchParams.append("customer_name", customer_name);
112
+ url.searchParams.append("token", token);
113
+ if (ticket_id && session) url.searchParams.append("session", session);
114
+ if (this.customisations) url.searchParams.append("customisations", base64EncodeUtf8(JSON.stringify(this.customisations)));
115
+ if (this.metadata) url.searchParams.append("metadata", JSON.stringify(this.metadata));
116
+ return url.toString();
117
+ });
118
+ _defineProperty(
119
+ this,
120
+ /**
121
+ * The page that clears this key's credentials from the WebView: its cookies,
122
+ * server-side, and the storage the frame keeps. Navigating a WebView here is
123
+ * the only way to reach either, so a host that only drops its own copy of
124
+ * `ticketId` and `session` leaves the next person on the device able to
125
+ * reopen the conversation.
126
+ *
127
+ * The page confirms with a `loggedOut` message, which the SDK reports as
128
+ * `logged_out`. The load event does not: an error page fires that too.
129
+ */
130
+ "getLogoutUrl",
131
+ () => {
132
+ const url = new URL(this.baseUrl);
133
+ url.pathname = "/logout";
134
+ url.searchParams.append("public_key", this.publicKey);
135
+ return url.toString();
136
+ }
137
+ );
138
+ if (api_base) this.baseUrl = api_base;
139
+ this.publicKey = public_key;
140
+ if (customisations) this.customisations = customisations;
141
+ if (metadata) this.metadata = metadata;
142
+ }
51
143
  };
52
- var MessageCirclePlusIcon = ({
53
- size = 24,
54
- color = "currentColor",
55
- strokeWidth = 2
56
- }) => {
57
- return /* @__PURE__ */ jsxs(Svg, { fill: "none", height: size, viewBox: "0 0 24 24", width: size, children: [
58
- /* @__PURE__ */ jsx(
59
- Path,
60
- {
61
- d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z",
62
- stroke: color,
63
- strokeLinecap: "round",
64
- strokeLinejoin: "round",
65
- strokeWidth
66
- }
67
- ),
68
- /* @__PURE__ */ jsx(
69
- Line,
70
- {
71
- stroke: color,
72
- strokeLinecap: "round",
73
- strokeWidth,
74
- x1: "12",
75
- x2: "12",
76
- y1: "8",
77
- y2: "16"
78
- }
79
- ),
80
- /* @__PURE__ */ jsx(
81
- Line,
82
- {
83
- stroke: color,
84
- strokeLinecap: "round",
85
- strokeWidth,
86
- x1: "8",
87
- x2: "16",
88
- y1: "12",
89
- y2: "12"
90
- }
91
- )
92
- ] });
144
+
145
+ //#endregion
146
+ //#region src/errors.ts
147
+ /**
148
+ * Codes the chat page posts to the host as `{ type: 'error', error: CODE }`.
149
+ * Mirrored here because the page publishes them nowhere else, so a host would
150
+ * otherwise be matching on strings it discovered by observation.
151
+ */
152
+ const LORIKEET_ERROR_CODES = [
153
+ "MISSING_TOKEN",
154
+ "CORRUPT_TOKEN_DATA",
155
+ "DECRYPT_FAILED",
156
+ "EXPIRED_TOKEN",
157
+ "MALFORMED_TOKEN",
158
+ "MISSING_PUBLIC_KEY",
159
+ "UNKNOWN_PUBLIC_KEY",
160
+ "CUSTOMER_ID_CONFLICT",
161
+ "CUSTOMER_ID_MISSING",
162
+ "FATAL_ERROR",
163
+ "INVALID_SESSION",
164
+ "DOMAIN_NOT_ALLOWED",
165
+ "TICKET_NOT_FOUND"
166
+ ];
167
+ /**
168
+ * The codes that mean the stored conversation cannot be reopened, as opposed to
169
+ * the trusted token being bad or the integration being misconfigured. Only
170
+ * these are worth abandoning a resume for: a token error fails again with the
171
+ * same token, and a configuration error fails again regardless.
172
+ */
173
+ const UNUSABLE_CONVERSATION_CODES = [
174
+ "CUSTOMER_ID_MISSING",
175
+ "INVALID_SESSION",
176
+ "TICKET_NOT_FOUND"
177
+ ];
178
+ /** Whether an error code means the stored conversation cannot be reopened. */
179
+ function isUnusableConversationCode(code) {
180
+ return UNUSABLE_CONVERSATION_CODES.includes(code);
181
+ }
182
+ function isUnusableConversation(message) {
183
+ if (typeof message !== "object" || message === null) return false;
184
+ const { type, error } = message;
185
+ return type === "error" && typeof error === "string" ? isUnusableConversationCode(error) : false;
186
+ }
187
+
188
+ //#endregion
189
+ //#region src/events.ts
190
+ const asRecord = (message) => typeof message === "object" && message !== null && !Array.isArray(message) ? message : null;
191
+ const asNonEmptyString = (value) => typeof value === "string" && value !== "" ? value : null;
192
+ /**
193
+ * The event a bridge message carries, or `null` for one that carries none.
194
+ *
195
+ * `ticket_created` is not derived here: telling a fresh ticket from a resumed
196
+ * one needs the URL the page was loaded with, which only the component holds.
197
+ */
198
+ const eventFromMessage = (message) => {
199
+ const record = asRecord(message);
200
+ if (record === null || typeof record.type !== "string") return null;
201
+ switch (record.type) {
202
+ case "ready": return { type: "ready" };
203
+ case "customerMessage": return { type: "customer_message" };
204
+ case "agentMessage": return typeof record.markUnread === "boolean" ? {
205
+ type: "agent_message",
206
+ markUnread: record.markUnread
207
+ } : { type: "agent_message" };
208
+ case "closeChat": {
209
+ const reason = asNonEmptyString(record.reason);
210
+ return reason === "ended_by_customer" || reason === "minimised" ? {
211
+ type: "close",
212
+ reason
213
+ } : { type: "close" };
214
+ }
215
+ case "conversationEnded": return { type: "conversation_ended" };
216
+ case "startNewChat": return { type: "new_chat" };
217
+ case "persistLorikeetTicketId": {
218
+ const ticketId = asNonEmptyString(record.ticketId);
219
+ return ticketId === null ? null : {
220
+ type: "persist_ticket_id",
221
+ ticketId
222
+ };
223
+ }
224
+ case "persistLorikeetSession": {
225
+ const session = asNonEmptyString(record.session);
226
+ return session === null ? null : {
227
+ type: "persist_session",
228
+ session
229
+ };
230
+ }
231
+ case "loggedOut": return { type: "logged_out" };
232
+ case "error": {
233
+ const error = asNonEmptyString(record.error);
234
+ return error === null ? null : {
235
+ type: "error",
236
+ error
237
+ };
238
+ }
239
+ case "preloadComplete": return {
240
+ type: "preload_complete",
241
+ clientStart: record.clientStart === true,
242
+ lazyTicketCreationEnabled: record.lazyTicketCreationEnabled === true
243
+ };
244
+ default: return null;
245
+ }
246
+ };
247
+ /**
248
+ * The ticket a URL is trying to resume, or `null` for one that creates its own.
249
+ *
250
+ * Read from the URL rather than from any state, so it holds for a host that
251
+ * built the URL itself. An unreadable URL reports nothing, which at worst
252
+ * announces a resumed ticket as created once.
253
+ */
254
+ const resumedTicketIdFromUri = (uri) => {
255
+ var _segments$;
256
+ let pathname;
257
+ try {
258
+ pathname = new URL(uri).pathname;
259
+ } catch {
260
+ return null;
261
+ }
262
+ const segments = pathname.split("/").filter((part) => part !== "");
263
+ if (segments.length < 3 || segments[0] !== "tickets") return null;
264
+ return (_segments$ = segments[2]) !== null && _segments$ !== void 0 ? _segments$ : null;
265
+ };
266
+
267
+ //#endregion
268
+ //#region src/lifecycle.ts
269
+ /**
270
+ * The page gives its message stream ten attempts with exponential backoff
271
+ * capped at 30s (`message-stream-utils.tsx`), so the budget runs out roughly
272
+ * 151s after the stream drops, or ~196s once jitter is counted. It then sits at
273
+ * `failed` with nothing to reset it, so a customer who comes back to a
274
+ * backgrounded app is looking at a stream that will never reconnect.
275
+ *
276
+ * Three minutes sits inside that band. Shorter would remount while the page's
277
+ * own retries could still win, which costs the customer their draft message for
278
+ * nothing; much longer leaves a dead stream on screen.
279
+ */
280
+ const BACKGROUND_RELOAD_THRESHOLD_MS = 18e4;
281
+ /**
282
+ * How long `/logout` gets to confirm it cleared the frame before the host is
283
+ * told it did not. Matches the web SDK's own budget, and like it, a logout that
284
+ * misses the deadline leaves the widget unavailable rather than letting the next
285
+ * customer start a chat on a device the previous one's credentials still open.
286
+ */
287
+ const LOGOUT_CONFIRMATION_TIMEOUT_MS = 1e4;
288
+ /**
289
+ * Elapsed time is the only signal available: the bridge reports `ready`,
290
+ * `error` and the persistence messages, but never stream health, so the SDK
291
+ * cannot ask whether the connection actually survived.
292
+ */
293
+ const shouldReloadAfterBackground = (backgroundedAt, now, thresholdMs = BACKGROUND_RELOAD_THRESHOLD_MS) => {
294
+ if (backgroundedAt === null) return false;
295
+ const elapsed = now - backgroundedAt;
296
+ if (elapsed < 0) return false;
297
+ return elapsed >= thresholdMs;
298
+ };
299
+
300
+ //#endregion
301
+ //#region src/links.ts
302
+ /**
303
+ * Schemes the WebView resolves for itself. None of them is a page the customer
304
+ * asked to leave the app for, and the OS cannot open them anyway.
305
+ */
306
+ const WEBVIEW_OWNED_SCHEMES = /* @__PURE__ */ new Set([
307
+ "about:",
308
+ "blob:",
309
+ "data:",
310
+ "javascript:"
311
+ ]);
312
+ const parseUrl = (value) => {
313
+ try {
314
+ return new URL(value);
315
+ } catch {
316
+ return null;
317
+ }
318
+ };
319
+ /**
320
+ * Whether the OS could be asked to open this at all. `window.open()` with no
321
+ * argument gives `about:blank`, and a scheme the WebView resolves for itself is
322
+ * not something the OS can place.
323
+ */
324
+ const canOpenOutsideApp = (url) => {
325
+ const target = parseUrl(url);
326
+ return target !== null && !WEBVIEW_OWNED_SCHEMES.has(target.protocol);
93
327
  };
94
- var stylesheet = StyleSheet.create({
95
- root: { flex: 1, backgroundColor: "#fff" },
96
- container: {
97
- flex: 1,
98
- borderRadius: 8,
99
- overflow: "hidden",
100
- shadowColor: "#000",
101
- shadowOffset: { width: 0, height: 2 },
102
- shadowOpacity: 0.1,
103
- shadowRadius: 4,
104
- elevation: 3
105
- },
106
- header: {
107
- padding: 16,
108
- display: "flex",
109
- flexDirection: "row",
110
- justifyContent: "space-between"
111
- },
112
- headerIcon: {
113
- width: 24
114
- },
115
- headerText: {
116
- textAlign: "center",
117
- fontSize: 16
118
- },
119
- webView: {
120
- flex: 1
121
- }
328
+ /**
329
+ * Whether a navigation belongs inside the widget.
330
+ *
331
+ * An origin comparison rather than the substring match this replaces. `baseUrl`
332
+ * appearing anywhere in a URL counted as internal, so
333
+ * `https://evil.example/?next=https://chat.lorikeetcx.ai` loaded in place of the
334
+ * conversation instead of opening in the browser. Comparing origins rather than
335
+ * matching a prefix also means a port, a trailing slash and a different path all
336
+ * still resolve to the same widget.
337
+ */
338
+ const isInternalNavigation = (url, baseUrl) => {
339
+ const base = parseUrl(baseUrl);
340
+ const target = parseUrl(url);
341
+ if (base === null || target === null) return true;
342
+ if (WEBVIEW_OWNED_SCHEMES.has(target.protocol)) return true;
343
+ return target.origin === base.origin;
344
+ };
345
+
346
+ //#endregion
347
+ //#region src/message-circle-plus-icon.tsx
348
+ const MessageCirclePlusIcon = ({ size = 24, color = "currentColor", strokeWidth = 2 }) => {
349
+ return /* @__PURE__ */ jsxs(Svg, {
350
+ fill: "none",
351
+ height: size,
352
+ viewBox: "0 0 24 24",
353
+ width: size,
354
+ children: [
355
+ /* @__PURE__ */ jsx(Path, {
356
+ d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z",
357
+ stroke: color,
358
+ strokeLinecap: "round",
359
+ strokeLinejoin: "round",
360
+ strokeWidth
361
+ }),
362
+ /* @__PURE__ */ jsx(Line, {
363
+ stroke: color,
364
+ strokeLinecap: "round",
365
+ strokeWidth,
366
+ x1: "12",
367
+ x2: "12",
368
+ y1: "8",
369
+ y2: "16"
370
+ }),
371
+ /* @__PURE__ */ jsx(Line, {
372
+ stroke: color,
373
+ strokeLinecap: "round",
374
+ strokeWidth,
375
+ x1: "8",
376
+ x2: "16",
377
+ y1: "12",
378
+ y2: "12"
379
+ })
380
+ ]
381
+ });
382
+ };
383
+
384
+ //#endregion
385
+ //#region src/widget.tsx
386
+ const stylesheet = StyleSheet.create({
387
+ root: {
388
+ flex: 1,
389
+ backgroundColor: "#fff"
390
+ },
391
+ container: {
392
+ flex: 1,
393
+ borderRadius: 8,
394
+ overflow: "hidden",
395
+ shadowColor: "#000",
396
+ shadowOffset: {
397
+ width: 0,
398
+ height: 2
399
+ },
400
+ shadowOpacity: .1,
401
+ shadowRadius: 4,
402
+ elevation: 3
403
+ },
404
+ header: {
405
+ padding: 16,
406
+ display: "flex",
407
+ flexDirection: "row",
408
+ justifyContent: "space-between"
409
+ },
410
+ headerIcon: { width: 24 },
411
+ headerText: {
412
+ textAlign: "center",
413
+ fontSize: 16
414
+ },
415
+ closedControl: { padding: 16 },
416
+ webView: { flex: 1 },
417
+ closed: {
418
+ flex: 1,
419
+ opacity: 0
420
+ }
122
421
  });
123
- var LorikeetWidgetWebView = forwardRef(({ uri, baseUrl = "https://chat.lorikeetcx.ai", webviewProps = {} }, ref) => {
124
- return /* @__PURE__ */ jsx(
125
- WebView,
126
- {
127
- ...webviewProps,
128
- onNavigationStateChange: (navState) => {
129
- if (navState.url.includes(baseUrl)) {
130
- return false;
131
- } else {
132
- void Linking.openURL(navState.url);
133
- return false;
134
- }
135
- },
136
- ref,
137
- source: {
138
- uri
139
- },
140
- style: stylesheet.webView
141
- }
142
- );
422
+ const LorikeetWidgetWebView = forwardRef(({ uri, baseUrl = "https://chat.lorikeetcx.ai", webviewProps = {}, onEvent }, ref) => {
423
+ const isInternal = (url) => isInternalNavigation(url, baseUrl);
424
+ const accounted = useRef(/* @__PURE__ */ new Set());
425
+ const seenUri = useRef(null);
426
+ if (seenUri.current !== uri) {
427
+ seenUri.current = uri;
428
+ const resumed = resumedTicketIdFromUri(uri);
429
+ if (resumed !== null) accounted.current.add(resumed);
430
+ }
431
+ const emit = (event) => {
432
+ onEvent === null || onEvent === void 0 || onEvent(event);
433
+ if (event.type !== "persist_ticket_id") return;
434
+ if (accounted.current.has(event.ticketId)) return;
435
+ accounted.current.add(event.ticketId);
436
+ onEvent === null || onEvent === void 0 || onEvent({
437
+ ticketId: event.ticketId,
438
+ type: "ticket_created"
439
+ });
440
+ };
441
+ return /* @__PURE__ */ jsx(WebView, {
442
+ style: stylesheet.webView,
443
+ ...webviewProps,
444
+ onMessage: (event) => {
445
+ var _webviewProps$onMessa;
446
+ let message;
447
+ try {
448
+ message = JSON.parse(event.nativeEvent.data);
449
+ } catch {}
450
+ const widgetEvent = eventFromMessage(message);
451
+ if (widgetEvent !== null) emit(widgetEvent);
452
+ (_webviewProps$onMessa = webviewProps.onMessage) === null || _webviewProps$onMessa === void 0 || _webviewProps$onMessa.call(webviewProps, event);
453
+ },
454
+ onNavigationStateChange: (navState) => {
455
+ var _webviewProps$onNavig;
456
+ if (!isInternal(navState.url)) Linking.openURL(navState.url);
457
+ (_webviewProps$onNavig = webviewProps.onNavigationStateChange) === null || _webviewProps$onNavig === void 0 || _webviewProps$onNavig.call(webviewProps, navState);
458
+ },
459
+ onOpenWindow: (event) => {
460
+ var _webviewProps$onOpenW;
461
+ const { targetUrl } = event.nativeEvent;
462
+ if (canOpenOutsideApp(targetUrl)) Linking.openURL(targetUrl);
463
+ (_webviewProps$onOpenW = webviewProps.onOpenWindow) === null || _webviewProps$onOpenW === void 0 || _webviewProps$onOpenW.call(webviewProps, event);
464
+ },
465
+ onShouldStartLoadWithRequest: (request) => {
466
+ var _webviewProps$onShoul;
467
+ if (((_webviewProps$onShoul = webviewProps.onShouldStartLoadWithRequest) === null || _webviewProps$onShoul === void 0 ? void 0 : _webviewProps$onShoul.call(webviewProps, request)) === false) return false;
468
+ if (request.isTopFrame === false || isInternal(request.url)) return true;
469
+ Linking.openURL(request.url);
470
+ return false;
471
+ },
472
+ ref,
473
+ source: { uri }
474
+ });
143
475
  });
144
476
  LorikeetWidgetWebView.displayName = "LorikeetWidgetWebView";
145
- var LorikeetWidgetHeader = ({
146
- title,
147
- style,
148
- icon = /* @__PURE__ */ jsx(MessageCirclePlusIcon, { size: 16 }),
149
- onIconPress
150
- }) => {
151
- return /* @__PURE__ */ jsxs(View, { style: stylesheet.header, children: [
152
- /* @__PURE__ */ jsx(View, { style: stylesheet.headerIcon }),
153
- /* @__PURE__ */ jsx(Text, { style: [stylesheet.headerText, style], children: title }),
154
- /* @__PURE__ */ jsx(TouchableOpacity, { onPress: onIconPress, style: stylesheet.headerIcon, children: icon })
155
- ] });
477
+ const LorikeetWidgetHeader = ({ title, style, icon = /* @__PURE__ */ jsx(MessageCirclePlusIcon, { size: 16 }), onIconPress }) => {
478
+ return /* @__PURE__ */ jsxs(View, {
479
+ style: stylesheet.header,
480
+ children: [
481
+ /* @__PURE__ */ jsx(View, { style: stylesheet.headerIcon }),
482
+ /* @__PURE__ */ jsx(Text, {
483
+ style: [stylesheet.headerText, style],
484
+ children: title
485
+ }),
486
+ /* @__PURE__ */ jsx(TouchableOpacity, {
487
+ onPress: onIconPress,
488
+ style: stylesheet.headerIcon,
489
+ children: icon
490
+ })
491
+ ]
492
+ });
156
493
  };
157
- function LorikeetWidgetView({
158
- publicKey,
159
- baseUrl,
160
- authToken,
161
- customisations,
162
- styles,
163
- onWebViewMessage
164
- }) {
165
- var _a;
166
- const [lorikeetClient, setLorikeetClient] = useState(
167
- null
168
- );
169
- const [uri, setUri] = useState(null);
170
- const [timestamp, setTimestamp] = useState(Date.now());
171
- const webViewRef = useRef(null);
172
- useEffect(() => {
173
- const lorikeetClient2 = new LorikeetClient({
174
- public_key: publicKey,
175
- api_base: baseUrl,
176
- customisations: {
177
- ...customisations,
178
- hideHeader: (customisations == null ? void 0 : customisations.hideHeader) === void 0 ? true : customisations.hideHeader
179
- }
180
- });
181
- setLorikeetClient(lorikeetClient2);
182
- setUri(
183
- lorikeetClient2.getWebviewUrl({
184
- token: authToken
185
- })
186
- );
187
- }, [publicKey, baseUrl]);
188
- if (!lorikeetClient || !uri) {
189
- return null;
190
- }
191
- return /* @__PURE__ */ jsx(SafeAreaView, { edges: ["top", "left", "right"], style: stylesheet.root, children: /* @__PURE__ */ jsxs(View, { style: stylesheet.container, children: [
192
- /* @__PURE__ */ jsx(
193
- LorikeetWidgetHeader,
194
- {
195
- onIconPress: () => {
196
- setTimestamp(Date.now());
197
- },
198
- style: styles == null ? void 0 : styles.header,
199
- title: (_a = customisations == null ? void 0 : customisations.title) != null ? _a : "Lorikeet AI Agent"
200
- }
201
- ),
202
- /* @__PURE__ */ jsx(
203
- LorikeetWidgetWebView,
204
- {
205
- baseUrl: lorikeetClient.baseUrl,
206
- ref: webViewRef,
207
- uri,
208
- webviewProps: {
209
- onMessage: onWebViewMessage,
210
- allowsInlineMediaPlayback: true,
211
- mediaPlaybackRequiresUserAction: false,
212
- scalesPageToFit: true,
213
- startInLoadingState: true,
214
- style: stylesheet.webView
215
- }
216
- },
217
- timestamp
218
- )
219
- ] }) });
220
- }
494
+ const LorikeetWidgetView = forwardRef(function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, styles, onEvent, onWebViewMessage, ticketId, session, metadata, webviewProps }, ref) {
495
+ var _customisations$hideH, _customisations$title;
496
+ const [lorikeetClient, setLorikeetClient] = useState(null);
497
+ const [loaded, setLoaded] = useState(null);
498
+ const [generation, setGeneration] = useState(0);
499
+ const generationRef = useRef(generation);
500
+ generationRef.current = generation;
501
+ const remount = useCallback(() => {
502
+ setGeneration((previous) => previous + 1);
503
+ }, []);
504
+ const webViewRef = useRef(null);
505
+ const [refusedTicketId, setRefusedTicketId] = useState(null);
506
+ const observedRef = useRef(null);
507
+ const backgroundedAtRef = useRef(null);
508
+ const uriRef = useRef(null);
509
+ const loadedForRef = useRef({
510
+ identity: "",
511
+ intendedTicketId: void 0
512
+ });
513
+ const identity = conversationIdentity({
514
+ authToken,
515
+ baseUrl,
516
+ publicKey
517
+ });
518
+ const resumingTicketId = ticketId !== void 0 && ticketId !== refusedTicketId ? ticketId : void 0;
519
+ const [attemptedTicketId, setAttemptedTicketId] = useState(void 0);
520
+ const [closed, setClosed] = useState(null);
521
+ const [clearing, setClearing] = useState(null);
522
+ const [loggedOutIdentity, setLoggedOutIdentity] = useState(null);
523
+ const onEventRef = useRef(onEvent);
524
+ onEventRef.current = onEvent;
525
+ const hideHeader = (_customisations$hideH = customisations === null || customisations === void 0 ? void 0 : customisations.hideHeader) !== null && _customisations$hideH !== void 0 ? _customisations$hideH : true;
526
+ const load = useCallback((client, forIdentity, intendedTicketId, resume, force = false) => {
527
+ const next = client.getWebviewUrl({
528
+ token: authToken,
529
+ ticket_id: resume === null || resume === void 0 ? void 0 : resume.ticketId,
530
+ session: resume === null || resume === void 0 ? void 0 : resume.session
531
+ });
532
+ const unchanged = uriRef.current === next;
533
+ if (loadedForRef.current.identity !== forIdentity || loadedForRef.current.intendedTicketId !== intendedTicketId) setClosed(null);
534
+ uriRef.current = next;
535
+ loadedForRef.current = {
536
+ identity: forIdentity,
537
+ intendedTicketId
538
+ };
539
+ if (unchanged && !force) {
540
+ setLoaded((previous) => previous === null || previous.identity !== forIdentity || previous.intendedTicketId !== intendedTicketId ? {
541
+ identity: forIdentity,
542
+ intendedTicketId,
543
+ uri: next
544
+ } : previous);
545
+ return;
546
+ }
547
+ setLoaded({
548
+ identity: forIdentity,
549
+ intendedTicketId,
550
+ uri: next
551
+ });
552
+ setAttemptedTicketId(resume === null || resume === void 0 ? void 0 : resume.ticketId);
553
+ remount();
554
+ }, [authToken, remount]);
555
+ useEffect(() => {
556
+ const lorikeetClient = new LorikeetClient({
557
+ public_key: publicKey,
558
+ api_base: baseUrl,
559
+ metadata,
560
+ customisations: {
561
+ ...customisations,
562
+ hideHeader
563
+ }
564
+ });
565
+ setLorikeetClient(lorikeetClient);
566
+ if (clearing !== null || loggedOutIdentity === identity) return;
567
+ if (loggedOutIdentity !== null) setLoggedOutIdentity(null);
568
+ const resume = resumingTicketId !== void 0 ? {
569
+ session,
570
+ ticketId: resumingTicketId
571
+ } : resumableConversation(observedRef.current, identity, void 0);
572
+ load(lorikeetClient, identity, resumingTicketId, resume);
573
+ }, [
574
+ publicKey,
575
+ baseUrl,
576
+ authToken,
577
+ clearing,
578
+ customisations,
579
+ hideHeader,
580
+ loggedOutIdentity,
581
+ metadata,
582
+ session,
583
+ resumingTicketId,
584
+ identity,
585
+ load
586
+ ]);
587
+ /**
588
+ * Clear this customer's chat from the device. Loads the page's own `/logout`,
589
+ * the only thing that can drop the chat's cookies and the storage the page
590
+ * keeps; a host clearing its stored `ticketId` and `session` leaves both
591
+ * behind.
592
+ *
593
+ * Nothing else loads until the page confirms with `logged_out`, and nothing
594
+ * loads for these credentials after it, so pass the next customer's in to
595
+ * start their chat. Clear your own stored `ticketId` and `session` too.
596
+ */
597
+ const logout = useCallback(() => {
598
+ observedRef.current = null;
599
+ setClosed(null);
600
+ setClearing((previous) => {
601
+ if (previous !== null) return {
602
+ ...previous,
603
+ attempt: previous.attempt + 1
604
+ };
605
+ setLoggedOutIdentity(identity);
606
+ return {
607
+ attempt: 1,
608
+ baseUrl,
609
+ identity,
610
+ uri: new LorikeetClient({
611
+ api_base: baseUrl,
612
+ public_key: publicKey
613
+ }).getLogoutUrl(),
614
+ webviewProps: {
615
+ ...webviewProps,
616
+ injectedJavaScript: void 0,
617
+ injectedJavaScriptBeforeContentLoaded: void 0,
618
+ onMessage: void 0,
619
+ onShouldStartLoadWithRequest: void 0
620
+ }
621
+ };
622
+ });
623
+ }, [
624
+ baseUrl,
625
+ identity,
626
+ publicKey,
627
+ webviewProps
628
+ ]);
629
+ /**
630
+ * Show a minimised conversation again, exactly as the customer left it.
631
+ *
632
+ * A no-op unless something is minimised, so a host with its own launcher can
633
+ * call it without first asking.
634
+ */
635
+ const reopen = useCallback(() => {
636
+ setClosed((previous) => previous === "minimised" ? null : previous);
637
+ }, []);
638
+ /**
639
+ * What the widget is actually on, for a host that has to store it or draw its
640
+ * own launcher around it.
641
+ */
642
+ const getConversation = useCallback(() => {
643
+ const resumable = resumableConversation(observedRef.current, identity, resumingTicketId);
644
+ if (resumable === null) return null;
645
+ return {
646
+ minimised: closed === "minimised",
647
+ session: resumable.session,
648
+ ticketId: resumable.ticketId
649
+ };
650
+ }, [
651
+ closed,
652
+ identity,
653
+ resumingTicketId
654
+ ]);
655
+ useImperativeHandle(ref, () => ({
656
+ getConversation,
657
+ logout,
658
+ reopen
659
+ }), [
660
+ getConversation,
661
+ logout,
662
+ reopen
663
+ ]);
664
+ const clearingAttempt = clearing === null || clearing === void 0 ? void 0 : clearing.attempt;
665
+ useEffect(() => {
666
+ if (clearingAttempt === void 0) return;
667
+ const timer = setTimeout(() => {
668
+ var _onEventRef$current;
669
+ (_onEventRef$current = onEventRef.current) === null || _onEventRef$current === void 0 || _onEventRef$current.call(onEventRef, { type: "logout_failed" });
670
+ }, LOGOUT_CONFIRMATION_TIMEOUT_MS);
671
+ return () => {
672
+ clearTimeout(timer);
673
+ };
674
+ }, [clearingAttempt]);
675
+ const handleClearingEvent = useCallback((event) => {
676
+ if (event.type === "logged_out") setClearing((previous) => (previous === null || previous === void 0 ? void 0 : previous.attempt) === clearingAttempt ? null : previous);
677
+ onEvent === null || onEvent === void 0 || onEvent(event);
678
+ }, [clearingAttempt, onEvent]);
679
+ const reload = useCallback(() => {
680
+ const resume = resumableConversation(observedRef.current, identity, resumingTicketId);
681
+ if (lorikeetClient !== null && resume !== null) {
682
+ load(lorikeetClient, identity, resumingTicketId, resume, true);
683
+ return;
684
+ }
685
+ remount();
686
+ }, [
687
+ identity,
688
+ load,
689
+ lorikeetClient,
690
+ remount,
691
+ resumingTicketId
692
+ ]);
693
+ useEffect(() => {
694
+ const subscription = AppState.addEventListener("change", (next) => {
695
+ if (next === "active") {
696
+ if (shouldReloadAfterBackground(backgroundedAtRef.current, Date.now())) reload();
697
+ backgroundedAtRef.current = null;
698
+ return;
699
+ }
700
+ if (next === "background") {
701
+ var _backgroundedAtRef$cu;
702
+ (_backgroundedAtRef$cu = backgroundedAtRef.current) !== null && _backgroundedAtRef$cu !== void 0 || (backgroundedAtRef.current = Date.now());
703
+ }
704
+ });
705
+ return () => {
706
+ subscription.remove();
707
+ };
708
+ }, [reload]);
709
+ const startNewChat = useCallback(() => {
710
+ setClosed(null);
711
+ if (lorikeetClient !== null && resumingTicketId === void 0) {
712
+ observedRef.current = null;
713
+ load(lorikeetClient, identity, void 0, null, true);
714
+ return;
715
+ }
716
+ remount();
717
+ }, [
718
+ identity,
719
+ load,
720
+ lorikeetClient,
721
+ remount,
722
+ resumingTicketId
723
+ ]);
724
+ const handleEvent = useCallback((event) => {
725
+ if (generation === generationRef.current && loadedForRef.current.identity === identity && loadedForRef.current.intendedTicketId === resumingTicketId) {
726
+ if (event.type === "persist_ticket_id" || event.type === "persist_session") {
727
+ var _observedRef$current;
728
+ const current = ((_observedRef$current = observedRef.current) === null || _observedRef$current === void 0 ? void 0 : _observedRef$current.identity) === identity && observedRef.current.intendedTicketId === resumingTicketId ? observedRef.current : null;
729
+ observedRef.current = {
730
+ ...current,
731
+ ...event.type === "persist_ticket_id" ? { ticketId: event.ticketId } : { session: event.session },
732
+ identity,
733
+ intendedTicketId: resumingTicketId
734
+ };
735
+ }
736
+ if (event.type === "close") setClosed(event.reason === "minimised" ? "minimised" : "ended");
737
+ if (event.type === "new_chat") startNewChat();
738
+ if (event.type === "error" && attemptedTicketId !== void 0 && isUnusableConversationCode(event.error)) {
739
+ observedRef.current = null;
740
+ setRefusedTicketId(attemptedTicketId);
741
+ if (lorikeetClient !== null) load(lorikeetClient, identity, void 0, null, true);
742
+ }
743
+ }
744
+ onEvent === null || onEvent === void 0 || onEvent(event);
745
+ }, [
746
+ attemptedTicketId,
747
+ generation,
748
+ identity,
749
+ load,
750
+ lorikeetClient,
751
+ onEvent,
752
+ resumingTicketId,
753
+ startNewChat
754
+ ]);
755
+ const handleMessage = useCallback((event) => {
756
+ var _webviewProps$onMessa2;
757
+ onWebViewMessage === null || onWebViewMessage === void 0 || onWebViewMessage(event);
758
+ webviewProps === null || webviewProps === void 0 || (_webviewProps$onMessa2 = webviewProps.onMessage) === null || _webviewProps$onMessa2 === void 0 || _webviewProps$onMessa2.call(webviewProps, event);
759
+ }, [onWebViewMessage, webviewProps]);
760
+ if (clearing !== null) return /* @__PURE__ */ jsx(SafeAreaView, {
761
+ edges: [
762
+ "top",
763
+ "left",
764
+ "right"
765
+ ],
766
+ style: stylesheet.root,
767
+ children: /* @__PURE__ */ jsx(View, {
768
+ accessibilityElementsHidden: true,
769
+ importantForAccessibility: "no-hide-descendants",
770
+ pointerEvents: "none",
771
+ style: stylesheet.closed,
772
+ children: /* @__PURE__ */ jsx(LorikeetWidgetWebView, {
773
+ baseUrl: clearing.baseUrl,
774
+ onEvent: handleClearingEvent,
775
+ uri: clearing.uri,
776
+ webviewProps: {
777
+ ...clearing.webviewProps,
778
+ style: stylesheet.webView
779
+ }
780
+ }, `logout|${clearing.attempt}`)
781
+ })
782
+ });
783
+ if (lorikeetClient === null || loaded === null || loaded.identity !== identity || loaded.intendedTicketId !== resumingTicketId || loggedOutIdentity === identity) return null;
784
+ const isClosed = closed !== null;
785
+ return /* @__PURE__ */ jsx(SafeAreaView, {
786
+ edges: [
787
+ "top",
788
+ "left",
789
+ "right"
790
+ ],
791
+ style: stylesheet.root,
792
+ children: /* @__PURE__ */ jsxs(View, {
793
+ style: stylesheet.container,
794
+ children: [
795
+ hideHeader && closed !== "minimised" ? /* @__PURE__ */ jsx(LorikeetWidgetHeader, {
796
+ onIconPress: startNewChat,
797
+ style: styles === null || styles === void 0 ? void 0 : styles.header,
798
+ title: (_customisations$title = customisations === null || customisations === void 0 ? void 0 : customisations.title) !== null && _customisations$title !== void 0 ? _customisations$title : "Lorikeet AI Agent"
799
+ }) : null,
800
+ closed === "minimised" ? /* @__PURE__ */ jsx(TouchableOpacity, {
801
+ onPress: reopen,
802
+ style: stylesheet.closedControl,
803
+ children: /* @__PURE__ */ jsx(Text, {
804
+ style: stylesheet.headerText,
805
+ children: "Reopen chat"
806
+ })
807
+ }) : null,
808
+ !hideHeader && closed === "ended" ? /* @__PURE__ */ jsx(TouchableOpacity, {
809
+ onPress: startNewChat,
810
+ style: stylesheet.closedControl,
811
+ children: /* @__PURE__ */ jsx(Text, {
812
+ style: stylesheet.headerText,
813
+ children: "Start new chat"
814
+ })
815
+ }) : null,
816
+ /* @__PURE__ */ jsx(View, {
817
+ accessibilityElementsHidden: isClosed,
818
+ importantForAccessibility: isClosed ? "no-hide-descendants" : "auto",
819
+ pointerEvents: isClosed ? "none" : "auto",
820
+ style: isClosed ? stylesheet.closed : stylesheet.webView,
821
+ children: /* @__PURE__ */ jsx(LorikeetWidgetWebView, {
822
+ baseUrl: lorikeetClient.baseUrl,
823
+ onEvent: handleEvent,
824
+ ref: webViewRef,
825
+ uri: loaded.uri,
826
+ webviewProps: {
827
+ allowsInlineMediaPlayback: true,
828
+ mediaPlaybackRequiresUserAction: false,
829
+ scalesPageToFit: true,
830
+ startInLoadingState: true,
831
+ style: stylesheet.webView,
832
+ ...webviewProps,
833
+ onMessage: handleMessage,
834
+ onRenderProcessGone: (event) => {
835
+ var _webviewProps$onRende;
836
+ if (generation === generationRef.current) reload();
837
+ webviewProps === null || webviewProps === void 0 || (_webviewProps$onRende = webviewProps.onRenderProcessGone) === null || _webviewProps$onRende === void 0 || _webviewProps$onRende.call(webviewProps, event);
838
+ },
839
+ onContentProcessDidTerminate: (event) => {
840
+ var _webviewProps$onConte;
841
+ if (generation === generationRef.current) reload();
842
+ webviewProps === null || webviewProps === void 0 || (_webviewProps$onConte = webviewProps.onContentProcessDidTerminate) === null || _webviewProps$onConte === void 0 || _webviewProps$onConte.call(webviewProps, event);
843
+ }
844
+ }
845
+ }, `${identity}|${resumingTicketId !== null && resumingTicketId !== void 0 ? resumingTicketId : ""}|${generation}`)
846
+ })
847
+ ]
848
+ })
849
+ });
850
+ });
851
+ LorikeetWidgetView.displayName = "LorikeetWidgetView";
221
852
 
222
- export { LorikeetClient, LorikeetWidgetHeader, LorikeetWidgetView, LorikeetWidgetWebView };
223
- //# sourceMappingURL=index.js.map
853
+ //#endregion
854
+ export { LORIKEET_ERROR_CODES, LorikeetClient, LorikeetWidgetHeader, LorikeetWidgetView, LorikeetWidgetWebView, isUnusableConversation, isUnusableConversationCode };
224
855
  //# sourceMappingURL=index.js.map