@lorikeetai/react-native-sdk 1.1.0 → 2.0.0-alpha.1

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,182 +1,677 @@
1
- import { forwardRef, useState, useRef, useEffect } from 'react';
2
- import { MessageCirclePlusIcon } from 'lucide-react-native';
3
- import { StyleSheet, Linking, View, Text, TouchableOpacity } from 'react-native';
4
- import { SafeAreaView } from 'react-native-safe-area-context';
5
- import { WebView } from 'react-native-webview';
6
- import { jsx, jsxs } from 'react/jsx-runtime';
1
+ import { forwardRef, useCallback, useEffect, 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
+ if (api_base) this.baseUrl = api_base;
119
+ this.publicKey = public_key;
120
+ if (customisations) this.customisations = customisations;
121
+ if (metadata) this.metadata = metadata;
122
+ }
123
+ };
124
+
125
+ //#endregion
126
+ //#region src/errors.ts
127
+ /**
128
+ * Codes the chat page posts to the host as `{ type: 'error', error: CODE }`.
129
+ * Mirrored here because the page publishes them nowhere else, so a host would
130
+ * otherwise be matching on strings it discovered by observation.
131
+ */
132
+ const LORIKEET_ERROR_CODES = [
133
+ "MISSING_TOKEN",
134
+ "CORRUPT_TOKEN_DATA",
135
+ "DECRYPT_FAILED",
136
+ "EXPIRED_TOKEN",
137
+ "MALFORMED_TOKEN",
138
+ "MISSING_PUBLIC_KEY",
139
+ "UNKNOWN_PUBLIC_KEY",
140
+ "CUSTOMER_ID_CONFLICT",
141
+ "CUSTOMER_ID_MISSING",
142
+ "FATAL_ERROR",
143
+ "INVALID_SESSION",
144
+ "DOMAIN_NOT_ALLOWED",
145
+ "TICKET_NOT_FOUND"
146
+ ];
147
+ /**
148
+ * The codes that mean the stored conversation cannot be reopened, as opposed to
149
+ * the trusted token being bad or the integration being misconfigured. Only
150
+ * these are worth abandoning a resume for: a token error fails again with the
151
+ * same token, and a configuration error fails again regardless.
152
+ */
153
+ const UNUSABLE_CONVERSATION_CODES = [
154
+ "CUSTOMER_ID_MISSING",
155
+ "INVALID_SESSION",
156
+ "TICKET_NOT_FOUND"
157
+ ];
158
+ /** Whether an error code means the stored conversation cannot be reopened. */
159
+ function isUnusableConversationCode(code) {
160
+ return UNUSABLE_CONVERSATION_CODES.includes(code);
161
+ }
162
+ function isUnusableConversation(message) {
163
+ if (typeof message !== "object" || message === null) return false;
164
+ const { type, error } = message;
165
+ return type === "error" && typeof error === "string" ? isUnusableConversationCode(error) : false;
166
+ }
167
+
168
+ //#endregion
169
+ //#region src/events.ts
170
+ const asRecord = (message) => typeof message === "object" && message !== null && !Array.isArray(message) ? message : null;
171
+ const asNonEmptyString = (value) => typeof value === "string" && value !== "" ? value : null;
172
+ /**
173
+ * The event a bridge message carries, or `null` for one that carries none.
174
+ *
175
+ * `ticket_created` is not derived here: telling a fresh ticket from a resumed
176
+ * one needs the URL the page was loaded with, which only the component holds.
177
+ */
178
+ const eventFromMessage = (message) => {
179
+ const record = asRecord(message);
180
+ if (record === null || typeof record.type !== "string") return null;
181
+ switch (record.type) {
182
+ case "ready": return { type: "ready" };
183
+ case "customerMessage": return { type: "customer_message" };
184
+ case "agentMessage": return { type: "agent_message" };
185
+ case "closeChat": {
186
+ const reason = asNonEmptyString(record.reason);
187
+ return reason === "ended_by_customer" || reason === "minimised" ? {
188
+ type: "close",
189
+ reason
190
+ } : { type: "close" };
191
+ }
192
+ case "conversationEnded": return { type: "conversation_ended" };
193
+ case "startNewChat": return { type: "new_chat" };
194
+ case "persistLorikeetTicketId": {
195
+ const ticketId = asNonEmptyString(record.ticketId);
196
+ return ticketId === null ? null : {
197
+ type: "persist_ticket_id",
198
+ ticketId
199
+ };
200
+ }
201
+ case "persistLorikeetSession": {
202
+ const session = asNonEmptyString(record.session);
203
+ return session === null ? null : {
204
+ type: "persist_session",
205
+ session
206
+ };
207
+ }
208
+ case "error": {
209
+ const error = asNonEmptyString(record.error);
210
+ return error === null ? null : {
211
+ type: "error",
212
+ error
213
+ };
214
+ }
215
+ case "preloadComplete": return {
216
+ type: "preload_complete",
217
+ clientStart: record.clientStart === true,
218
+ lazyTicketCreationEnabled: record.lazyTicketCreationEnabled === true
219
+ };
220
+ default: return null;
221
+ }
222
+ };
223
+ /**
224
+ * The ticket a URL is trying to resume, or `null` for one that creates its own.
225
+ *
226
+ * Read from the URL rather than from any state, so it holds for a host that
227
+ * built the URL itself. An unreadable URL reports nothing, which at worst
228
+ * announces a resumed ticket as created once.
229
+ */
230
+ const resumedTicketIdFromUri = (uri) => {
231
+ var _segments$;
232
+ let pathname;
233
+ try {
234
+ pathname = new URL(uri).pathname;
235
+ } catch {
236
+ return null;
237
+ }
238
+ const segments = pathname.split("/").filter((part) => part !== "");
239
+ if (segments.length < 3 || segments[0] !== "tickets") return null;
240
+ return (_segments$ = segments[2]) !== null && _segments$ !== void 0 ? _segments$ : null;
51
241
  };
