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

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,4 +1,4 @@
1
- import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
1
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
2
2
  import { AppState, Linking, StyleSheet, Text, TouchableOpacity, View } from "react-native";
3
3
  import { SafeAreaView } from "react-native-safe-area-context";
4
4
  import { WebView } from "react-native-webview";
@@ -115,6 +115,26 @@ var LorikeetClient = class {
115
115
  if (this.metadata) url.searchParams.append("metadata", JSON.stringify(this.metadata));
116
116
  return url.toString();
117
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
+ );
118
138
  if (api_base) this.baseUrl = api_base;
119
139
  this.publicKey = public_key;
120
140
  if (customisations) this.customisations = customisations;
@@ -181,7 +201,10 @@ const eventFromMessage = (message) => {
181
201
  switch (record.type) {
182
202
  case "ready": return { type: "ready" };
183
203
  case "customerMessage": return { type: "customer_message" };
184
- case "agentMessage": return { type: "agent_message" };
204
+ case "agentMessage": return typeof record.markUnread === "boolean" ? {
205
+ type: "agent_message",
206
+ markUnread: record.markUnread
207
+ } : { type: "agent_message" };
185
208
  case "closeChat": {
186
209
  const reason = asNonEmptyString(record.reason);
187
210
  return reason === "ended_by_customer" || reason === "minimised" ? {
@@ -205,6 +228,7 @@ const eventFromMessage = (message) => {
205
228
  session
206
229
  };
207
230
  }
231
+ case "loggedOut": return { type: "logged_out" };
208
232
  case "error": {
209
233
  const error = asNonEmptyString(record.error);
210
234
  return error === null ? null : {
@@ -255,6 +279,18 @@ const resumedTicketIdFromUri = (uri) => {
255
279
  */
256
280
  const BACKGROUND_RELOAD_THRESHOLD_MS = 18e4;
257
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
+ * `LorikeetWidgetView` applies this to its own `logout()`. Exported because a
288
+ * composed host has to run the timer itself and nothing else will report
289
+ * `logout_failed` for it, and two hosts choosing their own budgets is how one
290
+ * app calls a clear failed that the other never does.
291
+ */
292
+ const LOGOUT_CONFIRMATION_TIMEOUT_MS = 1e4;
293
+ /**
258
294
  * Elapsed time is the only signal available: the bridge reports `ready`,
259
295
  * `error` and the persistence messages, but never stream health, so the SDK
260
296
  * cannot ask whether the connection actually survived.
@@ -381,6 +417,7 @@ const stylesheet = StyleSheet.create({
381
417
  textAlign: "center",
382
418
  fontSize: 16
383
419
  },
420
+ closedControl: { padding: 16 },
384
421
  webView: { flex: 1 },
385
422
  closed: {
386
423
  flex: 1,
@@ -459,8 +496,8 @@ const LorikeetWidgetHeader = ({ title, style, icon = /* @__PURE__ */ jsx(Message
459
496
  ]
460
497
  });
461
498
  };
462
- function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, styles, onEvent, onWebViewMessage, ticketId, session, metadata, webviewProps }) {
463
- var _customisations$title;
499
+ const LorikeetWidgetView = forwardRef(function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, styles, onEvent, onWebViewMessage, ticketId, session, metadata, webviewProps }, ref) {
500
+ var _customisations$hideH, _customisations$title;
464
501
  const [lorikeetClient, setLorikeetClient] = useState(null);
465
502
  const [loaded, setLoaded] = useState(null);
466
503
  const [generation, setGeneration] = useState(0);
@@ -485,7 +522,12 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
485
522
  });
486
523
  const resumingTicketId = ticketId !== void 0 && ticketId !== refusedTicketId ? ticketId : void 0;
487
524
  const [attemptedTicketId, setAttemptedTicketId] = useState(void 0);
