opendesk-react-native 0.0.0-stage → 0.1.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/lib/client.js ADDED
@@ -0,0 +1,505 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenDesk = void 0;
4
+ const api_1 = require("./api");
5
+ const errors_1 = require("./errors");
6
+ const realtime_1 = require("./realtime");
7
+ const storage_1 = require("./storage");
8
+ const TYPING_TTL_MS = 6000;
9
+ const AI_REPLY_TTL_MS = 60000;
10
+ const initialState = {
11
+ status: 'idle',
12
+ error: null,
13
+ config: null,
14
+ session: null,
15
+ connected: false,
16
+ conversations: [],
17
+ messages: {},
18
+ typing: {},
19
+ aiReplies: {},
20
+ unread: {},
21
+ activeConversationId: null,
22
+ };
23
+ let localIds = 0;
24
+ /**
25
+ * Headless openDesk chat client: owns the visitor session, the realtime socket and the
26
+ * conversation state. Subscribe with `subscribe` (or the React hooks) to render it.
27
+ */
28
+ class OpenDesk {
29
+ constructor(options) {
30
+ this.state = initialState;
31
+ this.identity = {};
32
+ this.realtime = null;
33
+ this.bootPromise = null;
34
+ this.subscribers = new Set();
35
+ this.messageListeners = new Set();
36
+ this.typingTimers = {};
37
+ /** Files uploaded this session, so a message can show its attachments before the server answers. */
38
+ this.uploads = new Map();
39
+ this.getState = () => this.state;
40
+ this.subscribe = (listener) => {
41
+ this.subscribers.add(listener);
42
+ return () => this.subscribers.delete(listener);
43
+ };
44
+ this.api = new api_1.OpenDeskApi(options);
45
+ this.storage = options.storage ?? (0, storage_1.memoryStorage)();
46
+ this.storageKey = `opendesk.visitor.${options.widgetKey}`;
47
+ this.socketFactory = options.socketFactory ?? realtime_1.phoenixSocketFactory;
48
+ }
49
+ /** Called for every agent or AI reply, e.g. to show an in-app banner. */
50
+ onMessage(listener) {
51
+ this.messageListeners.add(listener);
52
+ return () => this.messageListeners.delete(listener);
53
+ }
54
+ get unreadCount() {
55
+ return Object.values(this.state.unread).reduce((total, count) => total + count, 0);
56
+ }
57
+ /** Loads the widget config and starts (or resumes) the visitor session. Safe to call repeatedly. */
58
+ boot() {
59
+ if (!this.bootPromise) {
60
+ this.bootPromise = this.runBoot().catch((error) => {
61
+ this.bootPromise = null;
62
+ this.set({ status: 'error', error: errorMessage(error) });
63
+ throw error;
64
+ });
65
+ }
66
+ return this.bootPromise;
67
+ }
68
+ /**
69
+ * Attaches the signed-in user. A different `user_id` from the one before drops the stored
70
+ * visitor token, so one user never resumes another's conversations on a shared device.
71
+ */
72
+ async identify(identity) {
73
+ const switchedUser = Boolean(this.identity.user_id) && this.identity.user_id !== identity.user_id;
74
+ this.identity = { ...(switchedUser ? {} : this.identity), ...identity };
75
+ if (switchedUser) {
76
+ this.resetConversations();
77
+ await this.forgetToken();
78
+ }
79
+ if (this.bootPromise) {
80
+ await this.bootPromise.catch(() => undefined);
81
+ if (this.state.status === 'ready') {
82
+ await this.startSession();
83
+ }
84
+ }
85
+ }
86
+ /** Ends the session and forgets the visitor. Call it when the user signs out. */
87
+ async shutdown() {
88
+ this.realtime?.stop();
89
+ this.realtime = null;
90
+ Object.values(this.typingTimers).forEach(clearTimeout);
91
+ await this.api.endSession().catch(() => undefined);
92
+ await this.forgetToken();
93
+ this.identity = {};
94
+ this.bootPromise = null;
95
+ this.state = { ...initialState, config: this.state.config };
96
+ this.emitChange();
97
+ }
98
+ loadConversations() {
99
+ return this.authorized(() => this.fetchConversations());
100
+ }
101
+ /** Marks a conversation as on screen: loads its messages and clears its unread count. */
102
+ async openConversation(conversationId) {
103
+ this.set((state) => ({
104
+ activeConversationId: conversationId,
105
+ unread: withoutKey(state.unread, conversationId),
106
+ }));
107
+ const messages = await this.refreshMessages(conversationId);
108
+ this.api.markSeen(conversationId).catch(() => undefined);
109
+ return messages;
110
+ }
111
+ closeConversation() {
112
+ this.realtime?.typingDone();
113
+ this.set({ activeConversationId: null });
114
+ }
115
+ async refreshMessages(conversationId) {
116
+ const messages = await this.authorized(() => this.api.messages(conversationId));
117
+ this.applyMessages(conversationId, messages, true);
118
+ return this.state.messages[conversationId] ?? [];
119
+ }
120
+ async startConversation(input) {
121
+ const { conversation, message } = await this.authorized(() => this.api.startConversation(input));
122
+ this.set((state) => ({
123
+ conversations: [conversation, ...state.conversations.filter((c) => c.id !== conversation.id)],
124
+ activeConversationId: conversation.id,
125
+ }));
126
+ if (message) {
127
+ this.applyMessages(conversation.id, [message], false);
128
+ }
129
+ return conversation;
130
+ }
131
+ /** Sends a reply, showing it at once and swapping in the server copy when it returns. */
132
+ async sendMessage(conversationId, input) {
133
+ const localId = `local-${++localIds}`;
134
+ const local = {
135
+ id: localId,
136
+ ticket_id: conversationId,
137
+ kind: 'reply',
138
+ direction: 'inbound',
139
+ author: { type: 'contact', contact: this.state.session?.contact ?? null },
140
+ body_text: input.body ?? '',
141
+ body_html: '',
142
+ attachments: (input.attachment_ids ?? []).flatMap((id) => this.uploads.get(id) ?? []),
143
+ created_at: new Date().toISOString(),
144
+ };
145
+ this.applyMessages(conversationId, [local], false);
146
+ this.realtime?.typingDone();
147
+ try {
148
+ const message = await this.authorized(() => this.api.sendMessage(conversationId, input));
149
+ this.applyMessages(conversationId, [message], false, localId);
150
+ this.touchConversation(conversationId, message);
151
+ return message;
152
+ }
153
+ catch (error) {
154
+ this.set((state) => ({
155
+ messages: {
156
+ ...state.messages,
157
+ [conversationId]: (state.messages[conversationId] ?? []).filter((m) => m.id !== localId),
158
+ },
159
+ }));
160
+ throw error;
161
+ }
162
+ }
163
+ async upload(file) {
164
+ const attachment = await this.authorized(() => this.api.upload(file));
165
+ this.uploads.set(attachment.id, attachment);
166
+ return attachment;
167
+ }
168
+ typing(conversationId) {
169
+ this.realtime?.typing(conversationId);
170
+ }
171
+ async requestHuman(conversationId) {
172
+ await this.authorized(() => this.api.handoff(conversationId));
173
+ this.set((state) => ({
174
+ conversations: state.conversations.map((c) => (c.id === conversationId ? { ...c, ai_handoff: true } : c)),
175
+ }));
176
+ }
177
+ decideAction(conversationId, actionId, decision) {
178
+ return this.authorized(() => this.api.decideAction(conversationId, actionId, decision));
179
+ }
180
+ rateMessage(conversationId, messageId, rating) {
181
+ return this.authorized(() => this.api.rateMessage(conversationId, messageId, rating));
182
+ }
183
+ async runBoot() {
184
+ this.set({ status: 'booting', error: null });
185
+ if (!this.api.visitorToken) {
186
+ this.api.visitorToken = await this.storage.getItem(this.storageKey).catch(() => null);
187
+ }
188
+ const config = await this.api.config();
189
+ this.set({ config });
190
+ await this.startSession();
191
+ this.set({ status: 'ready' });
192
+ }
193
+ async startSession() {
194
+ const session = await this.openSession();
195
+ this.set({ session, error: null });
196
+ this.realtime?.stop();
197
+ this.realtime = new realtime_1.OpenDeskRealtime(this.socketFactory, session.gateway_url, session.ws_token, session.contact.id, async () => (await this.openSession()).ws_token, {
198
+ onEvent: (event) => this.onRealtimeEvent(event),
199
+ onLive: (event) => this.onLiveEvent(event),
200
+ onTyping: (event) => this.onAgentTyping(event.ticket_id, event.user?.name || 'Support'),
201
+ onStatus: (connected) => this.set({ connected }),
202
+ onReconnect: () => void this.catchUp(),
203
+ });
204
+ this.realtime.start();
205
+ await this.fetchConversations();
206
+ }
207
+ async fetchConversations() {
208
+ const conversations = await this.api.conversations();
209
+ conversations.sort((a, b) => Date.parse(b.last_message_at) - Date.parse(a.last_message_at));
210
+ this.set({ conversations });
211
+ return conversations;
212
+ }
213
+ /** Resumes the stored session; a revoked or expired token falls back to a new one. */
214
+ async openSession() {
215
+ let session;
216
+ try {
217
+ session = await this.api.session(this.identity);
218
+ }
219
+ catch (error) {
220
+ if (!(error instanceof errors_1.OpenDeskError) || !this.api.visitorToken || error.status < 400 || error.status >= 500) {
221
+ throw error;
222
+ }
223
+ this.api.visitorToken = null;
224
+ session = await this.api.session(this.identity);
225
+ }
226
+ await this.storage.setItem(this.storageKey, session.visitor_token).catch(() => undefined);
227
+ return session;
228
+ }
229
+ /** Runs a visitor request, re-opening the session once if the token was revoked. */
230
+ async authorized(request) {
231
+ await this.boot();
232
+ try {
233
+ return await request();
234
+ }
235
+ catch (error) {
236
+ if (!(error instanceof errors_1.OpenDeskError) || !error.isUnauthorized) {
237
+ throw error;
238
+ }
239
+ this.api.visitorToken = null;
240
+ await this.startSession();
241
+ return request();
242
+ }
243
+ }
244
+ async catchUp() {
245
+ await this.loadConversations().catch(() => undefined);
246
+ const active = this.state.activeConversationId;
247
+ if (active) {
248
+ await this.refreshMessages(active).catch(() => undefined);
249
+ }
250
+ }
251
+ onRealtimeEvent(event) {
252
+ const conversationId = event.data.ticket_id ?? event.data.message?.ticket_id;
253
+ if (event.type === 'message.withdrawn') {
254
+ const messageId = event.data.message_id;
255
+ if (conversationId && messageId) {
256
+ this.set((state) => ({
257
+ messages: {
258
+ ...state.messages,
259
+ [conversationId]: (state.messages[conversationId] ?? []).filter((m) => m.id !== messageId),
260
+ },
261
+ }));
262
+ }
263
+ return;
264
+ }
265
+ if (event.type !== 'message.created' && event.type !== 'message.updated') {
266
+ return;
267
+ }
268
+ const message = event.data.message;
269
+ if (!message || !conversationId || message.kind === 'note') {
270
+ return;
271
+ }
272
+ this.applyMessages(conversationId, [message], false);
273
+ if (event.type === 'message.updated') {
274
+ return;
275
+ }
276
+ if (this.state.conversations.some((c) => c.id === conversationId)) {
277
+ this.touchConversation(conversationId, message);
278
+ }
279
+ else {
280
+ void this.loadConversations().catch(() => undefined);
281
+ }
282
+ if (message.direction !== 'outbound') {
283
+ return;
284
+ }
285
+ this.clearTyping(conversationId);
286
+ if (this.state.activeConversationId === conversationId) {
287
+ this.api.markSeen(conversationId).catch(() => undefined);
288
+ }
289
+ else {
290
+ this.set((state) => ({
291
+ unread: { ...state.unread, [conversationId]: (state.unread[conversationId] ?? 0) + 1 },
292
+ }));
293
+ }
294
+ const from = message.author.user?.name || message.author.bot?.name || this.state.config?.workspace_name || 'Support';
295
+ this.messageListeners.forEach((listener) => {
296
+ try {
297
+ listener({ conversationId, message, from });
298
+ }
299
+ catch {
300
+ // A listener's failure must not break the stream.
301
+ }
302
+ });
303
+ }
304
+ onLiveEvent(event) {
305
+ const { reply_id: replyId, ticket_id: conversationId } = event.data;
306
+ if (typeof replyId !== 'string' || typeof conversationId !== 'string') {
307
+ return;
308
+ }
309
+ const now = Date.now();
310
+ this.set((state) => {
311
+ const replies = pruneReplies(state.aiReplies, now);
312
+ const current = replies[replyId] ?? {
313
+ id: replyId,
314
+ conversationId,
315
+ text: '',
316
+ phase: 'typing',
317
+ tools: [],
318
+ bot: null,
319
+ messageId: null,
320
+ handoff: false,
321
+ actionId: null,
322
+ updatedAt: now,
323
+ };
324
+ switch (event.type) {
325
+ case 'ai.reply.started':
326
+ return { aiReplies: { ...replies, [replyId]: { ...current, bot: event.data.bot ?? current.bot, updatedAt: now } } };
327
+ case 'ai.reply.delta':
328
+ if (!event.data.text) {
329
+ return {};
330
+ }
331
+ return {
332
+ aiReplies: {
333
+ ...replies,
334
+ [replyId]: { ...current, text: current.text + event.data.text, phase: 'streaming', updatedAt: now },
335
+ },
336
+ };
337
+ case 'ai.reply.tool': {
338
+ const name = event.data.tool;
339
+ if (!name) {
340
+ return {};
341
+ }
342
+ const status = event.data.status === 'done' || event.data.status === 'error' ? event.data.status : 'running';
343
+ const tools = [...current.tools];
344
+ const running = tools.map((t) => t.name === name && t.status === 'running').lastIndexOf(true);
345
+ if (running >= 0) {
346
+ tools[running] = { name, status };
347
+ }
348
+ else {
349
+ tools.push({ name, status });
350
+ }
351
+ return { aiReplies: { ...replies, [replyId]: { ...current, tools, updatedAt: now } } };
352
+ }
353
+ case 'ai.reply.ended': {
354
+ const messageId = event.data.message_id || null;
355
+ const landed = !messageId || (state.messages[conversationId] ?? []).some((m) => m.id === messageId);
356
+ if (landed) {
357
+ return { aiReplies: withoutKey(replies, replyId) };
358
+ }
359
+ return {
360
+ aiReplies: {
361
+ ...replies,
362
+ [replyId]: {
363
+ ...current,
364
+ phase: 'done',
365
+ tools: current.tools.map((t) => (t.status === 'running' ? { ...t, status: 'done' } : t)),
366
+ messageId,
367
+ handoff: event.data.handoff === true,
368
+ actionId: event.data.action_id ?? null,
369
+ updatedAt: now,
370
+ },
371
+ },
372
+ };
373
+ }
374
+ default:
375
+ return {};
376
+ }
377
+ });
378
+ }
379
+ onAgentTyping(conversationId, name) {
380
+ this.set((state) => ({ typing: { ...state.typing, [conversationId]: name } }));
381
+ clearTimeout(this.typingTimers[conversationId]);
382
+ this.typingTimers[conversationId] = setTimeout(() => this.clearTyping(conversationId), TYPING_TTL_MS);
383
+ }
384
+ clearTyping(conversationId) {
385
+ clearTimeout(this.typingTimers[conversationId]);
386
+ delete this.typingTimers[conversationId];
387
+ if (conversationId in this.state.typing) {
388
+ this.set((state) => ({ typing: withoutKey(state.typing, conversationId) }));
389
+ }
390
+ }
391
+ /**
392
+ * Merges messages by id in time order; `replace` makes the list exactly the server's copy.
393
+ * A stored message takes over the render key of the optimistic copy (`replacesLocalId`, or
394
+ * a pending copy with the same text) or of the AI stream it finishes, so its row never remounts.
395
+ */
396
+ applyMessages(conversationId, incoming, replace, replacesLocalId) {
397
+ this.set((state) => {
398
+ const existing = state.messages[conversationId] ?? [];
399
+ const existingById = new Map(existing.map((m) => [m.id, m]));
400
+ const pending = existing.filter((m) => m.id.startsWith('local-'));
401
+ const usedLocalIds = new Set();
402
+ const finishedReplyIds = new Set();
403
+ const keyed = incoming.map((message) => {
404
+ if (message.client_id) {
405
+ return message;
406
+ }
407
+ const previous = existingById.get(message.id);
408
+ if (previous?.client_id) {
409
+ return { ...message, client_id: previous.client_id };
410
+ }
411
+ if (message.direction === 'inbound' && !message.id.startsWith('local-')) {
412
+ const local = pending.find((m) => m.id === replacesLocalId) ??
413
+ pending.find((m) => !usedLocalIds.has(m.id) && (m.body_text ?? '') === (message.body_text ?? ''));
414
+ if (local) {
415
+ usedLocalIds.add(local.id);
416
+ return { ...message, client_id: local.client_id ?? local.id };
417
+ }
418
+ }
419
+ if (message.ai && message.direction === 'outbound') {
420
+ const reply = Object.values(state.aiReplies).find((r) => !finishedReplyIds.has(r.id) &&
421
+ (r.messageId === message.id || (r.conversationId === conversationId && r.phase !== 'done')));
422
+ if (reply) {
423
+ finishedReplyIds.add(reply.id);
424
+ return { ...message, client_id: `ai-${reply.id}` };
425
+ }
426
+ }
427
+ return message;
428
+ });
429
+ const base = (replace ? pending : existing).filter((m) => !usedLocalIds.has(m.id));
430
+ const byId = new Map(base.map((m) => [m.id, m]));
431
+ keyed.forEach((m) => byId.set(m.id, m));
432
+ const merged = [...byId.values()].sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
433
+ const landedIds = new Set(incoming.map((m) => m.id));
434
+ const aiReplies = Object.fromEntries(Object.entries(state.aiReplies).filter(([id, reply]) => !finishedReplyIds.has(id) && !(reply.messageId && landedIds.has(reply.messageId))));
435
+ return { messages: { ...state.messages, [conversationId]: merged }, aiReplies };
436
+ });
437
+ }
438
+ touchConversation(conversationId, message) {
439
+ this.set((state) => {
440
+ const conversation = state.conversations.find((c) => c.id === conversationId);
441
+ if (!conversation) {
442
+ return {};
443
+ }
444
+ return {
445
+ conversations: [
446
+ {
447
+ ...conversation,
448
+ last_message_at: message.created_at,
449
+ last_message_preview: previewOf(message),
450
+ },
451
+ ...state.conversations.filter((c) => c.id !== conversationId),
452
+ ],
453
+ };
454
+ });
455
+ }
456
+ resetConversations() {
457
+ this.realtime?.stop();
458
+ this.realtime = null;
459
+ this.set({
460
+ session: null,
461
+ connected: false,
462
+ conversations: [],
463
+ messages: {},
464
+ typing: {},
465
+ aiReplies: {},
466
+ unread: {},
467
+ activeConversationId: null,
468
+ });
469
+ }
470
+ async forgetToken() {
471
+ this.api.visitorToken = null;
472
+ await this.storage.removeItem(this.storageKey).catch(() => undefined);
473
+ }
474
+ set(patch) {
475
+ const next = typeof patch === 'function' ? patch(this.state) : patch;
476
+ if (Object.keys(next).length === 0) {
477
+ return;
478
+ }
479
+ this.state = { ...this.state, ...next };
480
+ this.emitChange();
481
+ }
482
+ emitChange() {
483
+ this.subscribers.forEach((listener) => listener());
484
+ }
485
+ }
486
+ exports.OpenDesk = OpenDesk;
487
+ function withoutKey(record, key) {
488
+ if (!(key in record)) {
489
+ return record;
490
+ }
491
+ const copy = { ...record };
492
+ delete copy[key];
493
+ return copy;
494
+ }
495
+ function pruneReplies(replies, now) {
496
+ return Object.fromEntries(Object.entries(replies).filter(([, reply]) => now - reply.updatedAt < AI_REPLY_TTL_MS));
497
+ }
498
+ /** A list preview for a message; an attachment-only message shows its file names. */
499
+ function previewOf(message) {
500
+ const text = (message.body_text ?? '').trim();
501
+ return (text || (message.attachments ?? []).map((a) => a.filename).join(', ')).slice(0, 140);
502
+ }
503
+ function errorMessage(error) {
504
+ return error instanceof Error ? error.message : 'Support is unavailable right now';
505
+ }
@@ -0,0 +1,8 @@
1
+ /** A failed openDesk request; `status` is 0 when the server could not be reached. */
2
+ export declare class OpenDeskError extends Error {
3
+ readonly status: number;
4
+ readonly code: string;
5
+ constructor(status: number, code: string, message: string);
6
+ /** The visitor token is unknown or revoked; a fresh session is needed. */
7
+ get isUnauthorized(): boolean;
8
+ }
package/lib/errors.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenDeskError = void 0;
4
+ /** A failed openDesk request; `status` is 0 when the server could not be reached. */
5
+ class OpenDeskError extends Error {
6
+ constructor(status, code, message) {
7
+ super(message);
8
+ this.status = status;
9
+ this.code = code;
10
+ this.name = 'OpenDeskError';
11
+ }
12
+ /** The visitor token is unknown or revoked; a fresh session is needed. */
13
+ get isUnauthorized() {
14
+ return this.status === 401;
15
+ }
16
+ }
17
+ exports.OpenDeskError = OpenDeskError;
package/lib/index.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ export { OpenDesk } from './client';
2
+ export type { AIReply, IncomingMessage, OpenDeskOptions, OpenDeskState } from './client';
3
+ export { OpenDeskApi } from './api';
4
+ export type { Fetcher, OpenDeskApiOptions } from './api';
5
+ export { OpenDeskError } from './errors';
6
+ export { OpenDeskRealtime, phoenixSocketFactory } from './realtime';
7
+ export type { RealtimeChannel, RealtimeSocket, SocketFactory } from './realtime';
8
+ export { keychainStorage, memoryStorage, secureStoreStorage } from './storage';
9
+ export type { ExpoSecureStoreModule, KeychainModule, TokenStorage } from './storage';
10
+ export { OpenDeskProvider, useConversation, useConversations, useOpenDesk, useOpenDeskState, useUnreadCount, } from './react';
11
+ export type { ConversationView, OpenDeskProviderProps } from './react';
12
+ export { OpenDeskChat } from './ui/OpenDeskChat';
13
+ export type { OpenDeskChatProps } from './ui/OpenDeskChat';
14
+ export { OpenDeskLauncher } from './ui/OpenDeskLauncher';
15
+ export type { OpenDeskLauncherProps } from './ui/OpenDeskLauncher';
16
+ export { defaultStrings } from './ui/strings';
17
+ export type { ChatStrings } from './ui/strings';
18
+ export type * from './types';
package/lib/index.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultStrings = exports.OpenDeskLauncher = exports.OpenDeskChat = exports.useUnreadCount = exports.useOpenDeskState = exports.useOpenDesk = exports.useConversations = exports.useConversation = exports.OpenDeskProvider = exports.secureStoreStorage = exports.memoryStorage = exports.keychainStorage = exports.phoenixSocketFactory = exports.OpenDeskRealtime = exports.OpenDeskError = exports.OpenDeskApi = exports.OpenDesk = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "OpenDesk", { enumerable: true, get: function () { return client_1.OpenDesk; } });
6
+ var api_1 = require("./api");
7
+ Object.defineProperty(exports, "OpenDeskApi", { enumerable: true, get: function () { return api_1.OpenDeskApi; } });
8
+ var errors_1 = require("./errors");
9
+ Object.defineProperty(exports, "OpenDeskError", { enumerable: true, get: function () { return errors_1.OpenDeskError; } });
10
+ var realtime_1 = require("./realtime");
11
+ Object.defineProperty(exports, "OpenDeskRealtime", { enumerable: true, get: function () { return realtime_1.OpenDeskRealtime; } });
12
+ Object.defineProperty(exports, "phoenixSocketFactory", { enumerable: true, get: function () { return realtime_1.phoenixSocketFactory; } });
13
+ var storage_1 = require("./storage");
14
+ Object.defineProperty(exports, "keychainStorage", { enumerable: true, get: function () { return storage_1.keychainStorage; } });
15
+ Object.defineProperty(exports, "memoryStorage", { enumerable: true, get: function () { return storage_1.memoryStorage; } });
16
+ Object.defineProperty(exports, "secureStoreStorage", { enumerable: true, get: function () { return storage_1.secureStoreStorage; } });
17
+ var react_1 = require("./react");
18
+ Object.defineProperty(exports, "OpenDeskProvider", { enumerable: true, get: function () { return react_1.OpenDeskProvider; } });
19
+ Object.defineProperty(exports, "useConversation", { enumerable: true, get: function () { return react_1.useConversation; } });
20
+ Object.defineProperty(exports, "useConversations", { enumerable: true, get: function () { return react_1.useConversations; } });
21
+ Object.defineProperty(exports, "useOpenDesk", { enumerable: true, get: function () { return react_1.useOpenDesk; } });
22
+ Object.defineProperty(exports, "useOpenDeskState", { enumerable: true, get: function () { return react_1.useOpenDeskState; } });
23
+ Object.defineProperty(exports, "useUnreadCount", { enumerable: true, get: function () { return react_1.useUnreadCount; } });
24
+ var OpenDeskChat_1 = require("./ui/OpenDeskChat");
25
+ Object.defineProperty(exports, "OpenDeskChat", { enumerable: true, get: function () { return OpenDeskChat_1.OpenDeskChat; } });
26
+ var OpenDeskLauncher_1 = require("./ui/OpenDeskLauncher");
27
+ Object.defineProperty(exports, "OpenDeskLauncher", { enumerable: true, get: function () { return OpenDeskLauncher_1.OpenDeskLauncher; } });
28
+ var strings_1 = require("./ui/strings");
29
+ Object.defineProperty(exports, "defaultStrings", { enumerable: true, get: function () { return strings_1.defaultStrings; } });
package/lib/react.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { type ReactNode } from 'react';
2
+ import { OpenDesk, type AIReply, type OpenDeskState } from './client';
3
+ import type { Conversation, Identity, Message } from './types';
4
+ export interface OpenDeskProviderProps {
5
+ client: OpenDesk;
6
+ /**
7
+ * The signed-in user, fetched from your backend (it holds the widget secret). Pass `null`
8
+ * after sign-out to end the session; leave it undefined for anonymous chat.
9
+ */
10
+ identity?: Identity | null;
11
+ /** Starts the session on mount; turn off to boot lazily when the chat is first opened. */
12
+ autoBoot?: boolean;
13
+ children: ReactNode;
14
+ }
15
+ export declare function OpenDeskProvider({ client, identity, autoBoot, children }: OpenDeskProviderProps): import("react").JSX.Element;
16
+ export declare function useOpenDesk(): OpenDesk;
17
+ export declare function useOpenDeskState<T>(selector: (state: OpenDeskState) => T): T;
18
+ export declare function useUnreadCount(): number;
19
+ export declare function useConversations(): {
20
+ conversations: Conversation[];
21
+ ready: boolean;
22
+ error: string | null;
23
+ };
24
+ export interface ConversationView {
25
+ messages: Message[];
26
+ /** AI answers still streaming in this conversation. */
27
+ aiReplies: AIReply[];
28
+ /** Name of the agent typing, if any. */
29
+ typing: string | null;
30
+ conversation: Conversation | null;
31
+ }
32
+ /** Opens a conversation while mounted: loads its messages, keeps it read, and closes it on unmount. */
33
+ export declare function useConversation(conversationId: string | null): ConversationView;