52
- var stylesheet = StyleSheet.create({
53
- root: { flex: 1, backgroundColor: "#fff" },
54
- container: {
55
- flex: 1,
56
- borderRadius: 8,
57
- overflow: "hidden",
58
- shadowColor: "#000",
59
- shadowOffset: { width: 0, height: 2 },
60
- shadowOpacity: 0.1,
61
- shadowRadius: 4,
62
- elevation: 3
63
- },
64
- header: {
65
- padding: 16,
66
- display: "flex",
67
- flexDirection: "row",
68
- justifyContent: "space-between"
69
- },
70
- headerIcon: {
71
- width: 24
72
- },
73
- headerText: {
74
- textAlign: "center",
75
- fontSize: 16
76
- },
77
- webView: {
78
- flex: 1
79
- }
242
+
243
+ //#endregion
244
+ //#region src/lifecycle.ts
245
+ /**
246
+ * The page gives its message stream ten attempts with exponential backoff
247
+ * capped at 30s (`message-stream-utils.tsx`), so the budget runs out roughly
248
+ * 151s after the stream drops, or ~196s once jitter is counted. It then sits at
249
+ * `failed` with nothing to reset it, so a customer who comes back to a
250
+ * backgrounded app is looking at a stream that will never reconnect.
251
+ *
252
+ * Three minutes sits inside that band. Shorter would remount while the page's
253
+ * own retries could still win, which costs the customer their draft message for
254
+ * nothing; much longer leaves a dead stream on screen.
255
+ */
256
+ const BACKGROUND_RELOAD_THRESHOLD_MS = 18e4;
257
+ /**
258
+ * Elapsed time is the only signal available: the bridge reports `ready`,
259
+ * `error` and the persistence messages, but never stream health, so the SDK
260
+ * cannot ask whether the connection actually survived.
261
+ */
262
+ const shouldReloadAfterBackground = (backgroundedAt, now, thresholdMs = BACKGROUND_RELOAD_THRESHOLD_MS) => {
263
+ if (backgroundedAt === null) return false;
264
+ const elapsed = now - backgroundedAt;
265
+ if (elapsed < 0) return false;
266
+ return elapsed >= thresholdMs;
267
+ };
268
+
269
+ //#endregion
270
+ //#region src/links.ts
271
+ /**
272
+ * Schemes the WebView resolves for itself. None of them is a page the customer
273
+ * asked to leave the app for, and the OS cannot open them anyway.
274
+ */
275
+ const WEBVIEW_OWNED_SCHEMES = /* @__PURE__ */ new Set([
276
+ "about:",
277
+ "blob:",
278
+ "data:",
279
+ "javascript:"
280
+ ]);
281
+ const parseUrl = (value) => {
282
+ try {
283
+ return new URL(value);
284
+ } catch {
285
+ return null;
286
+ }
287
+ };
288
+ /**
289
+ * Whether the OS could be asked to open this at all. `window.open()` with no
290
+ * argument gives `about:blank`, and a scheme the WebView resolves for itself is
291
+ * not something the OS can place.
292
+ */
293
+ const canOpenOutsideApp = (url) => {
294
+ const target = parseUrl(url);
295
+ return target !== null && !WEBVIEW_OWNED_SCHEMES.has(target.protocol);
296
+ };
297
+ /**
298
+ * Whether a navigation belongs inside the widget.
299
+ *
300
+ * An origin comparison rather than the substring match this replaces. `baseUrl`
301
+ * appearing anywhere in a URL counted as internal, so
302
+ * `https://evil.example/?next=https://chat.lorikeetcx.ai` loaded in place of the
303
+ * conversation instead of opening in the browser. Comparing origins rather than
304
+ * matching a prefix also means a port, a trailing slash and a different path all
305
+ * still resolve to the same widget.
306
+ */
307
+ const isInternalNavigation = (url, baseUrl) => {
308
+ const base = parseUrl(baseUrl);
309
+ const target = parseUrl(url);
310
+ if (base === null || target === null) return true;
311
+ if (WEBVIEW_OWNED_SCHEMES.has(target.protocol)) return true;
312
+ return target.origin === base.origin;
313
+ };
314
+
315
+ //#endregion
316
+ //#region src/message-circle-plus-icon.tsx
317
+ const MessageCirclePlusIcon = ({ size = 24, color = "currentColor", strokeWidth = 2 }) => {
318
+ return /* @__PURE__ */ jsxs(Svg, {
319
+ fill: "none",
320
+ height: size,
321
+ viewBox: "0 0 24 24",
322
+ width: size,
323
+ children: [
324
+ /* @__PURE__ */ jsx(Path, {
325
+ d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z",
326
+ stroke: color,
327
+ strokeLinecap: "round",
328
+ strokeLinejoin: "round",
329
+ strokeWidth
330
+ }),
331
+ /* @__PURE__ */ jsx(Line, {
332
+ stroke: color,
333
+ strokeLinecap: "round",
334
+ strokeWidth,
335
+ x1: "12",
336
+ x2: "12",
337
+ y1: "8",
338
+ y2: "16"
339
+ }),
340
+ /* @__PURE__ */ jsx(Line, {
341
+ stroke: color,
342
+ strokeLinecap: "round",
343
+ strokeWidth,
344
+ x1: "8",
345
+ x2: "16",
346
+ y1: "12",
347
+ y2: "12"
348
+ })
349
+ ]
350
+ });
351
+ };
352
+
353
+ //#endregion
354
+ //#region src/widget.tsx
355
+ const stylesheet = StyleSheet.create({
356
+ root: {
357
+ flex: 1,
358
+ backgroundColor: "#fff"
359
+ },
360
+ container: {
361
+ flex: 1,
362
+ borderRadius: 8,
363
+ overflow: "hidden",
364
+ shadowColor: "#000",
365
+ shadowOffset: {
366
+ width: 0,
367
+ height: 2
368
+ },
369
+ shadowOpacity: .1,
370
+ shadowRadius: 4,
371
+ elevation: 3
372
+ },
373
+ header: {
374
+ padding: 16,
375
+ display: "flex",
376
+ flexDirection: "row",
377
+ justifyContent: "space-between"
378
+ },
379
+ headerIcon: { width: 24 },
380
+ headerText: {
381
+ textAlign: "center",
382
+ fontSize: 16
383
+ },
384
+ webView: { flex: 1 },
385
+ closed: {
386
+ flex: 1,
387
+ opacity: 0
388
+ }
80
389
  });
81
- var LorikeetWidgetWebView = forwardRef(({ uri, baseUrl = "https://chat.lorikeetcx.ai", webviewProps = {} }, ref) => {
82
- return /* @__PURE__ */ jsx(
83
- WebView,
84
- {
85
- ...webviewProps,
86
- onNavigationStateChange: (navState) => {
87
- if (navState.url.includes(baseUrl)) {
88
- return false;
89
- } else {
90
- void Linking.openURL(navState.url);
91
- return false;
92
- }
93
- },
94
- ref,
95
- source: {
96
- uri
97
- },
98
- style: stylesheet.webView
99
- }
100
- );
390
+ const LorikeetWidgetWebView = forwardRef(({ uri, baseUrl = "https://chat.lorikeetcx.ai", webviewProps = {}, onEvent }, ref) => {
391
+ const isInternal = (url) => isInternalNavigation(url, baseUrl);
392
+ const accounted = useRef(/* @__PURE__ */ new Set());
393
+ const seenUri = useRef(null);
394
+ if (seenUri.current !== uri) {
395
+ seenUri.current = uri;
396
+ const resumed = resumedTicketIdFromUri(uri);
397
+ if (resumed !== null) accounted.current.add(resumed);
398
+ }
399
+ const emit = (event) => {
400
+ onEvent === null || onEvent === void 0 || onEvent(event);
401
+ if (event.type !== "persist_ticket_id") return;
402
+ if (accounted.current.has(event.ticketId)) return;
403
+ accounted.current.add(event.ticketId);
404
+ onEvent === null || onEvent === void 0 || onEvent({
405
+ ticketId: event.ticketId,
406
+ type: "ticket_created"
407
+ });
408
+ };
409
+ return /* @__PURE__ */ jsx(WebView, {
410
+ style: stylesheet.webView,
411
+ ...webviewProps,
412
+ onMessage: (event) => {
413
+ var _webviewProps$onMessa;
414
+ let message;
415
+ try {
416
+ message = JSON.parse(event.nativeEvent.data);
417
+ } catch {}
418
+ const widgetEvent = eventFromMessage(message);
419
+ if (widgetEvent !== null) emit(widgetEvent);
420
+ (_webviewProps$onMessa = webviewProps.onMessage) === null || _webviewProps$onMessa === void 0 || _webviewProps$onMessa.call(webviewProps, event);
421
+ },
422
+ onNavigationStateChange: (navState) => {
423
+ var _webviewProps$onNavig;
424
+ if (!isInternal(navState.url)) Linking.openURL(navState.url);
425
+ (_webviewProps$onNavig = webviewProps.onNavigationStateChange) === null || _webviewProps$onNavig === void 0 || _webviewProps$onNavig.call(webviewProps, navState);
426
+ },
427
+ onOpenWindow: (event) => {
428
+ var _webviewProps$onOpenW;
429
+ const { targetUrl } = event.nativeEvent;
430
+ if (canOpenOutsideApp(targetUrl)) Linking.openURL(targetUrl);
431
+ (_webviewProps$onOpenW = webviewProps.onOpenWindow) === null || _webviewProps$onOpenW === void 0 || _webviewProps$onOpenW.call(webviewProps, event);
432
+ },
433
+ onShouldStartLoadWithRequest: (request) => {
434
+ var _webviewProps$onShoul;
435
+ if (((_webviewProps$onShoul = webviewProps.onShouldStartLoadWithRequest) === null || _webviewProps$onShoul === void 0 ? void 0 : _webviewProps$onShoul.call(webviewProps, request)) === false) return false;
436
+ if (request.isTopFrame === false || isInternal(request.url)) return true;
437
+ Linking.openURL(request.url);
438
+ return false;
439
+ },
440
+ ref,
441
+ source: { uri }
442
+ });
101
443
  });
102
444
  LorikeetWidgetWebView.displayName = "LorikeetWidgetWebView";
103
- var LorikeetWidgetHeader = ({
104
- title,
105
- style,
106
- icon = /* @__PURE__ */ jsx(MessageCirclePlusIcon, { size: 16 }),
107
- onIconPress
108
- }) => {
109
- return /* @__PURE__ */ jsxs(View, { style: stylesheet.header, children: [
110
- /* @__PURE__ */ jsx(View, { style: stylesheet.headerIcon }),
111
- /* @__PURE__ */ jsx(Text, { style: [stylesheet.headerText, style], children: title }),
112
- /* @__PURE__ */ jsx(TouchableOpacity, { onPress: onIconPress, style: stylesheet.headerIcon, children: icon })
113
- ] });
445
+ const LorikeetWidgetHeader = ({ title, style, icon = /* @__PURE__ */ jsx(MessageCirclePlusIcon, { size: 16 }), onIconPress }) => {
446
+ return /* @__PURE__ */ jsxs(View, {
447
+ style: stylesheet.header,
448
+ children: [
449
+ /* @__PURE__ */ jsx(View, { style: stylesheet.headerIcon }),
450
+ /* @__PURE__ */ jsx(Text, {
451
+ style: [stylesheet.headerText, style],
452
+ children: title
453
+ }),
454
+ /* @__PURE__ */ jsx(TouchableOpacity, {
455
+ onPress: onIconPress,
456
+ style: stylesheet.headerIcon,
457
+ children: icon
458
+ })
459
+ ]
460
+ });
114
461
  };
115
- function LorikeetWidgetView({
116
- publicKey,
117
- baseUrl,
118
- authToken,
119
- customisations,
120
- styles,
121
- onWebViewMessage
122
- }) {
123
- var _a;
124
- const [lorikeetClient, setLorikeetClient] = useState(
125
- null
126
- );
127
- const [uri, setUri] = useState(null);
128
- const [timestamp, setTimestamp] = useState(Date.now());
129
- const webViewRef = useRef(null);
130
- useEffect(() => {
131
- const lorikeetClient2 = new LorikeetClient({
132
- public_key: publicKey,
133
- api_base: baseUrl,
134
- customisations: {
135
- ...customisations,
136
- hideHeader: (customisations == null ? void 0 : customisations.hideHeader) === void 0 ? true : customisations.hideHeader
137
- }
138
- });
139
- setLorikeetClient(lorikeetClient2);
140
- setUri(
141
- lorikeetClient2.getWebviewUrl({
142
- token: authToken
143
- })
144
- );
145
- }, [publicKey, baseUrl]);
146
- if (!lorikeetClient || !uri) {
147
- return null;
148
- }
149
- return /* @__PURE__ */ jsx(SafeAreaView, { edges: ["top", "left", "right"], style: stylesheet.root, children: /* @__PURE__ */ jsxs(View, { style: stylesheet.container, children: [
150
- /* @__PURE__ */ jsx(
151
- LorikeetWidgetHeader,
152
- {
153
- onIconPress: () => {
154
- setTimestamp(Date.now());
155
- },
156
- style: styles == null ? void 0 : styles.header,
157
- title: (_a = customisations == null ? void 0 : customisations.title) != null ? _a : "Lorikeet AI Agent"
158
- }
159
- ),
160
- /* @__PURE__ */ jsx(
161
- LorikeetWidgetWebView,
162
- {
163
- baseUrl: lorikeetClient.baseUrl,
164
- ref: webViewRef,
165
- uri,
166
- webviewProps: {
167
- onMessage: onWebViewMessage,
168
- allowsInlineMediaPlayback: true,
169
- mediaPlaybackRequiresUserAction: false,
170
- scalesPageToFit: true,
171
- startInLoadingState: true,
172
- style: stylesheet.webView
173
- }
174
- },
175
- timestamp
176
- )
177
- ] }) });
462
+ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, styles, onEvent, onWebViewMessage, ticketId, session, metadata, webviewProps }) {
463
+ var _customisations$title;
464
+ const [lorikeetClient, setLorikeetClient] = useState(null);
465
+ const [loaded, setLoaded] = useState(null);
466
+ const [generation, setGeneration] = useState(0);
467
+ const generationRef = useRef(generation);
468
+ generationRef.current = generation;
469
+ const remount = useCallback(() => {
470
+ setGeneration((previous) => previous + 1);
471
+ }, []);
472
+ const webViewRef = useRef(null);
473
+ const [refusedTicketId, setRefusedTicketId] = useState(null);
474
+ const observedRef = useRef(null);
475
+ const backgroundedAtRef = useRef(null);
476
+ const uriRef = useRef(null);
477
+ const loadedForRef = useRef({
478
+ identity: "",
479
+ intendedTicketId: void 0
480
+ });
481
+ const identity = conversationIdentity({
482
+ authToken,
483
+ baseUrl,
484
+ publicKey
485
+ });
486
+ const resumingTicketId = ticketId !== void 0 && ticketId !== refusedTicketId ? ticketId : void 0;
487
+ const [attemptedTicketId, setAttemptedTicketId] = useState(void 0);
488
+ const [closed, setClosed] = useState(false);
489
+ const load = useCallback((client, forIdentity, intendedTicketId, resume, force = false) => {
490
+ const next = client.getWebviewUrl({
491
+ token: authToken,
492
+ ticket_id: resume === null || resume === void 0 ? void 0 : resume.ticketId,
493
+ session: resume === null || resume === void 0 ? void 0 : resume.session
494
+ });
495
+ const unchanged = uriRef.current === next;
496
+ if (loadedForRef.current.identity !== forIdentity || loadedForRef.current.intendedTicketId !== intendedTicketId) setClosed(false);
497
+ uriRef.current = next;
498
+ loadedForRef.current = {
499
+ identity: forIdentity,
500
+ intendedTicketId
501
+ };
502
+ if (unchanged && !force) {
503
+ setLoaded((previous) => previous === null || previous.identity !== forIdentity || previous.intendedTicketId !== intendedTicketId ? {
504
+ identity: forIdentity,
505
+ intendedTicketId,
506
+ uri: next
507
+ } : previous);
508
+ return;
509
+ }
510
+ setLoaded({
511
+ identity: forIdentity,
512
+ intendedTicketId,
513
+ uri: next
514
+ });
515
+ setAttemptedTicketId(resume === null || resume === void 0 ? void 0 : resume.ticketId);
516
+ remount();
517
+ }, [authToken, remount]);
518
+ useEffect(() => {
519
+ const lorikeetClient = new LorikeetClient({
520
+ public_key: publicKey,
521
+ api_base: baseUrl,
522
+ metadata,
523
+ customisations: {
524
+ ...customisations,
525
+ hideHeader: (customisations === null || customisations === void 0 ? void 0 : customisations.hideHeader) === void 0 ? true : customisations.hideHeader
526
+ }
527
+ });
528
+ setLorikeetClient(lorikeetClient);
529
+ const resume = resumingTicketId !== void 0 ? {
530
+ session,
531
+ ticketId: resumingTicketId
532
+ } : resumableConversation(observedRef.current, identity, void 0);
533
+ load(lorikeetClient, identity, resumingTicketId, resume);
534
+ }, [
535
+ publicKey,
536
+ baseUrl,
537
+ authToken,
538
+ customisations,
539
+ metadata,
540
+ session,
541
+ resumingTicketId,
542
+ identity,
543
+ load
544
+ ]);
545
+ const reload = useCallback(() => {
546
+ const resume = resumableConversation(observedRef.current, identity, resumingTicketId);
547
+ if (lorikeetClient !== null && resume !== null) {
548
+ load(lorikeetClient, identity, resumingTicketId, resume, true);
549
+ return;
550
+ }
551
+ remount();
552
+ }, [
553
+ identity,
554
+ load,
555
+ lorikeetClient,
556
+ remount,
557
+ resumingTicketId
558
+ ]);
559
+ useEffect(() => {
560
+ const subscription = AppState.addEventListener("change", (next) => {
561
+ if (next === "active") {
562
+ if (shouldReloadAfterBackground(backgroundedAtRef.current, Date.now())) reload();
563
+ backgroundedAtRef.current = null;
564
+ return;
565
+ }
566
+ if (next === "background") {
567
+ var _backgroundedAtRef$cu;
568
+ (_backgroundedAtRef$cu = backgroundedAtRef.current) !== null && _backgroundedAtRef$cu !== void 0 || (backgroundedAtRef.current = Date.now());
569
+ }
570
+ });
571
+ return () => {
572
+ subscription.remove();
573
+ };
574
+ }, [reload]);
575
+ const startNewChat = useCallback(() => {
576
+ setClosed(false);
577
+ if (lorikeetClient !== null && resumingTicketId === void 0) {
578
+ observedRef.current = null;
579
+ load(lorikeetClient, identity, void 0, null, true);
580
+ return;
581
+ }
582
+ remount();
583
+ }, [
584
+ identity,
585
+ load,
586
+ lorikeetClient,
587
+ remount,
588
+ resumingTicketId
589
+ ]);
590
+ const handleEvent = useCallback((event) => {
591
+ if (generation === generationRef.current && loadedForRef.current.identity === identity && loadedForRef.current.intendedTicketId === resumingTicketId) {
592
+ if (event.type === "persist_ticket_id" || event.type === "persist_session") {
593
+ var _observedRef$current;
594
+ const current = ((_observedRef$current = observedRef.current) === null || _observedRef$current === void 0 ? void 0 : _observedRef$current.identity) === identity && observedRef.current.intendedTicketId === resumingTicketId ? observedRef.current : null;
595
+ observedRef.current = {
596
+ ...current,
597
+ ...event.type === "persist_ticket_id" ? { ticketId: event.ticketId } : { session: event.session },
598
+ identity,
599
+ intendedTicketId: resumingTicketId
600
+ };
601
+ }
602
+ if (event.type === "close") setClosed(true);
603
+ if (event.type === "new_chat") startNewChat();
604
+ if (event.type === "error" && attemptedTicketId !== void 0 && isUnusableConversationCode(event.error)) {
605
+ observedRef.current = null;
606
+ setRefusedTicketId(attemptedTicketId);
607
+ if (lorikeetClient !== null) load(lorikeetClient, identity, void 0, null, true);
608
+ }
609
+ }
610
+ onEvent === null || onEvent === void 0 || onEvent(event);
611
+ }, [
612
+ attemptedTicketId,
613
+ generation,
614
+ identity,
615
+ load,
616
+ lorikeetClient,
617
+ onEvent,
618
+ resumingTicketId,
619
+ startNewChat
620
+ ]);
621
+ const handleMessage = useCallback((event) => {
622
+ var _webviewProps$onMessa2;
623
+ onWebViewMessage === null || onWebViewMessage === void 0 || onWebViewMessage(event);
624
+ webviewProps === null || webviewProps === void 0 || (_webviewProps$onMessa2 = webviewProps.onMessage) === null || _webviewProps$onMessa2 === void 0 || _webviewProps$onMessa2.call(webviewProps, event);
625
+ }, [onWebViewMessage, webviewProps]);
626
+ if (lorikeetClient === null || loaded === null || loaded.identity !== identity || loaded.intendedTicketId !== resumingTicketId) return null;
627
+ return /* @__PURE__ */ jsx(SafeAreaView, {
628
+ edges: [
629
+ "top",
630
+ "left",
631
+ "right"
632
+ ],
633
+ style: stylesheet.root,
634
+ children: /* @__PURE__ */ jsxs(View, {
635
+ style: stylesheet.container,
636
+ children: [/* @__PURE__ */ jsx(LorikeetWidgetHeader, {
637
+ onIconPress: startNewChat,
638
+ style: styles === null || styles === void 0 ? void 0 : styles.header,
639
+ title: (_customisations$title = customisations === null || customisations === void 0 ? void 0 : customisations.title) !== null && _customisations$title !== void 0 ? _customisations$title : "Lorikeet AI Agent"
640
+ }), /* @__PURE__ */ jsx(View, {
641
+ accessibilityElementsHidden: closed,
642
+ importantForAccessibility: closed ? "no-hide-descendants" : "auto",
643
+ pointerEvents: closed ? "none" : "auto",
644
+ style: closed ? stylesheet.closed : stylesheet.webView,
645
+ children: /* @__PURE__ */ jsx(LorikeetWidgetWebView, {
646
+ baseUrl: lorikeetClient.baseUrl,
647
+ onEvent: handleEvent,
648
+ ref: webViewRef,
649
+ uri: loaded.uri,
650
+ webviewProps: {
651
+ allowsInlineMediaPlayback: true,
652
+ mediaPlaybackRequiresUserAction: false,
653
+ scalesPageToFit: true,
654
+ startInLoadingState: true,
655
+ style: stylesheet.webView,
656
+ ...webviewProps,
657
+ onMessage: handleMessage,
658
+ onRenderProcessGone: (event) => {
659
+ var _webviewProps$onRende;
660
+ if (generation === generationRef.current) reload();
661
+ webviewProps === null || webviewProps === void 0 || (_webviewProps$onRende = webviewProps.onRenderProcessGone) === null || _webviewProps$onRende === void 0 || _webviewProps$onRende.call(webviewProps, event);
662
+ },
663
+ onContentProcessDidTerminate: (event) => {
664
+ var _webviewProps$onConte;
665
+ if (generation === generationRef.current) reload();
666
+ webviewProps === null || webviewProps === void 0 || (_webviewProps$onConte = webviewProps.onContentProcessDidTerminate) === null || _webviewProps$onConte === void 0 || _webviewProps$onConte.call(webviewProps, event);
667
+ }
668
+ }
669
+ }, `${identity}|${resumingTicketId !== null && resumingTicketId !== void 0 ? resumingTicketId : ""}|${generation}`)
670
+ })]
671
+ })
672
+ });
178
673
  }
179
674
 
180
- export { LorikeetClient, LorikeetWidgetHeader, LorikeetWidgetView, LorikeetWidgetWebView };
181
- //# sourceMappingURL=index.js.map
675
+ //#endregion
676
+ export { LORIKEET_ERROR_CODES, LorikeetClient, LorikeetWidgetHeader, LorikeetWidgetView, LorikeetWidgetWebView, isUnusableConversation, isUnusableConversationCode };
182
677
  //# sourceMappingURL=index.js.map