488
- const [closed, setClosed] = useState(false);
525
+ const [closed, setClosed] = useState(null);
526
+ const [clearing, setClearing] = useState(null);
527
+ const [loggedOutIdentity, setLoggedOutIdentity] = useState(null);
528
+ const onEventRef = useRef(onEvent);
529
+ onEventRef.current = onEvent;
530
+ const hideHeader = (_customisations$hideH = customisations === null || customisations === void 0 ? void 0 : customisations.hideHeader) !== null && _customisations$hideH !== void 0 ? _customisations$hideH : true;
489
531
  const load = useCallback((client, forIdentity, intendedTicketId, resume, force = false) => {
490
532
  const next = client.getWebviewUrl({
491
533
  token: authToken,
@@ -493,7 +535,7 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
493
535
  session: resume === null || resume === void 0 ? void 0 : resume.session
494
536
  });
495
537
  const unchanged = uriRef.current === next;
496
- if (loadedForRef.current.identity !== forIdentity || loadedForRef.current.intendedTicketId !== intendedTicketId) setClosed(false);
538
+ if (loadedForRef.current.identity !== forIdentity || loadedForRef.current.intendedTicketId !== intendedTicketId) setClosed(null);
497
539
  uriRef.current = next;
498
540
  loadedForRef.current = {
499
541
  identity: forIdentity,
@@ -522,10 +564,12 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
522
564
  metadata,
523
565
  customisations: {
524
566
  ...customisations,
525
- hideHeader: (customisations === null || customisations === void 0 ? void 0 : customisations.hideHeader) === void 0 ? true : customisations.hideHeader
567
+ hideHeader
526
568
  }
527
569
  });
528
570
  setLorikeetClient(lorikeetClient);
571
+ if (clearing !== null || loggedOutIdentity === identity) return;
572
+ if (loggedOutIdentity !== null) setLoggedOutIdentity(null);
529
573
  const resume = resumingTicketId !== void 0 ? {
530
574
  session,
531
575
  ticketId: resumingTicketId
@@ -535,13 +579,108 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
535
579
  publicKey,
536
580
  baseUrl,
537
581
  authToken,
582
+ clearing,
538
583
  customisations,
584
+ hideHeader,
585
+ loggedOutIdentity,
539
586
  metadata,
540
587
  session,
541
588
  resumingTicketId,
542
589
  identity,
543
590
  load
544
591
  ]);
592
+ /**
593
+ * Clear this customer's chat from the device. Loads the page's own `/logout`,
594
+ * the only thing that can drop the chat's cookies and the storage the page
595
+ * keeps; a host clearing its stored `ticketId` and `session` leaves both
596
+ * behind.
597
+ *
598
+ * Nothing else loads until the page confirms with `logged_out`, and nothing
599
+ * loads for these credentials after it, so pass the next customer's in to
600
+ * start their chat. Clear your own stored `ticketId` and `session` too.
601
+ */
602
+ const logout = useCallback(() => {
603
+ observedRef.current = null;
604
+ setClosed(null);
605
+ setClearing((previous) => {
606
+ if (previous !== null) return {
607
+ ...previous,
608
+ attempt: previous.attempt + 1
609
+ };
610
+ setLoggedOutIdentity(identity);
611
+ return {
612
+ attempt: 1,
613
+ baseUrl,
614
+ identity,
615
+ uri: new LorikeetClient({
616
+ api_base: baseUrl,
617
+ public_key: publicKey
618
+ }).getLogoutUrl(),
619
+ webviewProps: {
620
+ ...webviewProps,
621
+ injectedJavaScript: void 0,
622
+ injectedJavaScriptBeforeContentLoaded: void 0,
623
+ onMessage: void 0,
624
+ onShouldStartLoadWithRequest: void 0
625
+ }
626
+ };
627
+ });
628
+ }, [
629
+ baseUrl,
630
+ identity,
631
+ publicKey,
632
+ webviewProps
633
+ ]);
634
+ /**
635
+ * Show a minimised conversation again, exactly as the customer left it.
636
+ *
637
+ * A no-op unless something is minimised, so a host with its own launcher can
638
+ * call it without first asking.
639
+ */
640
+ const reopen = useCallback(() => {
641
+ setClosed((previous) => previous === "minimised" ? null : previous);
642
+ }, []);
643
+ /**
644
+ * What the widget is actually on, for a host that has to store it or draw its
645
+ * own launcher around it.
646
+ */
647
+ const getConversation = useCallback(() => {
648
+ const resumable = resumableConversation(observedRef.current, identity, resumingTicketId);
649
+ if (resumable === null) return null;
650
+ return {
651
+ minimised: closed === "minimised",
652
+ session: resumable.session,
653
+ ticketId: resumable.ticketId
654
+ };
655
+ }, [
656
+ closed,
657
+ identity,
658
+ resumingTicketId
659
+ ]);
660
+ useImperativeHandle(ref, () => ({
661
+ getConversation,
662
+ logout,
663
+ reopen
664
+ }), [
665
+ getConversation,
666
+ logout,
667
+ reopen
668
+ ]);
669
+ const clearingAttempt = clearing === null || clearing === void 0 ? void 0 : clearing.attempt;
670
+ useEffect(() => {
671
+ if (clearingAttempt === void 0) return;
672
+ const timer = setTimeout(() => {
673
+ var _onEventRef$current;
674
+ (_onEventRef$current = onEventRef.current) === null || _onEventRef$current === void 0 || _onEventRef$current.call(onEventRef, { type: "logout_failed" });
675
+ }, LOGOUT_CONFIRMATION_TIMEOUT_MS);
676
+ return () => {
677
+ clearTimeout(timer);
678
+ };
679
+ }, [clearingAttempt]);
680
+ const handleClearingEvent = useCallback((event) => {
681
+ if (event.type === "logged_out") setClearing((previous) => (previous === null || previous === void 0 ? void 0 : previous.attempt) === clearingAttempt ? null : previous);
682
+ onEvent === null || onEvent === void 0 || onEvent(event);
683
+ }, [clearingAttempt, onEvent]);
545
684
  const reload = useCallback(() => {
546
685
  const resume = resumableConversation(observedRef.current, identity, resumingTicketId);
547
686
  if (lorikeetClient !== null && resume !== null) {
@@ -573,7 +712,7 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
573
712
  };
574
713
  }, [reload]);
