@lorikeetai/react-native-sdk 1.2.0 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LorikeetCX.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,12 +2,50 @@
2
2
 
3
3
  SDK containing embeddable React Native components that renders the LorikeetCX web widget inside a react-native-webview.
4
4
 
5
+ > **Alpha.** The surface can still change between releases. Resume, storage and
6
+ > token refresh are host responsibilities today; see [Resuming a
7
+ > conversation](#resuming-a-conversation) and [Identity](#identity).
8
+
5
9
  ## Installation
6
10
 
11
+ The three native modules are peer dependencies, so install them alongside the
12
+ SDK. Name them explicitly rather than relying on your package manager to add
13
+ them for you: npm and pnpm auto-install peers, but **neither Yarn Classic nor
14
+ Yarn Berry does, and neither fails the install.** Classic prints `has unmet peer
15
+ dependency`, Berry prints `YN0002 ... doesn't provide`, and both exit
16
+ successfully, and the failure surfaces later, when Metro cannot resolve the
17
+ module.
18
+ Metro reports one missing module at a time, so install all three at once.
19
+
20
+ Name the SDK's version too. Alphas go to the `alpha` dist tag, not `latest`, so a
21
+ bare `pnpm add @lorikeetai/react-native-sdk` installs the 1.x line, and no
22
+ semver range reaches a prerelease either.
23
+
7
24
  ```bash
8
- pnpm add @lorikeetai/react-native-sdk
25
+ pnpm add @lorikeetai/react-native-sdk@2.0.0-alpha.2 \
26
+ react-native-webview@^13.15.0 \
27
+ react-native-safe-area-context@^5.6.0 \
28
+ react-native-svg@^15.13.0
9
29
  ```
10
30
 
31
+ These are native modules, so rebuild rather than just restarting the bundler:
32
+ `pod install` for iOS, a Gradle sync for Android.
33
+
34
+ The peer ranges are bounded to the major versions this release is built and
35
+ tested against, so an unpinned install cannot silently resolve to an untested
36
+ major. Widening them is a change that needs testing, not a version bump.
37
+
38
+ ### Upgrading from 1.x
39
+
40
+ - **Peer dependencies.** The three modules above used to arrive transitively.
41
+ Declare them yourself, and rebuild native.
42
+ - **`webviewProps` now reach the WebView.** The drop-in previously discarded
43
+ them. If you were passing props that silently did nothing, they take effect
44
+ now.
45
+ - **Lazy ticket creation.** A ticket is no longer created when the widget opens,
46
+ which changes when conversations appear in your Lorikeet queue and in anything
47
+ counting them.
48
+
11
49
  ## Basic Usage
12
50
 
13
51
  ```tsx
@@ -29,9 +67,204 @@ export default function Screen() {
29
67
  - `publicKey` (required): The public key for your Lorikeet account
30
68
  - `authToken` (required): Authentication token for the user
31
69
  - `baseUrl` (optional): Base URL for the Lorikeet API (defaults to `https://chat.lorikeetcx.ai`)
32
- - `customisations` (optional): Widget customization options from `@lorikeetai/web-sdk`
70
+ - `customisations` (optional): Widget customisation options. Exported as
71
+ `Customisations` from this package, so there is nothing else to install
33
72
  - `styles` (optional): Style overrides for the header text
34
- - `onWebViewMessage` (optional): Handler for messages from the webview
73
+ - `onEvent` (optional): What the page reported, as typed events. See [Events](#events)
74
+ - `onWebViewMessage` (optional): Handler for raw messages from the webview.
75
+ Deprecated in favour of `onEvent`, and still called
76
+ - `ticketId` (optional): Resume this conversation instead of starting a new one
77
+ - `session` (optional): The session token that authorises `ticketId`
78
+ - `metadata` (optional): Arbitrary context attached to the conversation, for
79
+ routing or for the agent to read. Survives a resume
80
+ - `webviewProps` (optional): Forwarded to the underlying WebView. The SDK's
81
+ defaults for media playback, scaling and the loading state can all be
82
+ overridden; `onMessage` is additive, so the SDK reads the bridge first and
83
+ then calls yours
84
+
85
+ ## Resuming a conversation
86
+
87
+ Without `ticketId`, every mount loads `/widget`, and `/widget` mints a new
88
+ ticket. A customer who closes the app and comes back gets an empty chat while
89
+ their previous ticket stays open.
90
+
91
+ The widget reports what you need to resume over the WebView bridge. Store both
92
+ values, scoped to the customer, somewhere that survives an app restart, and pass
93
+ them back on the next mount:
94
+
95
+ ```tsx
96
+ const [resume, setResume] = useState<{ ticketId?: string; session?: string }>(
97
+ {},
98
+ )
99
+
100
+ useEffect(() => {
101
+ void loadResumeForCustomer(customerId).then(setResume)
102
+ }, [customerId])
103
+
104
+ return (
105
+ <LorikeetWidgetView
106
+ publicKey="wk_xxx"
107
+ authToken={token}
108
+ ticketId={resume.ticketId}
109
+ session={resume.session}
110
+ onWebViewMessage={(event) => {
111
+ const message = JSON.parse(event.nativeEvent.data)
112
+ if (message.type === 'persistLorikeetTicketId') {
113
+ void saveResumeForCustomer(customerId, { ticketId: message.ticketId })
114
+ }
115
+ if (message.type === 'persistLorikeetSession') {
116
+ void saveResumeForCustomer(customerId, { session: message.session })
117
+ }
118
+ }}
119
+ />
120
+ )
121
+ ```
122
+
123
+ Pass both or neither. `session` authorises exactly the ticket it was issued
124
+ for, and a resume without it fails with `CUSTOMER_ID_MISSING`.
125
+
126
+ The SDK does not store these for you, and it does not clear them. On logout,
127
+ clear your own copy and call the widget's `logout()`; see
128
+ [Logging out](#logging-out).
129
+
130
+ While `ticketId` is set, the header's new-chat icon reloads that conversation
131
+ rather than starting a new one. To start a new one, clear your stored
132
+ `ticketId` and `session` and let the next mount fall back to `/widget`.
133
+
134
+ ## Events
135
+
136
+ `onEvent` reports what the page said, as a `LorikeetWidgetEvent`:
137
+
138
+ | Event | Means |
139
+ | --------------------------------- | --------------------------------------------------------------- |
140
+ | `ready` | The conversation is usable |
141
+ | `ticket_created` | A ticket this page load created. Once per ticket, never a resume |
142
+ | `customer_message` | The customer sent something |
143
+ | `agent_message` | The agent replied. `markUnread` is the page asking for an unread indicator |
144
+ | `close` | The page hid itself, with a `reason` of `ended_by_customer` or `minimised` |
145
+ | `conversation_ended` | It ended without the customer ending it |
146
+ | `new_chat` | The page is asking for a replacement conversation |
147
+ | `persist_ticket_id` | Store this, with the session, to resume later |
148
+ | `persist_session` | The other half of that pair |
149
+ | `logged_out` | `/logout` cleared this frame. See [Logging out](#logging-out) |
150
+ | `logout_failed` | `/logout` did not confirm. The widget stays unavailable |
151
+ | `error` | A page-level failure. `LORIKEET_ERROR_CODES` lists the codes |
152
+
153
+ Names match the web SDK's wherever it has one, so a host integrating both does
154
+ not learn two vocabularies.
155
+
156
+ `preload_complete` is in the union but cannot reach React Native: the preload
157
+ page posts it to a web parent only. Do not wait on it.
158
+
159
+ ## Logging out
160
+
161
+ Clearing your own stored `ticketId` and `session` is not enough. The chat keeps
162
+ cookies and its own storage inside the WebView, which native code cannot reach,
163
+ so the next person on the device can reopen the previous customer's
164
+ conversation. Only the chat's `/logout` page can clear those, and only if the
165
+ WebView is navigated to it.
166
+
167
+ With the drop-in, call `logout()` on its ref:
168
+
169
+ ```tsx
170
+ const widget = useRef<LorikeetWidgetHandle>(null)
171
+
172
+ return (
173
+ <>
174
+ <LorikeetWidgetView {...props} ref={widget} />
175
+ <Button
176
+ onPress={() => {
177
+ widget.current?.logout()
178
+ void clearResumeForCustomer(customerId)
179
+ }}
180
+ title="Sign out"
181
+ />
182
+ </>
183
+ )
184
+ ```
185
+
186
+ The widget stays on that page until the chat confirms, and nothing you pass it
187
+ in the meantime changes that: handing it the next customer's credentials while
188
+ the clear is still running would unmount the page mid-clear and leave the last
189
+ customer's cookies on the device. Pass them once `logged_out` has arrived and
190
+ the next chat starts normally.
191
+
192
+ A logout that does not confirm within ten seconds reports `logout_failed` and
193
+ leaves the widget unavailable rather than opening a chat on a device the
194
+ previous customer's credentials still unlock. Call `logout()` again to retry.
195
+
196
+ That obligation outlives the component. The SDK stores nothing, so a widget
197
+ unmounted before the clear confirmed, by a screen change or an app restart,
198
+ comes back with no memory of it and opens a chat normally, on a device whose
199
+ cookies were never cleared. Record the failure alongside the conversation you
200
+ already store, and call `logout()` again before mounting a chat for anyone
201
+ else. We would rather own this than ask you to, and
202
+ [ADP-3160](https://linear.app/lorikeet/issue/ADP-3160) is where that storage
203
+ layer belongs.
204
+
205
+ If you compose the parts yourself, build the URL and navigate to it:
206
+
207
+ ```tsx
208
+ const client = new LorikeetClient({ public_key: publicKey, api_base: baseUrl })
209
+
210
+ setUri(client.getLogoutUrl())
211
+ ```
212
+
213
+ Either way, wait for `logged_out` before treating the device as clean. The
214
+ WebView's load event does not prove anything: an error page fires it too.
215
+
216
+ The anonymous customer's long-lived token is deliberately not among these
217
+ events. It identifies the same anonymous customer for a year, this SDK offers
218
+ no way to hand one back, and the web SDK keeps the same message inside the
219
+ frame. A host that genuinely needs it can read the raw bridge, but storing it
220
+ is storing a credential, and [ADP-3160](https://linear.app/lorikeet/issue/ADP-3160)
221
+ is the work that should remove the need.
222
+
223
+ ## Identity
224
+
225
+ The public key, base URL and auth token are one unit, and a conversation belongs
226
+ to the credentials that created it. Never take some of the three from one source
227
+ and the rest from another, even briefly during a render.
228
+
229
+ When any of them change, which includes logout and switching environment, clear
230
+ your stored `ticketId` and `session` at the same time. Keeping them across a
231
+ credential change either resumes the previous customer's conversation on a
232
+ shared device or sends a ticket id to a backend that has never heard of it.
233
+
234
+ Anything you load or persist asynchronously needs to be discarded if the
235
+ credentials moved on while it was in flight; a write that lands late will
236
+ otherwise put the old conversation back.
237
+
238
+ ## App lifecycle
239
+
240
+ The drop-in handles two failures that a WebView does not recover from on its own:
241
+
242
+ - **Renderer death.** When the OS kills the WebView's content process, the view
243
+ goes permanently blank with no error. The SDK detects this on both platforms
244
+ and remounts.
245
+ - **A stale stream after a long background.** The chat page gives its message
246
+ stream ten attempts with backoff capped at 30s, then stops trying. If the app
247
+ was backgrounded past that budget, the SDK remounts on return rather than
248
+ showing a conversation whose stream is dead. Shorter absences are left alone,
249
+ so an app switch does not cost the customer a draft message.
250
+
251
+ In both cases the remount resumes the conversation that was on screen. The SDK
252
+ watches the bridge for the live ticket and its session, so a conversation created
253
+ by a mount that had no `ticketId` is not lost, which is what a naive reload would
254
+ do: it would load `/widget` again and mint a second ticket. That memory
255
+ lasts for the life of the component and is scoped to the credentials that
256
+ produced it; surviving a process death is still your storage's job.
257
+
258
+ If you compose the parts yourself rather than using `LorikeetWidgetView`, both
259
+ are yours to handle.
260
+
261
+ ## Customisation encoding
262
+
263
+ `customisations` is serialised as base64 of its UTF-8 bytes, which is what the
264
+ chat page decodes. Any Unicode is safe, including emoji and typographic
265
+ punctuation. Earlier versions used the global `btoa`, which accepts Latin-1
266
+ only: a title such as `Self's AI Agent` with a typographic apostrophe threw
267
+ during render.
35
268
 
36
269
  ## Headless components
37
270
 
@@ -71,7 +304,8 @@ WebView component that handles the widget content.
71
304
 
72
305
  - `uri` (required): The URI that the web view will load
73
306
  - `baseUrl` (optional): Base URL for the Lorikeet API (defaults to `https://chat.lorikeetcx.ai`)
74
- - `webviewProps` (optional): Passed through to the webview component
307
+ - `webviewProps` (optional): Passed through to the webview component. The SDK
308
+ adds external-link handling on `onNavigationStateChange` and then calls yours
75
309
 
76
310
  ## Development
77
311