@lorikeetai/react-native-sdk 2.0.0-alpha.1 → 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,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,13 @@ 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
+ const LOGOUT_CONFIRMATION_TIMEOUT_MS = 1e4;
288
+ /**
258
289
  * Elapsed time is the only signal available: the bridge reports `ready`,
259
290
  * `error` and the persistence messages, but never stream health, so the SDK
260
291
  * cannot ask whether the connection actually survived.
@@ -381,6 +412,7 @@ const stylesheet = StyleSheet.create({
381
412
  textAlign: "center",
382
413
  fontSize: 16
383
414
  },
415
+ closedControl: { padding: 16 },
384
416
  webView: { flex: 1 },
385
417
  closed: {
386
418
  flex: 1,
@@ -459,8 +491,8 @@ const LorikeetWidgetHeader = ({ title, style, icon = /* @__PURE__ */ jsx(Message
459
491
  ]
460
492
  });
461
493
  };
462
- function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, styles, onEvent, onWebViewMessage, ticketId, session, metadata, webviewProps }) {
463
- var _customisations$title;
494
+ const LorikeetWidgetView = forwardRef(function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, styles, onEvent, onWebViewMessage, ticketId, session, metadata, webviewProps }, ref) {
495
+ var _customisations$hideH, _customisations$title;
464
496
  const [lorikeetClient, setLorikeetClient] = useState(null);
465
497
  const [loaded, setLoaded] = useState(null);
466
498
  const [generation, setGeneration] = useState(0);
@@ -485,7 +517,12 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
485
517
  });
486
518
  const resumingTicketId = ticketId !== void 0 && ticketId !== refusedTicketId ? ticketId : void 0;
487
519
  const [attemptedTicketId, setAttemptedTicketId] = useState(void 0);
488
- const [closed, setClosed] = useState(false);
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;
489
526
  const load = useCallback((client, forIdentity, intendedTicketId, resume, force = false) => {
490
527
  const next = client.getWebviewUrl({
491
528
  token: authToken,
@@ -493,7 +530,7 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
493
530
  session: resume === null || resume === void 0 ? void 0 : resume.session
494
531
  });
495
532
  const unchanged = uriRef.current === next;
496
- if (loadedForRef.current.identity !== forIdentity || loadedForRef.current.intendedTicketId !== intendedTicketId) setClosed(false);
533
+ if (loadedForRef.current.identity !== forIdentity || loadedForRef.current.intendedTicketId !== intendedTicketId) setClosed(null);
497
534
  uriRef.current = next;
498
535
  loadedForRef.current = {
499
536
  identity: forIdentity,
@@ -522,10 +559,12 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
522
559
  metadata,
523
560
  customisations: {
524
561
  ...customisations,
525
- hideHeader: (customisations === null || customisations === void 0 ? void 0 : customisations.hideHeader) === void 0 ? true : customisations.hideHeader
562
+ hideHeader
526
563
  }
527
564
  });
528
565
  setLorikeetClient(lorikeetClient);
566
+ if (clearing !== null || loggedOutIdentity === identity) return;
567
+ if (loggedOutIdentity !== null) setLoggedOutIdentity(null);
529
568
  const resume = resumingTicketId !== void 0 ? {
530
569
  session,
531
570
  ticketId: resumingTicketId
@@ -535,13 +574,108 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
535
574
  publicKey,
536
575
  baseUrl,
537
576
  authToken,
577
+ clearing,
538
578
  customisations,
579
+ hideHeader,
580
+ loggedOutIdentity,
539
581
  metadata,
540
582
  session,
541
583
  resumingTicketId,
542
584
  identity,
543
585
  load
544
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]);
545
679
  const reload = useCallback(() => {
546
680
  const resume = resumableConversation(observedRef.current, identity, resumingTicketId);
547
681
  if (lorikeetClient !== null && resume !== null) {
@@ -573,7 +707,7 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
573
707
  };
574
708
  }, [reload]);
575
709
  const startNewChat = useCallback(() => {
576
- setClosed(false);
710
+ setClosed(null);
577
711
  if (lorikeetClient !== null && resumingTicketId === void 0) {
578
712
  observedRef.current = null;
579
713
  load(lorikeetClient, identity, void 0, null, true);
@@ -599,7 +733,7 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
599
733
  intendedTicketId: resumingTicketId
600
734
  };
601
735
  }
602
- if (event.type === "close") setClosed(true);
736
+ if (event.type === "close") setClosed(event.reason === "minimised" ? "minimised" : "ended");
603
737
  if (event.type === "new_chat") startNewChat();
604
738
  if (event.type === "error" && attemptedTicketId !== void 0 && isUnusableConversationCode(event.error)) {
605
739
  observedRef.current = null;
@@ -623,7 +757,31 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
623
757
  onWebViewMessage === null || onWebViewMessage === void 0 || onWebViewMessage(event);
624
758
  webviewProps === null || webviewProps === void 0 || (_webviewProps$onMessa2 = webviewProps.onMessage) === null || _webviewProps$onMessa2 === void 0 || _webviewProps$onMessa2.call(webviewProps, event);
625
759
  }, [onWebViewMessage, webviewProps]);
626
- if (lorikeetClient === null || loaded === null || loaded.identity !== identity || loaded.intendedTicketId !== resumingTicketId) return null;
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;
627
785
  return /* @__PURE__ */ jsx(SafeAreaView, {
628
786
  edges: [
629
787
  "top",
@@ -633,44 +791,64 @@ function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, sty
633
791
  style: stylesheet.root,
634
792
  children: /* @__PURE__ */ jsxs(View, {
635
793
  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);
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
+ }
667
844
  }
668
- }
669
- }, `${identity}|${resumingTicketId !== null && resumingTicketId !== void 0 ? resumingTicketId : ""}|${generation}`)
670
- })]
845
+ }, `${identity}|${resumingTicketId !== null && resumingTicketId !== void 0 ? resumingTicketId : ""}|${generation}`)
846
+ })
847
+ ]
671
848
  })
672
849
  });
673
- }
850
+ });
851
+ LorikeetWidgetView.displayName = "LorikeetWidgetView";
674
852
 
675
853
  //#endregion
676
854
  export { LORIKEET_ERROR_CODES, LorikeetClient, LorikeetWidgetHeader, LorikeetWidgetView, LorikeetWidgetWebView, isUnusableConversation, isUnusableConversationCode };