575
714
  const startNewChat = useCallback(() => {
576
- setClosed(false);
715
+ setClosed(null);
577
716
  if (lorikeetClient !== null && resumingTicketId === void 0) {
578
717
  observedRef.current = null;
579
718
  load(lorikeetClient, identity, void 0, null, true);
@@ -599,7 +738,7 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
599
738
  intendedTicketId: resumingTicketId
600
739
  };
601
740
  }
602
- if (event.type === "close") setClosed(true);
741
+ if (event.type === "close") setClosed(event.reason === "minimised" ? "minimised" : "ended");
603
742
  if (event.type === "new_chat") startNewChat();
604
743
  if (event.type === "error" && attemptedTicketId !== void 0 && isUnusableConversationCode(event.error)) {
605
744
  observedRef.current = null;
@@ -623,7 +762,31 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
623
762
  onWebViewMessage === null || onWebViewMessage === void 0 || onWebViewMessage(event);
624
763
  webviewProps === null || webviewProps === void 0 || (_webviewProps$onMessa2 = webviewProps.onMessage) === null || _webviewProps$onMessa2 === void 0 || _webviewProps$onMessa2.call(webviewProps, event);
625
764
  }, [onWebViewMessage, webviewProps]);
626
- if (lorikeetClient === null || loaded === null || loaded.identity !== identity || loaded.intendedTicketId !== resumingTicketId) return null;
765
+ if (clearing !== null) return /* @__PURE__ */ jsx(SafeAreaView, {
766
+ edges: [
767
+ "top",
768
+ "left",
769
+ "right"
770
+ ],
771
+ style: stylesheet.root,
772
+ children: /* @__PURE__ */ jsx(View, {
773
+ accessibilityElementsHidden: true,
774
+ importantForAccessibility: "no-hide-descendants",
775
+ pointerEvents: "none",
776
+ style: stylesheet.closed,
777
+ children: /* @__PURE__ */ jsx(LorikeetWidgetWebView, {
778
+ baseUrl: clearing.baseUrl,
779
+ onEvent: handleClearingEvent,
780
+ uri: clearing.uri,
781
+ webviewProps: {
782
+ ...clearing.webviewProps,
783
+ style: stylesheet.webView
784
+ }
785
+ }, `logout|${clearing.attempt}`)
786
+ })
787
+ });
788
+ if (lorikeetClient === null || loaded === null || loaded.identity !== identity || loaded.intendedTicketId !== resumingTicketId || loggedOutIdentity === identity) return null;
789
+ const isClosed = closed !== null;
627
790
  return /* @__PURE__ */ jsx(SafeAreaView, {
628
791
  edges: [
629
792
  "top",
@@ -633,45 +796,65 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
633
796
  style: stylesheet.root,
634
797
  children: /* @__PURE__ */ jsxs(View, {
635
798
  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);
799
+ children: [
800
+ hideHeader && closed !== "minimised" ? /* @__PURE__ */ jsx(LorikeetWidgetHeader, {
801
+ onIconPress: startNewChat,
802
+ style: styles === null || styles === void 0 ? void 0 : styles.header,
803
+ title: (_customisations$title = customisations === null || customisations === void 0 ? void 0 : customisations.title) !== null && _customisations$title !== void 0 ? _customisations$title : "Lorikeet AI Agent"
804
+ }) : null,
805
+ closed === "minimised" ? /* @__PURE__ */ jsx(TouchableOpacity, {
806
+ onPress: reopen,
807
+ style: stylesheet.closedControl,
808
+ children: /* @__PURE__ */ jsx(Text, {
809
+ style: stylesheet.headerText,
810
+ children: "Reopen chat"
811
+ })
812
+ }) : null,
813
+ !hideHeader && closed === "ended" ? /* @__PURE__ */ jsx(TouchableOpacity, {
814
+ onPress: startNewChat,
815
+ style: stylesheet.closedControl,
816
+ children: /* @__PURE__ */ jsx(Text, {
817
+ style: stylesheet.headerText,
818
+ children: "Start new chat"
819
+ })
820
+ }) : null,
821
+ /* @__PURE__ */ jsx(View, {
822
+ accessibilityElementsHidden: isClosed,
823
+ importantForAccessibility: isClosed ? "no-hide-descendants" : "auto",
824
+ pointerEvents: isClosed ? "none" : "auto",
825
+ style: isClosed ? stylesheet.closed : stylesheet.webView,
826
+ children: /* @__PURE__ */ jsx(LorikeetWidgetWebView, {
827
+ baseUrl: lorikeetClient.baseUrl,
828
+ onEvent: handleEvent,
829
+ ref: webViewRef,
830
+ uri: loaded.uri,
831
+ webviewProps: {
832
+ allowsInlineMediaPlayback: true,
833
+ mediaPlaybackRequiresUserAction: false,
834
+ scalesPageToFit: true,
835
+ startInLoadingState: true,
836
+ style: stylesheet.webView,
837
+ ...webviewProps,
838
+ onMessage: handleMessage,
839
+ onRenderProcessGone: (event) => {
840
+ var _webviewProps$onRende;
841
+ if (generation === generationRef.current) reload();
842
+ webviewProps === null || webviewProps === void 0 || (_webviewProps$onRende = webviewProps.onRenderProcessGone) === null || _webviewProps$onRende === void 0 || _webviewProps$onRende.call(webviewProps, event);
843
+ },
844
+ onContentProcessDidTerminate: (event) => {
845
+ var _webviewProps$onConte;
846
+ if (generation === generationRef.current) reload();
847
+ webviewProps === null || webviewProps === void 0 || (_webviewProps$onConte = webviewProps.onContentProcessDidTerminate) === null || _webviewProps$onConte === void 0 || _webviewProps$onConte.call(webviewProps, event);
848
+ }
667
849
  }
668
- }
669
- }, `${identity}|${resumingTicketId !== null && resumingTicketId !== void 0 ? resumingTicketId : ""}|${generation}`)
670
- })]
850
+ }, `${identity}|${resumingTicketId !== null && resumingTicketId !== void 0 ? resumingTicketId : ""}|${generation}`)
851
+ })
852
+ ]
671
853
  })
672
854
  });
673
- }
855
+ });
856
+ LorikeetWidgetView.displayName = "LorikeetWidgetView";
674
857
 
675
858
  //#endregion
676
- export { LORIKEET_ERROR_CODES, LorikeetClient, LorikeetWidgetHeader, LorikeetWidgetView, LorikeetWidgetWebView, isUnusableConversation, isUnusableConversationCode };
859
+ export { LOGOUT_CONFIRMATION_TIMEOUT_MS, LORIKEET_ERROR_CODES, LorikeetClient, LorikeetWidgetHeader, LorikeetWidgetView, LorikeetWidgetWebView, isUnusableConversation, isUnusableConversationCode };
677
860
  //# sourceMappingURL=index.js.map