@lorikeetai/react-native-sdk 2.0.0 → 2.2.0-beta.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/README.md CHANGED
@@ -40,9 +40,6 @@ major. Widening them is a change that needs testing, not a version bump.
40
40
  - **`webviewProps` now reach the WebView.** The drop-in previously discarded
41
41
  them. If you were passing props that silently did nothing, they take effect
42
42
  now.
43
- - **Lazy ticket creation.** A ticket is no longer created when the widget opens,
44
- which changes when conversations appear in your Lorikeet queue and in anything
45
- counting them.
46
43
 
47
44
  ## Basic Usage
48
45
 
@@ -52,7 +49,7 @@ import { LorikeetWidgetView } from '@lorikeetai/react-native-sdk'
52
49
  export default function Screen() {
53
50
  return (
54
51
  <LorikeetWidgetView
55
- publicKey="wk_xxx"
52
+ publicKey="your-public-key"
56
53
  authToken="your-auth-token"
57
54
  onWebViewMessage={(event) => console.log(event.nativeEvent.data)}
58
55
  />
@@ -62,7 +59,9 @@ export default function Screen() {
62
59
 
63
60
  **Props:**
64
61
 
65
- - `publicKey` (required): The public key for your Lorikeet account
62
+ - `publicKey` (required): The public key issued for your Lorikeet account. Use the value you were
63
+ given rather than matching a shape: the format has changed over time, and keys issued under
64
+ earlier ones keep working
66
65
  - `authToken` (required): Authentication token for the user
67
66
  - `baseUrl` (optional): Base URL for the Lorikeet API (defaults to `https://chat.lorikeetcx.ai`)
68
67
  - `customisations` (optional): Widget customisation options. Exported as
@@ -79,6 +78,48 @@ export default function Screen() {
79
78
  defaults for media playback, scaling and the loading state can all be
80
79
  overridden; `onMessage` is additive, so the SDK reads the bridge first and
81
80
  then calls yours
81
+ - `avoidKeyboard` (optional): Pads the widget clear of the keyboard. Defaults to
82
+ `true`. See [The keyboard](#the-keyboard)
83
+
84
+ ## The keyboard
85
+
86
+ Android apps drawn edge to edge, the default for current Expo and React Native
87
+ apps, no longer resize their window for the keyboard. `adjustResize` in the
88
+ manifest does not change that. Nothing shrinks the WebView, so the keyboard
89
+ covers the composer and the page is never told the keyboard is there.
90
+
91
+ `LorikeetWidgetView` handles this itself: it wraps its content in a
92
+ `KeyboardAvoidingView` with `behavior="padding"` on both platforms, and measures
93
+ its own position so it works below a navigation header or tab bar. On iOS the
94
+ same padding keeps the page's header on screen. If your screen already makes
95
+ room for the keyboard, pass `avoidKeyboard={false}`, or the widget is padded
96
+ twice.
97
+
98
+ If you compose the parts yourself, the keyboard is yours. Wrap them in a
99
+ `KeyboardAvoidingView` with `behavior="padding"` on both platforms, and set
100
+ `keyboardVerticalOffset` to their distance from the top of the screen.
101
+ `apps/rn-widget-demo/src/components/WidgetPane.tsx` measures it with
102
+ `measureInWindow`. Check it on an Android device: an iOS simulator will not
103
+ show the problem.
104
+
105
+ ## Which header you get
106
+
107
+ `hideHeader` decides whose header the customer sees, not whether there is one.
108
+ `LorikeetWidgetView` defaults it to `true`.
109
+
110
+ | Value | The chat page's header | The SDK's own header |
111
+ | ------------------------------ | ---------------------- | -------------------------------------------------- |
112
+ | `false` | Shown | Not rendered |
113
+ | `true` (the drop-in's default) | Hidden | Rendered, with a new-chat control and nothing else |
114
+
115
+ The page's header is the one carrying the controls for leaving a conversation.
116
+ The SDK's carries a new-chat control and nothing else, so under the drop-in's
117
+ default a host that neither sets `hideHeader: false` nor builds its own header
118
+ leaves the customer no way out of the chat.
119
+
120
+ Which controls the page's header offers, and what they are called, depends on
121
+ the account. Build against "the page's own controls" rather than a fixed set,
122
+ and ask us what your account serves before reproducing them.
82
123
 
83
124
  ## Resuming a conversation
84
125
 
@@ -101,7 +142,7 @@ useEffect(() => {
101
142
 
102
143
  return (
103
144
  <LorikeetWidgetView
104
- publicKey="wk_xxx"
145
+ publicKey="your-public-key"
105
146
  authToken={token}
106
147
  ticketId={resume.ticketId}
107
148
  session={resume.session}
@@ -154,6 +195,43 @@ not learn two vocabularies.
154
195
  `preload_complete` is in the union but cannot reach React Native: the preload
155
196
  page posts it to a web parent only. Do not wait on it.
156
197
 
198
+ ## Minimise, end and new chat
199
+
200
+ `close` means the page hid itself. It may carry a `reason`: a minimise keeps the
201
+ conversation and offers a control that reopens it, an end offers one that starts a
202
+ replacement. Treat an absent reason as an end, because that is what the widget
203
+ does.
204
+
205
+ Do not unmount on `close`. The end and any CSAT rating are still in flight, and
206
+ tearing the page down cancels them. The widget hides the chat itself.
207
+
208
+ While `ticketId` is set, the new-chat control reloads that conversation rather
209
+ than replacing it. To start a new conversation, clear your stored `ticketId` and
210
+ `session` and stop passing them, so the next load falls back to `/widget`.
211
+
212
+ ## The widget handle
213
+
214
+ Pass a `ref` to `LorikeetWidgetView` to reach these.
215
+
216
+ ```tsx
217
+ const widget = useRef<LorikeetWidgetHandle>(null)
218
+
219
+ widget.current?.getConversation() // { minimised, ticketId?, session? } | null
220
+ widget.current?.logout()
221
+ widget.current?.reopen()
222
+ ```
223
+
224
+ - `getConversation()` reports the conversation the widget is on, which is not
225
+ always the one the host passed in: a refused resume is replaced, and a new chat
226
+ replaces the conversation before it. It returns `null` until the page has
227
+ reported a ticket, and it can report a ticket with no session, so it supplements
228
+ the persistence events rather than replacing them.
229
+ - `logout()` clears this customer's chat from the device. See
230
+ [Logging out](#logging-out).
231
+ - `reopen()` shows a minimised conversation again, exactly as the customer left
232
+ it. Nothing reloads and nothing is reported. A no-op unless the widget is
233
+ minimised, so a host with its own launcher can call it unconditionally.
234
+
157
235
  ## Logging out
158
236
 
159
237
  Clearing your own stored `ticketId` and `session` is not enough. The chat keeps
@@ -194,11 +272,26 @@ previous customer's credentials still unlock. Call `logout()` again to retry.
194
272
  That obligation outlives the component. The SDK stores nothing, so a widget
195
273
  unmounted before the clear confirmed, by a screen change or an app restart,
196
274
  comes back with no memory of it and opens a chat normally, on a device whose
197
- cookies were never cleared. Record the failure alongside the conversation you
198
- already store, and call `logout()` again before mounting a chat for anyone
199
- else. We would rather own this than ask you to, and
200
- [ADP-3160](https://linear.app/lorikeet/issue/ADP-3160) is where that storage
201
- layer belongs.
275
+ cookies were never cleared.
276
+
277
+ So persist the logout before you start it, rather than recording that it failed.
278
+ Write down the `baseUrl`, `publicKey` and cookie-store `webviewProps` you are
279
+ clearing, read it at launch, and delete it only once `logged_out` has arrived.
280
+ Waiting for `logout_failed` is not enough on its own: unmounting cancels its
281
+ timer, so an app that dies inside the ten seconds reports nothing at all.
282
+
283
+ Recovery then has to clear that recorded target rather than whatever the next
284
+ launch happens to configure. The page drops only that public key's entry, and
285
+ settings such as `sharedCookiesEnabled` decide which cookie store it reaches, so
286
+ clearing another account or another store reports `logged_out` just the same.
287
+
288
+ It does not need the customer who left, though. The page needs no `authToken`, so
289
+ a host that no longer holds the departed customer's, which after a restart is the
290
+ normal case, can still clear the device by navigating a WebView to
291
+ `getLogoutUrl()` and waiting for its confirmation, as below. Do not reach for the
292
+ drop-in's `logout()` here: it clears for whichever credentials are in props and
293
+ then will not open a chat for them until they change, so calling it with the next
294
+ customer's props locks that customer out instead of the last one.
202
295
 
203
296
  If you compose the parts yourself, build the URL and navigate to it:
204
297
 
@@ -215,8 +308,7 @@ The anonymous customer's long-lived token is deliberately not among these
215
308
  events. It identifies the same anonymous customer for a year, this SDK offers
216
309
  no way to hand one back, and the web SDK keeps the same message inside the
217
310
  frame. A host that genuinely needs it can read the raw bridge, but storing it
218
- is storing a credential, and [ADP-3160](https://linear.app/lorikeet/issue/ADP-3160)
219
- is the work that should remove the need.
311
+ is storing a credential.
220
312
 
221
313
  ## Identity
222
314
 
@@ -305,10 +397,175 @@ WebView component that handles the widget content.
305
397
  - `webviewProps` (optional): Passed through to the webview component. The SDK
306
398
  adds external-link handling on `onNavigationStateChange` and then calls yours
307
399
 
400
+ ## Voice calls (beta)
401
+
402
+ `@lorikeetai/react-native-sdk/voice` runs a Lorikeet voice call inside your
403
+ app with native audio, so the customer can keep talking while the app is in
404
+ the background. It is a beta. Install it from the `beta` tag, and expect its
405
+ API to change before it is stable.
406
+
407
+ Nothing here touches the chat widget. Only the `voice` entry point loads
408
+ LiveKit, so an app that never imports it installs nothing new and needs no
409
+ native rebuild.
410
+
411
+ ### Install
412
+
413
+ The LiveKit packages are optional peer dependencies, so install them yourself:
414
+
415
+ ```bash
416
+ pnpm add @lorikeetai/react-native-sdk@beta \
417
+ @livekit/react-native@^3.1.0 \
418
+ @livekit/react-native-webrtc@^150.1.0 \
419
+ livekit-client@^2.20.0
420
+ ```
421
+
422
+ `@livekit/react-native` depends on `@livekit/components-react`, which needs
423
+ `react-dom`. Install the `react-dom` version that matches your `react`, or the
424
+ package manager may pick a newer one that your `react` does not satisfy.
425
+
426
+ Do not install `@livekit/krisp-noise-filter`. The call does not use it, and
427
+ its license is not open source.
428
+
429
+ Voice requires React Native 0.79 or later, though the chat widget's peer range
430
+ is unchanged. The `voice` subpath is in the package's `exports` map, and 0.79
431
+ ships the first Metro (0.82) that reads that map by default. TypeScript reads
432
+ it with `moduleResolution` set to `bundler` or `node16`, and Expo's base
433
+ `tsconfig` uses `bundler`.
434
+
435
+ ### Native setup
436
+
437
+ These are native modules. Expo Go does not include them, so use a
438
+ [development build](https://docs.expo.dev/develop/development-builds/introduction/)
439
+ on Expo, and rebuild native code after installing.
440
+
441
+ With Expo SDK 56 or later, add the two config plugins, the iOS microphone
442
+ string and audio background mode, and the Android audio permissions to
443
+ `app.json`, then run `expo prebuild`:
444
+
445
+ ```bash
446
+ npx expo install @livekit/react-native-expo-plugin @config-plugins/react-native-webrtc
447
+ ```
448
+
449
+ ```json
450
+ {
451
+ "expo": {
452
+ "platforms": ["ios", "android"],
453
+ "plugins": [
454
+ "@livekit/react-native-expo-plugin",
455
+ "@config-plugins/react-native-webrtc"
456
+ ],
457
+ "ios": {
458
+ "infoPlist": {
459
+ "NSMicrophoneUsageDescription": "Talk to our support team.",
460
+ "UIBackgroundModes": ["audio"]
461
+ }
462
+ },
463
+ "android": {
464
+ "permissions": [
465
+ "android.permission.RECORD_AUDIO",
466
+ "android.permission.MODIFY_AUDIO_SETTINGS"
467
+ ]
468
+ }
469
+ }
470
+ }
471
+ ```
472
+
473
+ Set `platforms` to iOS and Android. Expo treats an app with `react-dom`
474
+ installed as web-capable, so `expo export` would also try a web bundle, which
475
+ LiveKit's native modules cannot build.
476
+
477
+ Use version 1.1.0 or later of `@livekit/react-native-expo-plugin`. It adds the
478
+ `livekit/podspecs` CocoaPods source, which LiveKit's iOS pods need once
479
+ CocoaPods trunk stops accepting new pods on 2 December 2026.
480
+
481
+ Without Expo, follow LiveKit's
482
+ [installation steps](https://github.com/livekit/client-sdk-react-native#installation).
483
+ Add the `livekit/podspecs` source to your `Podfile`, call
484
+ `LiveKitReactNative.setup` in `MainApplication` and `LivekitReactNative.setup`
485
+ in `AppDelegate`, and add the same permissions and background mode as above.
486
+
487
+ On Android, the microphone keeps working in the background only while your
488
+ app runs a foreground service of type `microphone`, which the SDK does not
489
+ start for you. Without one, the agent can still be heard, but the customer's
490
+ microphone goes silent when the app leaves the foreground.
491
+
492
+ ### Usage
493
+
494
+ Your backend creates the session with
495
+ `POST /v1/conversation/voice/web/create`, using your API credentials, and
496
+ returns its `{ conversationId, wsUrl, token }` to the app. Never put your API
497
+ credentials in the app.
498
+
499
+ ```tsx
500
+ import { Button, Text, View } from 'react-native'
501
+
502
+ import { useLorikeetCall } from '@lorikeetai/react-native-sdk/voice'
503
+
504
+ export function SupportCall() {
505
+ const call = useLorikeetCall({
506
+ getSession: async () => {
507
+ const response = await fetch('https://api.example.com/support/call', {
508
+ method: 'POST',
509
+ })
510
+ if (response.status === 404) return null
511
+ if (!response.ok) throw new Error(`Call session: ${response.status}`)
512
+ return response.json()
513
+ },
514
+ onError: (cause) => console.warn('Support call failed', cause),
515
+ })
516
+
517
+ return (
518
+ <View>
519
+ <Text>{call.state}</Text>
520
+ {call.state === 'idle' || call.state === 'error' ? (
521
+ <Button title="Start call" onPress={() => void call.start()} />
522
+ ) : (
523
+ <Button title="End call" onPress={call.end} />
524
+ )}
525
+ </View>
526
+ )
527
+ }
528
+ ```
529
+
530
+ `useLorikeetCall({ getSession, onError })` returns:
531
+
532
+ - `state`: `idle`, `starting`, `connecting`, `active` or `error`, as in the web
533
+ Voice Widget. `starting` covers asking for the microphone and fetching the
534
+ session, and `connecting` covers joining the call.
535
+ - `error`: why the call is in `error`. `microphone_denied` means the customer
536
+ refused the microphone, or it is off for the app in the device's settings.
537
+ `not_available` means `getSession` returned `null`. `failed` is anything
538
+ else, and `onError` receives its cause. `microphone_system_denied` is the web
539
+ Voice Widget's code for a browser blocked by the operating system, and a
540
+ mobile device never reports it.
541
+ - `start()`: start a call from `idle` or `error`. The hook asks for microphone
542
+ access before it calls `getSession`, so a refusal starts no conversation.
543
+ - `end()`: hang up, or abandon a call still starting. The call also ends, and
544
+ returns to `idle`, when the agent leaves.
545
+ - `ended`: whether the last call ended after it began connecting.
546
+ - `microphoneMuted` and `setMicrophoneMuted(muted)`: mute the customer.
547
+ - `speakerMuted` and `setSpeakerMuted(muted)`: mute the agent.
548
+
549
+ The call has no live transcript. The conversation is in Lorikeet under its
550
+ `conversationId`.
551
+
552
+ If your app already calls LiveKit's `registerGlobals()`, the hook leaves your
553
+ configuration alone. Otherwise the hook calls it when the first call starts.
554
+
555
+ The hook owns the native audio session for the length of a call and stops it
556
+ when the call ends. An app that runs its own LiveKit call at the same time
557
+ should not use the hook.
558
+
308
559
  ## Development
309
560
 
310
- - Build: `pnpm -w turbo build --filter=@lorikeetai/react-native-sdk`
311
- - Lint: `pnpm --filter=@lorikeetai/react-native-sdk lint`
561
+ This package is outside the root pnpm workspace, on its own lockfile, so run these from `packages/react-native-widget` after `pnpm install` there.
562
+
563
+ - Build: `pnpm build`
564
+ - Test: `pnpm test`
565
+ - Typecheck: `pnpm typecheck`
566
+ - Lint: `pnpm lint`. It uses the root's oxlint and oxfmt, so run `pnpm install` at the repository root first.
567
+
568
+ To release, bump `version` in `package.json` and add its `CHANGELOG.md` section in the same pull request. See `docs/engineering/sdk-releases.md`.
312
569
 
313
570
  ## License
314
571
 
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- //#region \0rolldown/runtime.js
2
+ // \0rolldown/runtime.js
3
3
  var __create = Object.create;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -25,7 +25,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  enumerable: true
26
26
  }) : target, mod));
27
27
 
28
- //#endregion
29
28
  let react = require("react");
30
29
  let react_native = require("react-native");
31
30
  let react_native_safe_area_context = require("react-native-safe-area-context");
@@ -34,7 +33,7 @@ let react_native_svg = require("react-native-svg");
34
33
  react_native_svg = __toESM(react_native_svg, 1);
35
34
  let react_jsx_runtime = require("react/jsx-runtime");
36
35
 
37
- //#region src/bridge.ts
36
+ // src/bridge.ts
38
37
  /**
39
38
  * The public key, base URL and token are one unit, and a conversation belongs to
40
39
  * the credentials that created it. Stamping an observation with them is what
@@ -54,8 +53,7 @@ const resumableConversation = (observed, identity, intendedTicketId) => {
54
53
  };
55
54
  };
56
55
 
57
- //#endregion
58
- //#region src/encode.ts
56
+ // src/encode.ts
59
57
  const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
60
58
  /**
61
59
  * Base64 of a string's UTF-8 bytes.
@@ -82,8 +80,7 @@ function base64EncodeUtf8(value) {
82
80
  return encoded;
83
81
  }
84
82
 
85
- //#endregion
86
- //#region \0@oxc-project+runtime@0.147.0/helpers/esm/typeof.js
83
+ // \0@oxc-project+runtime@0.156.0/helpers/esm/typeof.js
87
84
  function _typeof(o) {
88
85
  "@babel/helpers - typeof";
89
86
  return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
@@ -93,8 +90,7 @@ function _typeof(o) {
93
90
  }, _typeof(o);
94
91
  }
95
92
 
96
- //#endregion
97
- //#region \0@oxc-project+runtime@0.147.0/helpers/esm/toPrimitive.js
93
+ // \0@oxc-project+runtime@0.156.0/helpers/esm/toPrimitive.js
98
94
  function toPrimitive(t, r) {
99
95
  if ("object" != _typeof(t) || !t) return t;
100
96
  var e = t[Symbol.toPrimitive];
@@ -106,15 +102,13 @@ function toPrimitive(t, r) {
106
102
  return ("string" === r ? String : Number)(t);
107
103
  }
108
104
 
109
- //#endregion
110
- //#region \0@oxc-project+runtime@0.147.0/helpers/esm/toPropertyKey.js
105
+ // \0@oxc-project+runtime@0.156.0/helpers/esm/toPropertyKey.js
111
106
  function toPropertyKey(t) {
112
107
  var i = toPrimitive(t, "string");
113
108
  return "symbol" == _typeof(i) ? i : i + "";
114
109
  }
115
110
 
116
- //#endregion
117
- //#region \0@oxc-project+runtime@0.147.0/helpers/esm/defineProperty.js
111
+ // \0@oxc-project+runtime@0.156.0/helpers/esm/defineProperty.js
118
112
  function _defineProperty(e, r, t) {
119
113
  return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
120
114
  value: t,
@@ -124,8 +118,7 @@ function _defineProperty(e, r, t) {
124
118
  }) : e[r] = t, e;
125
119
  }
126
120
 
127
- //#endregion
128
- //#region src/client.ts
121
+ // src/client.ts
129
122
  var LorikeetClient = class {
130
123
  constructor({ customisations, api_base, metadata, public_key }) {
131
124
  _defineProperty(this, "baseUrl", "https://chat.lorikeetcx.ai");
@@ -171,8 +164,7 @@ var LorikeetClient = class {
171
164
  }
172
165
  };
173
166
 
174
- //#endregion
175
- //#region src/errors.ts
167
+ // src/errors.ts
176
168
  /**
177
169
  * The chat page's error vocabulary, as it reaches a host in
178
170
  * `{ type: 'error', error: CODE }`. Mirrored here because the page publishes it
@@ -219,8 +211,7 @@ function isUnusableConversation(message) {
219
211
  return type === "error" && typeof error === "string" ? isUnusableConversationCode(error) : false;
220
212
  }
221
213
 
222
- //#endregion
223
- //#region src/events.ts
214
+ // src/events.ts
224
215
  const asRecord = (message) => typeof message === "object" && message !== null && !Array.isArray(message) ? message : null;
225
216
  const asNonEmptyString = (value) => typeof value === "string" && value !== "" ? value : null;
226
217
  /**
@@ -298,8 +289,7 @@ const resumedTicketIdFromUri = (uri) => {
298
289
  return (_segments$ = segments[2]) !== null && _segments$ !== void 0 ? _segments$ : null;
299
290
  };
300
291
 
301
- //#endregion
302
- //#region src/lifecycle.ts
292
+ // src/lifecycle.ts
303
293
  /**
304
294
  * The page gives its message stream ten attempts with exponential backoff
305
295
  * capped at 30s (`message-stream-utils.tsx`), so the budget runs out roughly
@@ -336,8 +326,7 @@ const shouldReloadAfterBackground = (backgroundedAt, now, thresholdMs = BACKGROU
336
326
  return elapsed >= thresholdMs;
337
327
  };
338
328
 
339
- //#endregion
340
- //#region src/links.ts
329
+ // src/links.ts
341
330
  /**
342
331
  * Schemes the WebView resolves for itself. None of them is a page the customer
343
332
  * asked to leave the app for, and the OS cannot open them anyway.
@@ -382,8 +371,7 @@ const isInternalNavigation = (url, baseUrl) => {
382
371
  return target.origin === base.origin;
383
372
  };
384
373
 
385
- //#endregion
386
- //#region src/message-circle-plus-icon.tsx
374
+ // src/message-circle-plus-icon.tsx
387
375
  const MessageCirclePlusIcon = ({ size = 24, color = "currentColor", strokeWidth = 2 }) => {
388
376
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native_svg.default, {
389
377
  fill: "none",
@@ -420,13 +408,13 @@ const MessageCirclePlusIcon = ({ size = 24, color = "currentColor", strokeWidth
420
408
  });
421
409
  };
422
410
 
423
- //#endregion
424
- //#region src/widget.tsx
411
+ // src/widget.tsx
425
412
  const stylesheet = react_native.StyleSheet.create({
426
413
  root: {
427
414
  flex: 1,
428
415
  backgroundColor: "#fff"
429
416
  },
417
+ keyboardAvoiding: { flex: 1 },
430
418
  container: {
431
419
  flex: 1,
432
420
  borderRadius: 8,
@@ -530,7 +518,7 @@ const LorikeetWidgetHeader = ({ title, style, icon = /* @__PURE__ */ (0, react_j
530
518
  ]
531
519
  });
532
520
  };
533
- const LorikeetWidgetView = (0, react.forwardRef)(function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, styles, onEvent, onWebViewMessage, ticketId, session, metadata, webviewProps }, ref) {
521
+ const LorikeetWidgetView = (0, react.forwardRef)(function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, styles, onEvent, onWebViewMessage, ticketId, session, metadata, webviewProps, avoidKeyboard = true }, ref) {
534
522
  var _customisations$hideH, _customisations$title;
535
523
  const [lorikeetClient, setLorikeetClient] = (0, react.useState)(null);
536
524
  const [loaded, setLoaded] = (0, react.useState)(null);
@@ -541,6 +529,15 @@ const LorikeetWidgetView = (0, react.forwardRef)(function LorikeetWidgetView({ p
541
529
  setGeneration((previous) => previous + 1);
542
530
  }, []);
543
531
  const webViewRef = (0, react.useRef)(null);
532
+ const containerRef = (0, react.useRef)(null);
533
+ const [keyboardOffset, setKeyboardOffset] = (0, react.useState)(0);
534
+ const measureKeyboardOffset = (0, react.useCallback)((event) => {
535
+ var _containerRef$current;
536
+ const { y } = event.nativeEvent.layout;
537
+ (_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 || _containerRef$current.measureInWindow((_x, windowY) => {
538
+ setKeyboardOffset(windowY - y);
539
+ });
540
+ }, []);
544
541
  const [refusedTicketId, setRefusedTicketId] = (0, react.useState)(null);
545
542
  const observedRef = (0, react.useRef)(null);
546
543
  const backgroundedAtRef = (0, react.useRef)(null);
@@ -828,68 +825,75 @@ const LorikeetWidgetView = (0, react.forwardRef)(function LorikeetWidgetView({ p
828
825
  "right"
829
826
  ],
830
827
  style: stylesheet.root,
831
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
832
- style: stylesheet.container,
833
- children: [
834
- hideHeader && closed !== "minimised" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LorikeetWidgetHeader, {
835
- onIconPress: startNewChat,
836
- style: styles === null || styles === void 0 ? void 0 : styles.header,
837
- title: (_customisations$title = customisations === null || customisations === void 0 ? void 0 : customisations.title) !== null && _customisations$title !== void 0 ? _customisations$title : "Lorikeet AI Agent"
838
- }) : null,
839
- closed === "minimised" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.TouchableOpacity, {
840
- onPress: reopen,
841
- style: stylesheet.closedControl,
842
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
843
- style: stylesheet.headerText,
844
- children: "Reopen chat"
845
- })
846
- }) : null,
847
- !hideHeader && closed === "ended" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.TouchableOpacity, {
848
- onPress: startNewChat,
849
- style: stylesheet.closedControl,
850
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
851
- style: stylesheet.headerText,
852
- children: "Start new chat"
853
- })
854
- }) : null,
855
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
856
- accessibilityElementsHidden: isClosed,
857
- importantForAccessibility: isClosed ? "no-hide-descendants" : "auto",
858
- pointerEvents: isClosed ? "none" : "auto",
859
- style: isClosed ? stylesheet.closed : stylesheet.webView,
860
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LorikeetWidgetWebView, {
861
- baseUrl: lorikeetClient.baseUrl,
862
- onEvent: handleEvent,
863
- ref: webViewRef,
864
- uri: loaded.uri,
865
- webviewProps: {
866
- allowsInlineMediaPlayback: true,
867
- mediaPlaybackRequiresUserAction: false,
868
- scalesPageToFit: true,
869
- startInLoadingState: true,
870
- style: stylesheet.webView,
871
- ...webviewProps,
872
- onMessage: handleMessage,
873
- onRenderProcessGone: (event) => {
874
- var _webviewProps$onRende;
875
- if (generation === generationRef.current) reload();
876
- webviewProps === null || webviewProps === void 0 || (_webviewProps$onRende = webviewProps.onRenderProcessGone) === null || _webviewProps$onRende === void 0 || _webviewProps$onRende.call(webviewProps, event);
877
- },
878
- onContentProcessDidTerminate: (event) => {
879
- var _webviewProps$onConte;
880
- if (generation === generationRef.current) reload();
881
- webviewProps === null || webviewProps === void 0 || (_webviewProps$onConte = webviewProps.onContentProcessDidTerminate) === null || _webviewProps$onConte === void 0 || _webviewProps$onConte.call(webviewProps, event);
828
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.KeyboardAvoidingView, {
829
+ behavior: "padding",
830
+ enabled: avoidKeyboard,
831
+ keyboardVerticalOffset: keyboardOffset,
832
+ onLayout: measureKeyboardOffset,
833
+ style: stylesheet.keyboardAvoiding,
834
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
835
+ ref: containerRef,
836
+ style: stylesheet.container,
837
+ children: [
838
+ hideHeader && closed !== "minimised" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LorikeetWidgetHeader, {
839
+ onIconPress: startNewChat,
840
+ style: styles === null || styles === void 0 ? void 0 : styles.header,
841
+ title: (_customisations$title = customisations === null || customisations === void 0 ? void 0 : customisations.title) !== null && _customisations$title !== void 0 ? _customisations$title : "Lorikeet AI Agent"
842
+ }) : null,
843
+ closed === "minimised" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.TouchableOpacity, {
844
+ onPress: reopen,
845
+ style: stylesheet.closedControl,
846
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
847
+ style: stylesheet.headerText,
848
+ children: "Reopen chat"
849
+ })
850
+ }) : null,
851
+ !hideHeader && closed === "ended" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.TouchableOpacity, {
852
+ onPress: startNewChat,
853
+ style: stylesheet.closedControl,
854
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
855
+ style: stylesheet.headerText,
856
+ children: "Start new chat"
857
+ })
858
+ }) : null,
859
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
860
+ accessibilityElementsHidden: isClosed,
861
+ importantForAccessibility: isClosed ? "no-hide-descendants" : "auto",
862
+ pointerEvents: isClosed ? "none" : "auto",
863
+ style: isClosed ? stylesheet.closed : stylesheet.webView,
864
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LorikeetWidgetWebView, {
865
+ baseUrl: lorikeetClient.baseUrl,
866
+ onEvent: handleEvent,
867
+ ref: webViewRef,
868
+ uri: loaded.uri,
869
+ webviewProps: {
870
+ allowsInlineMediaPlayback: true,
871
+ mediaPlaybackRequiresUserAction: false,
872
+ scalesPageToFit: true,
873
+ startInLoadingState: true,
874
+ style: stylesheet.webView,
875
+ ...webviewProps,
876
+ onMessage: handleMessage,
877
+ onRenderProcessGone: (event) => {
878
+ var _webviewProps$onRende;
879
+ if (generation === generationRef.current) reload();
880
+ webviewProps === null || webviewProps === void 0 || (_webviewProps$onRende = webviewProps.onRenderProcessGone) === null || _webviewProps$onRende === void 0 || _webviewProps$onRende.call(webviewProps, event);
881
+ },
882
+ onContentProcessDidTerminate: (event) => {
883
+ var _webviewProps$onConte;
884
+ if (generation === generationRef.current) reload();
885
+ webviewProps === null || webviewProps === void 0 || (_webviewProps$onConte = webviewProps.onContentProcessDidTerminate) === null || _webviewProps$onConte === void 0 || _webviewProps$onConte.call(webviewProps, event);
886
+ }
882
887
  }
883
- }
884
- }, `${identity}|${resumingTicketId !== null && resumingTicketId !== void 0 ? resumingTicketId : ""}|${generation}`)
885
- })
886
- ]
888
+ }, `${identity}|${resumingTicketId !== null && resumingTicketId !== void 0 ? resumingTicketId : ""}|${generation}`)
889
+ })
890
+ ]
891
+ })
887
892
  })
888
893
  });
889
894
  });
890
895
  LorikeetWidgetView.displayName = "LorikeetWidgetView";
891
896
 
892
- //#endregion
893
897
  exports.LOGOUT_CONFIRMATION_TIMEOUT_MS = LOGOUT_CONFIRMATION_TIMEOUT_MS;
894
898
  exports.LORIKEET_ERROR_CODES = LORIKEET_ERROR_CODES;
895
899
  exports.LorikeetClient = LorikeetClient;