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